Convert BackupRestoreService to a WorkManager job

Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
This commit is contained in:
arkon 2023-03-19 17:28:59 -04:00
parent 14d1bcacc9
commit cdc160afc2
11 changed files with 124 additions and 198 deletions

View file

@ -209,10 +209,6 @@
android:name=".data.updater.AppUpdateService"
android:exported="false" />
<service
android:name=".data.backup.BackupRestoreService"
android:exported="false" />
<service android:name=".extension.util.ExtensionInstallService"
android:exported="false" />

View file

@ -41,9 +41,9 @@ import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
@ -93,7 +93,7 @@ object SettingsBackupScreen : SearchableSettings {
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
)
BackupCreatorJob.startNow(context, it, flag)
BackupCreateJob.startNow(context, it, flag)
}
flag = 0
}
@ -119,7 +119,7 @@ object SettingsBackupScreen : SearchableSettings {
subtitle = stringResource(R.string.pref_create_backup_summ),
onClick = {
scope.launch {
if (!BackupCreatorJob.isManualJobRunning(context)) {
if (!BackupCreateJob.isManualJobRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
@ -271,7 +271,7 @@ object SettingsBackupScreen : SearchableSettings {
confirmButton = {
TextButton(
onClick = {
BackupRestoreService.start(context, err.uri)
BackupRestoreJob.start(context, err.uri)
onDismissRequest()
},
) {
@ -301,7 +301,7 @@ object SettingsBackupScreen : SearchableSettings {
}
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
BackupRestoreService.start(context, it)
BackupRestoreJob.start(context, it)
return@rememberLauncherForActivityResult
}
@ -313,7 +313,7 @@ object SettingsBackupScreen : SearchableSettings {
title = stringResource(R.string.pref_restore_backup),
subtitle = stringResource(R.string.pref_restore_backup_summ),
onClick = {
if (!BackupRestoreService.isRunning(context)) {
if (!BackupRestoreJob.isRunning(context)) {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
@ -364,7 +364,7 @@ object SettingsBackupScreen : SearchableSettings {
168 to stringResource(R.string.update_weekly),
),
onValueChanged = {
BackupCreatorJob.setupTask(context, it)
BackupCreateJob.setupTask(context, it)
true
},
),

View file

@ -11,11 +11,11 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
/**
* Create a BitmapPainter from an drawable resource.
*
* > Only use this if [androidx.compose.ui.res.painterResource] doesn't work.
* Create a BitmapPainter from a drawable resource.
* Use this only if [androidx.compose.ui.res.painterResource] doesn't work.
*
* @param id the resource identifier
*
* @return the bitmap associated with the resource
*/
@Composable

View file

@ -8,7 +8,7 @@ import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.track.TrackManager
@ -57,7 +57,7 @@ object Migrations {
// Always set up background tasks to ensure they're running
LibraryUpdateJob.setupTask(context)
BackupCreatorJob.setupTask(context)
BackupCreateJob.setupTask(context)
// Fresh install
if (oldVersion == 0) {
@ -99,7 +99,7 @@ object Migrations {
if (oldVersion < 43) {
// Restore jobs after migrating from Evernote's job scheduler to WorkManager.
LibraryUpdateJob.setupTask(context)
BackupCreatorJob.setupTask(context)
BackupCreateJob.setupTask(context)
}
if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source
@ -249,7 +249,7 @@ object Migrations {
}
}
if (oldVersion < 76) {
BackupCreatorJob.setupTask(context)
BackupCreateJob.setupTask(context)
}
if (oldVersion < 77) {
val oldReaderTap = prefs.getBoolean("reader_tap", false)
@ -284,7 +284,7 @@ object Migrations {
}
if (backupPreferences.backupInterval().get() == 0) {
backupPreferences.backupInterval().set(12)
BackupCreatorJob.setupTask(context)
BackupCreateJob.setupTask(context)
}
}
if (oldVersion < 85) {

View file

@ -23,7 +23,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
class BackupCreateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val notifier = BackupNotifier(context)
@ -41,7 +41,6 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
}
context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
return try {
val location = BackupManager(context).createBackup(uri, flags, isAutoBackup)
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
@ -70,7 +69,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
val interval = prefInterval ?: backupPreferences.backupInterval().get()
val workManager = WorkManager.getInstance(context)
if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
val request = PeriodicWorkRequestBuilder<BackupCreateJob>(
interval.toLong(),
TimeUnit.HOURS,
10,
@ -92,7 +91,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
LOCATION_URI_KEY to uri.toString(),
BACKUP_FLAGS_KEY to flags,
)
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
val request = OneTimeWorkRequestBuilder<BackupCreateJob>()
.addTag(TAG_MANUAL)
.setInputData(inputData)
.build()

View file

@ -67,9 +67,7 @@ class BackupNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.backup_created))
setContentText(unifile.filePath ?: unifile.name)
// Clear old actions if they exist
clearActions()
addAction(
R.drawable.ic_share_24dp,
context.getString(R.string.action_share),
@ -91,12 +89,10 @@ class BackupNotifier(private val context: Context) {
setProgress(maxAmount, progress, false)
setOnlyAlertOnce(true)
// Clear old actions if they exist
clearActions()
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_stop),
context.getString(R.string.action_cancel),
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS),
)
}
@ -132,9 +128,7 @@ class BackupNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.restore_completed))
setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
// Clear old actions if they exist
clearActions()
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
val destFile = File(path, file)
val uri = destFile.getUriCompat(context)

View file

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.CancellationException
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
class BackupRestoreJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
private val notifier = BackupNotifier(context)
override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
?: return Result.failure()
try {
setForeground(getForegroundInfo())
} catch (e: IllegalStateException) {
logcat(LogPriority.ERROR, e) { "Not allowed to run on foreground service" }
}
return try {
val restorer = BackupRestorer(context, notifier)
restorer.restoreBackup(uri)
Result.success()
} catch (e: Exception) {
if (e is CancellationException) {
notifier.showRestoreError(context.getString(R.string.restoring_backup_canceled))
Result.success()
} else {
logcat(LogPriority.ERROR, e)
notifier.showRestoreError(e.message)
Result.failure()
}
} finally {
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
}
}
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(
Notifications.ID_RESTORE_PROGRESS,
notifier.showRestoreProgress().build(),
)
}
companion object {
fun isRunning(context: Context): Boolean {
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG).get()
return list.find { it.state == WorkInfo.State.RUNNING } != null
}
fun start(context: Context, uri: Uri) {
val inputData = workDataOf(
LOCATION_URI_KEY to uri.toString(),
)
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
.addTag(TAG)
.setInputData(inputData)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(TAG, ExistingWorkPolicy.KEEP, request)
}
fun stop(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork(TAG)
}
}
}
private const val TAG = "BackupRestore"
private const val LOCATION_URI_KEY = "location_uri" // String

View file

@ -1,141 +0,0 @@
package eu.kanade.tachiyomi.data.backup
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
/**
* Restores backup.
*/
class BackupRestoreService : Service() {
companion object {
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupRestoreService::class.java)
/**
* Starts a service to restore a backup from Json
*
* @param context context of application
* @param uri path of Uri
*/
fun start(context: Context, uri: Uri) {
if (isRunning(context)) return
val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
}
ContextCompat.startForegroundService(context, intent)
}
/**
* Stops the service.
*
* @param context the application context.
*/
fun stop(context: Context) {
context.stopService(Intent(context, BackupRestoreService::class.java))
BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled))
}
}
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var scope: CoroutineScope
private var restorer: BackupRestorer? = null
private lateinit var notifier: BackupNotifier
override fun onCreate() {
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
notifier = BackupNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
}
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
destroyJob()
}
private fun destroyJob() {
restorer?.job?.cancel()
scope.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
/**
* Method called when the service receives an intent.
*
* @param intent the start intent from.
* @param flags the flags of the command.
* @param startId the start id of this command.
* @return the start value of the command.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.getParcelableExtraCompat<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
// Cancel any previous job if needed.
restorer?.job?.cancel()
restorer = BackupRestorer(this, notifier)
val handler = CoroutineExceptionHandler { _, exception ->
logcat(LogPriority.ERROR, exception)
restorer?.writeErrorLog()
notifier.showRestoreError(exception.message)
stopSelf(startId)
}
val job = scope.launch(handler) {
if (restorer?.restoreBackup(uri) == false) {
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
}
}
job.invokeOnCompletion {
stopSelf(startId)
}
restorer?.job = job
return START_NOT_STICKY
}
}

View file

@ -9,8 +9,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.util.BackupUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import kotlinx.coroutines.Job
import okio.source
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
@ -24,8 +24,6 @@ class BackupRestorer(
private val notifier: BackupNotifier,
) {
var job: Job? = null
private var backupManager = BackupManager(context)
private var restoreAmount = 0
@ -56,7 +54,7 @@ class BackupRestorer(
return true
}
fun writeErrorLog(): File {
private fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val file = context.createFileInCacheDir("tachiyomi_restore.txt")
@ -90,18 +88,18 @@ class BackupRestorer(
val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
sourceMapping = backupMaps.associate { it.sourceId to it.name }
return coroutineScope {
// Restore individual manga
backup.backupManga.forEach {
if (job?.isActive != true) {
return false
if (!isActive) {
return@coroutineScope false
}
restoreManga(it, backup.backupCategories)
}
// TODO: optionally trigger online library + tracker update
return true
true
}
}
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {

View file

@ -6,10 +6,9 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.AppUpdateService
@ -82,10 +81,7 @@ class NotificationReceiver : BroadcastReceiver() {
"application/x-protobuf+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
ACTION_CANCEL_RESTORE -> cancelRestore(
context,
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1),
)
ACTION_CANCEL_RESTORE -> cancelRestore(context)
// Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
// Cancel downloading app update
@ -206,11 +202,9 @@ class NotificationReceiver : BroadcastReceiver() {
* Method called when user wants to stop a backup restore job.
*
* @param context context of application
* @param notificationId id of notification
*/
private fun cancelRestore(context: Context, notificationId: Int) {
BackupRestoreService.stop(context)
ContextCompat.getMainExecutor(context).execute { dismissNotification(context, notificationId) }
private fun cancelRestore(context: Context) {
BackupRestoreJob.stop(context)
}
/**

View file

@ -86,7 +86,6 @@
<string name="delete_category">Delete category</string>
<string name="action_edit_cover">Edit cover</string>
<string name="action_view_chapters">View chapters</string>
<string name="action_stop">Stop</string>
<string name="action_pause">Pause</string>
<string name="action_previous_chapter">Previous chapter</string>
<string name="action_next_chapter">Next chapter</string>