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.
This commit is contained in:
Torsten Grote
2025-12-01 13:49:51 -03:00
parent 14f0d70d2c
commit 2a6736096c
5 changed files with 18 additions and 3 deletions

View File

@@ -128,6 +128,7 @@ class AppInstallManager @Inject constructor(
return currentState
}
val iconDownloadRequest = iconModel as? DownloadRequest
currentCoroutineContext().ensureActive()
val job = scope.async {
startInstall(
appMetadata = appMetadata,

View File

@@ -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<Pair<Repository, Exception>>()
// 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) {

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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)
}
}