From a5d235e559edec51204c1551d20ee4aaddb8588f Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Wed, 9 Nov 2022 13:01:33 +0300 Subject: [PATCH] Fixing ForegroundServiceDidNotStartInTimeException --- .../java/chat/simplex/app/SimplexService.kt | 61 ++++++++++++------- .../java/chat/simplex/app/model/SimpleXAPI.kt | 2 +- .../chat/simplex/app/views/call/CallView.kt | 2 +- .../app/views/database/DatabaseView.kt | 2 +- .../usersettings/NotificationsSettingsView.kt | 2 +- 5 files changed, 42 insertions(+), 27 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt index b567e0d8c..4f5eb464b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexService.kt @@ -10,7 +10,6 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.* import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.onboarding.OnboardingStage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -20,7 +19,6 @@ import kotlinx.coroutines.withContext class SimplexService: Service() { private var wakeLock: PowerManager.WakeLock? = null - private var isServiceStarted = false private var isStartingService = false private var notificationManager: NotificationManager? = null private var serviceNotification: Notification? = null @@ -48,11 +46,32 @@ class SimplexService: Service() { notificationManager = createNotificationChannel() serviceNotification = createNotification(title, text) startForeground(SIMPLEX_SERVICE_ID, serviceNotification) + /** + * The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and + * we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown. + * To prevent that, we can call [stopSelf] only when the service made [startForeground] call + * */ + if (stopAfterStart) { + stopForeground(true) + stopSelf() + } else { + isServiceStarted = true + } } override fun onDestroy() { Log.d(TAG, "Simplex service destroyed") - stopService() + try { + wakeLock?.let { + while (it.isHeld) it.release() // release all, in case acquired more than once + } + wakeLock = null + } catch (e: Exception) { + Log.d(TAG, "Exception while releasing wakelock: ${e.message}") + } + isServiceStarted = false + stopAfterStart = false + saveServiceState(this, ServiceState.STOPPED) // If notification service is enabled and battery optimization is disabled, restart the service if (SimplexApp.context.allowToStartServiceAfterAppExit()) @@ -62,7 +81,7 @@ class SimplexService: Service() { private fun startService() { Log.d(TAG, "SimplexService startService") - if (isServiceStarted || isStartingService) return + if (wakeLock != null || isStartingService) return val self = this isStartingService = true withApi { @@ -73,10 +92,9 @@ class SimplexService: Service() { if (chatDbStatus != DBMigrationResult.OK) { Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus") showPassphraseNotification(chatDbStatus) - stopService() + safeStopService(self) return@withApi } - isServiceStarted = true saveServiceState(self, ServiceState.STARTED) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run { newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply { @@ -89,22 +107,6 @@ class SimplexService: Service() { } } - private fun stopService() { - Log.d(TAG, "Stopping foreground service") - try { - wakeLock?.let { - while (it.isHeld) it.release() // release all, in case acquired more than once - } - wakeLock = null - stopForeground(true) - stopSelf() - } catch (e: Exception) { - Log.d(TAG, "Service stopped without being started: ${e.message}") - } - isServiceStarted = false - saveServiceState(this, ServiceState.STOPPED) - } - private fun createNotificationChannel(): NotificationManager? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -235,6 +237,9 @@ class SimplexService: Service() { private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE" private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce" + private var isServiceStarted = false + private var stopAfterStart = false + fun scheduleStart(context: Context) { Log.d(TAG, "Enqueuing work to start subscriber service") val workManager = WorkManager.getInstance(context) @@ -244,7 +249,17 @@ class SimplexService: Service() { suspend fun start(context: Context) = serviceAction(context, Action.START) - fun stop(context: Context) = context.stopService(Intent(context, SimplexService::class.java)) + /** + * If there is a need to stop the service, use this function only. It makes sure that the service will be stopped without an + * exception related to foreground services lifecycle + * */ + fun safeStopService(context: Context) { + if (isServiceStarted) { + context.stopService(Intent(context, SimplexService::class.java)) + } else { + stopAfterStart = true + } + } private suspend fun serviceAction(context: Context, action: Action) { Log.d(TAG, "SimplexService serviceAction: ${action.name}") diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index a77cc0dfe..7cacfcfb9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1216,7 +1216,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a chatModel.notificationsMode.value = NotificationsMode.OFF SimplexService.StartReceiver.toggleReceiver(false) MessagesFetcherWorker.cancelAll() - SimplexService.stop(SimplexApp.context) + SimplexService.safeStopService(SimplexApp.context) } else { // show battery optimization notice showBGServiceNoticeIgnoreOptimization(mode) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt index 8bbbe9efd..6aebed488 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/call/CallView.kt @@ -63,7 +63,7 @@ fun ActiveCallView(chatModel: ChatModel) { DisposableEffect(Unit) { onDispose { // Stop it when call ended - if (!ntfModeService) SimplexService.stop(SimplexApp.context) + if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context) // Clear selected communication device to default value after we changed it in call if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index ba40dad85..b4cb0edf8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -409,7 +409,7 @@ private fun stopChat(m: ChatModel, runChat: MutableState, context: Cont m.controller.apiStopChat() runChat.value = false m.chatRunning.value = false - SimplexService.stop(context) + SimplexService.safeStopService(context) MessagesFetcherWorker.cancelAll() } catch (e: Error) { runChat.value = true diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt index 72f955f39..6206979e7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt @@ -57,7 +57,7 @@ fun NotificationsSettingsView( if (mode == NotificationsMode.SERVICE) SimplexService.start(SimplexApp.context) else - SimplexService.stop(SimplexApp.context) + SimplexService.safeStopService(SimplexApp.context) } if (mode != NotificationsMode.PERIODIC) {