feat(android): enable sticky notification service to prevent app from getting killed

This commit is contained in:
isra el
2025-05-31 13:59:20 +03:00
parent 61a666da35
commit cdf3b7bb51
5 changed files with 146 additions and 23 deletions

View File

@@ -18,4 +18,5 @@ public class AppConstants {
public static final String SHARED_PREFS_TRACK_SENT_SMS_STATUS_KEY = "TRACK_SENT_SMS_STATUS";
public static final String SHARED_PREFS_LAST_VERSION_CODE_KEY = "LAST_VERSION_CODE";
public static final String SHARED_PREFS_LAST_VERSION_NAME_KEY = "LAST_VERSION_NAME";
public static final String SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY = "STICKY_NOTIFICATION_ENABLED";
}

View File

@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.vernu.sms.services.StickyNotificationService;
import com.vernu.sms.helpers.SharedPreferenceHelper;
import java.util.ArrayList;
import java.util.List;
@@ -38,22 +39,34 @@ public class TextBeeUtils {
}
public static void startStickyNotificationService(Context context) {
if(!isPermissionGranted(context, Manifest.permission.RECEIVE_SMS)){
return;
}
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(notificationIntent);
// Only start service if user has enabled sticky notification
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
context,
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
false
);
if (stickyNotificationEnabled) {
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(notificationIntent);
} else {
context.startService(notificationIntent);
}
Log.i(TAG, "Starting sticky notification service");
} else {
context.startService(notificationIntent);
Log.i(TAG, "Sticky notification disabled by user, not starting service");
}
}
public static void stopStickyNotificationService(Context context) {
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
context.stopService(notificationIntent);
Log.i(TAG, "Stopping sticky notification service");
}
/**

View File

@@ -42,7 +42,7 @@ import retrofit2.Response;
public class MainActivity extends AppCompatActivity {
private Context mContext;
private Switch gatewaySwitch, receiveSMSSwitch;
private Switch gatewaySwitch, receiveSMSSwitch, stickyNotificationSwitch;
private EditText apiKeyEditText, fcmTokenEditText, deviceIdEditText;
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn, checkUpdatesBtn;
private ImageButton copyDeviceIdImgBtn;
@@ -62,6 +62,7 @@ public class MainActivity extends AppCompatActivity {
setContentView(R.layout.activity_main);
gatewaySwitch = findViewById(R.id.gatewaySwitch);
receiveSMSSwitch = findViewById(R.id.receiveSMSSwitch);
stickyNotificationSwitch = findViewById(R.id.stickyNotificationSwitch);
apiKeyEditText = findViewById(R.id.apiKeyEditText);
fcmTokenEditText = findViewById(R.id.fcmTokenEditText);
deviceIdEditText = findViewById(R.id.deviceIdEditText);
@@ -98,6 +99,14 @@ public class MainActivity extends AppCompatActivity {
crashlytics.setCustomKey("app_version", versionName);
crashlytics.setCustomKey("app_version_code", BuildConfig.VERSION_CODE);
// Start sticky notification service if enabled
boolean gatewayEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, false);
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, false);
if (gatewayEnabled && stickyNotificationEnabled) {
TextBeeUtils.startStickyNotificationService(mContext);
Log.d(TAG, "Starting sticky notification service on app start");
}
if (deviceId == null || deviceId.isEmpty()) {
registerDeviceBtn.setText("Register");
} else {
@@ -150,11 +159,14 @@ public class MainActivity extends AppCompatActivity {
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, isCheked);
boolean enabled = Boolean.TRUE.equals(Objects.requireNonNull(response.body()).data.get("enabled"));
compoundButton.setChecked(enabled);
// if (enabled) {
// TextBeeUtils.startStickyNotificationService(mContext);
// } else {
// TextBeeUtils.stopStickyNotificationService(mContext);
// }
if (enabled) {
// Check if sticky notification is enabled
if (SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, false)) {
TextBeeUtils.startStickyNotificationService(mContext);
}
} else {
TextBeeUtils.stopStickyNotificationService(mContext);
}
compoundButton.setEnabled(true);
}
@Override
@@ -176,6 +188,21 @@ public class MainActivity extends AppCompatActivity {
Snackbar.make(view, "Receive SMS " + (isCheked ? "enabled" : "disabled"), Snackbar.LENGTH_LONG).show();
});
// Setup sticky notification switch
stickyNotificationSwitch.setChecked(SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, false));
stickyNotificationSwitch.setOnCheckedChangeListener((compoundButton, isChecked) -> {
View view = compoundButton.getRootView();
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY, isChecked);
if (isChecked) {
TextBeeUtils.startStickyNotificationService(mContext);
Snackbar.make(view, "Background service enabled - app will be more reliable", Snackbar.LENGTH_LONG).show();
} else {
TextBeeUtils.stopStickyNotificationService(mContext);
Snackbar.make(view, "Background service disabled - app may be killed when in background", Snackbar.LENGTH_LONG).show();
}
});
// TODO: check gateway status/api key/device validity and update UI accordingly
registerDeviceBtn.setOnClickListener(view -> {
String _deviceId = SharedPreferenceHelper.getSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, "");

View File

@@ -15,6 +15,8 @@ import androidx.core.app.NotificationCompat;
import com.vernu.sms.R;
import com.vernu.sms.activities.MainActivity;
import com.vernu.sms.receivers.SMSBroadcastReceiver;
import com.vernu.sms.AppConstants;
import com.vernu.sms.helpers.SharedPreferenceHelper;
public class StickyNotificationService extends Service {
@@ -32,15 +34,25 @@ public class StickyNotificationService extends Service {
super.onCreate();
Log.i(TAG, "Service Started");
// Only register receiver and show notification if enabled in preferences
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
getApplicationContext(),
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
false
);
// IntentFilter filter = new IntentFilter();
// filter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
// filter.addAction(android.telephony.TelephonyManager.ACTION_PHONE_STATE_CHANGED);
// registerReceiver(receiver, filter);
//
// Notification notification = createNotification();
// startForeground(1, notification);
if (stickyNotificationEnabled) {
IntentFilter filter = new IntentFilter();
filter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
filter.addAction(android.telephony.TelephonyManager.ACTION_PHONE_STATE_CHANGED);
registerReceiver(receiver, filter);
Notification notification = createNotification();
startForeground(1, notification);
Log.i(TAG, "Started foreground service with sticky notification");
} else {
Log.i(TAG, "Sticky notification disabled by user preference");
}
}
@Override
@@ -52,9 +64,19 @@ public class StickyNotificationService extends Service {
@Override
public void onDestroy() {
super.onDestroy();
// unregisterReceiver(receiver);
// Only unregister if we had registered in the first place
boolean stickyNotificationEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(
getApplicationContext(),
AppConstants.SHARED_PREFS_STICKY_NOTIFICATION_ENABLED_KEY,
false
);
if (stickyNotificationEnabled) {
unregisterReceiver(receiver);
}
Log.i(TAG, "StickyNotificationService destroyed");
// Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show();
}
private Notification createNotification() {
@@ -72,10 +94,19 @@ public class StickyNotificationService extends Service {
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Notification.Builder builder = new Notification.Builder(this, notificationChannelId);
return builder.setContentTitle("TextBee is running").setContentText("TextBee is running in the background.").setContentIntent(pendingIntent).setOngoing(true).setSmallIcon(R.drawable.ic_launcher_foreground).build();
return builder.setContentTitle("TextBee Active")
.setContentText("SMS gateway service is active")
.setContentIntent(pendingIntent)
.setOngoing(true)
.setSmallIcon(R.mipmap.ic_launcher)
.build();
} else {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationChannelId);
return builder.setContentTitle("TextBee is running").setContentText("TextBee is running in the background.").setOngoing(true).setSmallIcon(R.drawable.ic_launcher_foreground).build();
return builder.setContentTitle("TextBee Active")
.setContentText("SMS gateway service is active")
.setOngoing(true)
.setSmallIcon(R.mipmap.ic_launcher)
.build();
}
}

View File

@@ -371,6 +371,57 @@
android:minHeight="32dp" />
</LinearLayout>
<!-- Sticky Notification Setting -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sticky Notification"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_dialog_info"
android:tint="?attr/colorPrimary"
android:layout_marginEnd="4dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Prevents app from being killed by the system (optional, but recommended for reliability)"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<Switch
android:id="@+id/stickyNotificationSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="32dp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"