diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 5d1f4b0b7..f2a3e51c4 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -674,6 +674,9 @@ class MainActivity : OrientationAwareActivity() { return@launch } + // Retry foreground-service startup now that runtime permissions are available. + try { com.bitchat.android.service.MeshForegroundService.start(applicationContext) } catch (_: Exception) { } + // Set up mesh service delegate and start services meshService.delegate = chatViewModel meshService.startServices() diff --git a/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt b/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt index aa9e6fc33..a439b5565 100644 --- a/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt +++ b/app/src/main/java/com/bitchat/android/service/MeshForegroundService.kt @@ -41,17 +41,20 @@ class MeshForegroundService : Service() { // On API >= 26, avoid background-service start restrictions by using startForegroundService // only when we can actually post a notification (Android 13+ requires runtime notif permission) val bgEnabled = MeshServicePreferences.isBackgroundEnabled(true) + val hasBtPerms = hasBluetoothPermissionsStatic(context) val hasNotifPerm = hasNotificationPermissionStatic(context) + val canStartAsForeground = bgEnabled && hasBtPerms && hasNotifPerm if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (bgEnabled && hasNotifPerm) { + if (canStartAsForeground) { context.startForegroundService(intent) } else { // Do not attempt to start a background service from headless context without notif permission - // or when background is disabled, to avoid BackgroundServiceStartNotAllowedException. + // or when background is disabled / bluetooth permission is missing, + // to avoid BackgroundServiceStartNotAllowedException and FGS start timeout crashes. android.util.Log.i( "MeshForegroundService", - "Not starting service on API>=26 (bgEnabled=$bgEnabled, hasNotifPerm=$hasNotifPerm)" + "Not starting service on API>=26 (bgEnabled=$bgEnabled, hasBtPerms=$hasBtPerms, hasNotifPerm=$hasNotifPerm)" ) } } else { @@ -68,9 +71,8 @@ class MeshForegroundService : Service() { * promoting/starting the foreground service immediately without polling. */ fun onNotificationPermissionGranted(context: Context) { - // If background is enabled and permission now granted, start/promo service - val hasNotifPerm = hasNotificationPermissionStatic(context) - if (!MeshServicePreferences.isBackgroundEnabled(true) || !hasNotifPerm) return + // If we can now fully run as foreground, start/promote immediately. + if (!shouldStartAsForeground(context)) return val intent = Intent(context, MeshForegroundService::class.java).apply { action = ACTION_UPDATE_NOTIFICATION } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -124,16 +126,6 @@ class MeshForegroundService : Service() { super.onCreate() notificationManager = NotificationManagerCompat.from(this) createChannel() - - // Ensure mesh service exists in holder (create if needed) - val existing = MeshServiceHolder.meshService - if (existing != null) { - Log.d("MeshForegroundService", "Using existing BluetoothMeshService from holder") - } else { - val created = MeshServiceHolder.getOrCreate(applicationContext) - Log.i("MeshForegroundService", "Created new BluetoothMeshService via holder") - MeshServiceHolder.attach(created) - } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -176,28 +168,21 @@ class MeshForegroundService : Service() { return START_NOT_STICKY } ACTION_UPDATE_NOTIFICATION -> { - // If we became eligible and are not in foreground yet, promote once - if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) { - val n = buildNotification(meshService?.getActivePeerCount() ?: 0) - startForegroundCompat(n) - isInForeground = true - } else { - updateNotification(force = true) - } + updateNotification(force = true) } else -> { /* ACTION_START or null */ } } - // Ensure mesh is running (only after permissions are granted) - ensureMeshStarted() - - // Promote exactly once when eligible, otherwise stay background (or stop) + // Promote as early as possible after startForegroundService() to avoid timeout. if (MeshServicePreferences.isBackgroundEnabled(true) && hasAllRequiredPermissions() && !isInForeground) { val notification = buildNotification(meshService?.getActivePeerCount() ?: 0) startForegroundCompat(notification) isInForeground = true } + // Ensure mesh is running (only after permissions are granted) + ensureMeshStarted() + // Periodically refresh the notification with live network size if (updateJob == null) { updateJob = scope.launch {