Move workers to foreground service context a bit more safely (#10202)

The system will crash the app if the worker that calls setForeground() finished
before the service runner be able to call Service.startForeground(). This edge
case is not handled by WorkManager and there is no way to check if the required
calls are done.

So here we suspend the worker by an arbitrary duration assuming the transition
to foreground service is done by then.
This commit is contained in:
Ivan Iskandar 2023-12-02 23:46:59 +07:00 committed by GitHub
parent dfa5c229b3
commit 24e1b4034e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 41 additions and 41 deletions

View File

@ -17,6 +17,7 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority import logcat.LogPriority
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
@ -39,19 +40,14 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry() if (isAutoBackup && BackupRestoreJob.isRunning(context)) return Result.retry()
val backupPreferences = Injekt.get<BackupPreferences>()
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: getAutomaticBackupLocation() ?: getAutomaticBackupLocation()
?: return Result.failure() ?: return Result.failure()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults) setForegroundSafely()
try { val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults)
setForeground(getForegroundInfo()) val backupPreferences = Injekt.get<BackupPreferences>()
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
}
return try { return try {
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup) val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)

View File

@ -12,6 +12,7 @@ import androidx.work.workDataOf
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import logcat.LogPriority import logcat.LogPriority
@ -29,11 +30,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
?: return Result.failure() ?: return Result.failure()
val sync = inputData.getBoolean(SYNC_KEY, false) val sync = inputData.getBoolean(SYNC_KEY, false)
try { setForegroundSafely()
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
}
return try { return try {
val restorer = BackupRestorer(context, notifier) val restorer = BackupRestorer(context, notifier)

View File

@ -16,11 +16,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.download.service.DownloadPreferences import tachiyomi.domain.download.service.DownloadPreferences
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -51,21 +50,18 @@ class DownloadJob(context: Context, workerParams: WorkerParameters) : CoroutineW
} }
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
try { var active = checkConnectivity() && downloadManager.downloaderStart()
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) { if (!active) {
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" } return Result.failure()
} }
var networkCheck = checkConnectivity() setForegroundSafely()
var active = networkCheck
downloadManager.downloaderStart()
// Keep the worker running when needed // Keep the worker running when needed
while (active) { while (active) {
delay(100) delay(100)
networkCheck = checkConnectivity() active = !isStopped && downloadManager.isRunning && checkConnectivity()
active = !isStopped && networkCheck && downloadManager.isRunning
} }
return Result.success() return Result.success()

View File

@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.createFileInCacheDir import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -108,11 +109,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
} }
} }
try { setForegroundSafely()
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
}
libraryPreferences.lastUpdatedTimestamp().set(Date().time) libraryPreferences.lastUpdatedTimestamp().set(Date().time)

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.system.isRunning import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -53,11 +54,7 @@ class MetadataUpdateJob(private val context: Context, workerParams: WorkerParame
private var mangaToUpdate: List<LibraryManga> = mutableListOf() private var mangaToUpdate: List<LibraryManga> = mutableListOf()
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
try { setForegroundSafely()
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
}
addMangaToQueue() addMangaToQueue()

View File

@ -17,13 +17,12 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.workManager import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import okhttp3.internal.http2.ErrorCode import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException import okhttp3.internal.http2.StreamResetException
import tachiyomi.core.i18n.stringResource import tachiyomi.core.i18n.stringResource
import tachiyomi.core.util.lang.withIOContext import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
@ -43,11 +42,7 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
return Result.failure() return Result.failure()
} }
try { setForegroundSafely()
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
}
withIOContext { withIOContext {
downloadApk(title, url) downloadApk(title, url)

View File

@ -1,8 +1,12 @@
package eu.kanade.tachiyomi.util.system package eu.kanade.tachiyomi.util.system
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import kotlinx.coroutines.delay
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
val Context.workManager: WorkManager val Context.workManager: WorkManager
get() = WorkManager.getInstance(this) get() = WorkManager.getInstance(this)
@ -11,3 +15,21 @@ fun WorkManager.isRunning(tag: String): Boolean {
val list = this.getWorkInfosByTag(tag).get() val list = this.getWorkInfosByTag(tag).get()
return list.any { it.state == WorkInfo.State.RUNNING } return list.any { it.state == WorkInfo.State.RUNNING }
} }
/**
* Makes this worker run in the context of a foreground service.
*
* Note that this function is a no-op if the process is subject to foreground
* service restrictions.
*
* Moving to foreground service context requires the worker to run a bit longer,
* allowing Service.startForeground() to be called and avoiding system crash.
*/
suspend fun CoroutineWorker.setForegroundSafely() {
try {
setForeground(getForegroundInfo())
delay(500)
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to set foreground job" }
}
}