From 2a6736096c8f0b3bec0abb22ab81f430d1f1c2ca Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 1 Dec 2025 13:49:51 -0300 Subject: [PATCH] Improve handling of worker cancellation If we keep running our code, but the worker gets cancelled, the system may freeze our code execution making notifications hang. Also, it may cause the code to run more than once at roughly the same time, because the system reschedules our worker. --- .../main/kotlin/org/fdroid/install/AppInstallManager.kt | 1 + next/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt | 5 +++++ next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt | 3 +++ .../src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt | 8 +++++--- next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt | 4 ++++ 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt b/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt index 230ec3691..b72932520 100644 --- a/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt +++ b/next/src/main/kotlin/org/fdroid/install/AppInstallManager.kt @@ -128,6 +128,7 @@ class AppInstallManager @Inject constructor( return currentState } val iconDownloadRequest = iconModel as? DownloadRequest + currentCoroutineContext().ensureActive() val job = scope.async { startInstall( appMetadata = appMetadata, diff --git a/next/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt b/next/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt index 4ab01f18e..de8b5c154 100644 --- a/next/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt +++ b/next/src/main/kotlin/org/fdroid/repo/RepoUpdateManager.kt @@ -5,6 +5,8 @@ import android.text.format.Formatter import android.util.Log import androidx.annotation.WorkerThread import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map @@ -142,6 +144,7 @@ class RepoUpdateManager( @WorkerThread suspend fun updateRepos() { + currentCoroutineContext().ensureActive() if (isUpdating.value) { // This is a workaround for what looks like a WorkManager bug. // Sometimes it goes through scheduling/cancellation loops @@ -158,6 +161,7 @@ class RepoUpdateManager( } _isUpdating.value = true try { + currentCoroutineContext().ensureActive() var reposUpdated = false val repoErrors = mutableListOf>() // always get repos fresh from DB, because @@ -167,6 +171,7 @@ class RepoUpdateManager( // it might not be in the FDroidApp list, yet db.getRepositoryDao().getRepositories().forEach { repo -> if (!repo.enabled) return@forEach + currentCoroutineContext().ensureActive() // show notification if (isUpdateNotificationEnabled) { diff --git a/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt b/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt index e18465dbe..7ee9165be 100644 --- a/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt +++ b/next/src/main/kotlin/org/fdroid/repo/RepoUpdateWorker.kt @@ -20,6 +20,8 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import mu.KotlinLogging @@ -116,6 +118,7 @@ class RepoUpdateWorker @AssistedInject constructor( } val repoId = inputData.getLong("repoId", -1) return try { + currentCoroutineContext().ensureActive() if (repoId >= 0) repoUpdateManager.updateRepo(repoId) else repoUpdateManager.updateRepos() Result.success() diff --git a/next/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt b/next/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt index dbb371379..dd10d987b 100644 --- a/next/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt +++ b/next/src/main/kotlin/org/fdroid/updates/AppUpdateWorker.kt @@ -17,8 +17,11 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.joinAll import mu.KotlinLogging import org.fdroid.NotificationManager import org.fdroid.NotificationManager.Companion.NOTIFICATION_ID_APP_INSTALLS @@ -96,14 +99,13 @@ class AppUpdateWorker @AssistedInject constructor( log.error(e) { "Error while running setForeground: " } } return try { + currentCoroutineContext().ensureActive() nm.cancelAppUpdatesAvailableNotification() // Updating apps will try start a foreground service // and it will "share" the same notification. // This is easier than trying to tell the [AppInstallManager] // not to start a foreground service in this specific case. - updatesManager.updateAll().forEach { job -> - job.join() - } + updatesManager.updateAll().joinAll() // show success notification, if at least one app got installed val notificationState = appInstallManager.installNotificationState if (notificationState.numInstalled > 0) { diff --git a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt index d4c0e2760..5a65cd5a4 100644 --- a/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt +++ b/next/src/main/kotlin/org/fdroid/updates/UpdatesManager.kt @@ -5,6 +5,8 @@ import androidx.core.os.LocaleListCompat import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first @@ -117,10 +119,12 @@ class UpdatesManager @Inject constructor( val concurrencyLimit = min(Runtime.getRuntime().availableProcessors(), 8) val semaphore = Semaphore(concurrencyLimit) return appsToUpdate.map { update -> + currentCoroutineContext().ensureActive() // launch a new co-routine for each app to update coroutineScope.launch { // suspend here until we get a permit from the semaphore (there's free workers) semaphore.withPermit { + currentCoroutineContext().ensureActive() updateApp(update, canAskPreApprovalNow) } }