diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 010ee07f0..d5664e443 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -209,10 +209,6 @@ android:name=".data.updater.AppUpdateService" android:exported="false" /> - - diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt index 9cf2bbe72..b28af0912 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsBackupScreen.kt @@ -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 }, ), diff --git a/app/src/main/java/eu/kanade/presentation/util/Resources.kt b/app/src/main/java/eu/kanade/presentation/util/Resources.kt index 44d9968b9..5b7bc9948 100644 --- a/app/src/main/java/eu/kanade/presentation/util/Resources.kt +++ b/app/src/main/java/eu/kanade/presentation/util/Resources.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 08447b980..d21a48721 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt similarity index 93% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index dfbff66b6..d3495c578 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreatorJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -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( + val request = PeriodicWorkRequestBuilder( 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() + val request = OneTimeWorkRequestBuilder() .addTag(TAG_MANUAL) .setInputData(inputData) .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt index 613f66bf2..c38fb2f2d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt new file mode 100644 index 000000000..4ec24679c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt @@ -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() + .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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt deleted file mode 100644 index 28b796586..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ /dev/null @@ -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(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 - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 29880a3e1..0ff096497 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -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 } - // Restore individual manga - backup.backupManga.forEach { - if (job?.isActive != true) { - return false + return coroutineScope { + // Restore individual manga + backup.backupManga.forEach { + if (!isActive) { + return@coroutineScope false + } + + restoreManga(it, backup.backupCategories) } - - restoreManga(it, backup.backupCategories) + // TODO: optionally trigger online library + tracker update + true } - - // TODO: optionally trigger online library + tracker update - - return true } private suspend fun restoreCategories(backupCategories: List) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 5ad86ad50..2428da302 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -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) } /** diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 88ac37d83..b6da8239f 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -86,7 +86,6 @@ Delete category Edit cover View chapters - Stop Pause Previous chapter Next chapter