From 437e6809bfa61ff763f1d1aef763df576e4ffd3d Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 14 Jun 2025 15:04:10 +0300 Subject: [PATCH] Backup restorng fixes --- .../backups/data/model/BookmarkBackup.kt | 2 +- .../backups/ui/BaseBackupRestoreService.kt | 94 +++++++++++++++---- .../backups/ui/backup/BackupService.kt | 33 +------ .../backups/ui/restore/RestoreService.kt | 34 +------ 4 files changed, 85 insertions(+), 78 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/backups/data/model/BookmarkBackup.kt b/app/src/main/kotlin/org/koitharu/kotatsu/backups/data/model/BookmarkBackup.kt index 8c6ab56ab..cfdc890e6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/backups/data/model/BookmarkBackup.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/backups/data/model/BookmarkBackup.kt @@ -20,7 +20,7 @@ class BookmarkBackup( @SerialName("chapter_id") val chapterId: Long, @SerialName("page") val page: Int, @SerialName("scroll") val scroll: Int, - @SerialName("image") val imageUrl: String, + @SerialName("image_url") val imageUrl: String, @SerialName("created_at") val createdAt: Long, @SerialName("percent") val percent: Float, ) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/BaseBackupRestoreService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/BaseBackupRestoreService.kt index 97741b0f6..bcb6f7aaa 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/BaseBackupRestoreService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/BaseBackupRestoreService.kt @@ -1,20 +1,25 @@ package org.koitharu.kotatsu.backups.ui -import android.app.Notification +import android.net.Uri import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat +import androidx.core.app.ShareCompat import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ErrorReporterReceiver import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.ui.CoroutineIntentService +import org.koitharu.kotatsu.core.util.CompositeResult import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.getFileDisplayName +import androidx.appcompat.R as appcompatR abstract class BaseBackupRestoreService : CoroutineIntentService() { protected abstract val notificationTag: String + protected abstract val isRestoreService: Boolean protected lateinit var notificationManager: NotificationManagerCompat private set @@ -26,10 +31,7 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() { } override fun IntentJobContext.onError(error: Throwable) { - if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { - val notification = createErrorNotification(error) - notificationManager.notify(notificationTag, startId, notification) - } + showResultNotification(null, CompositeResult.failure(error)) } private fun createNotificationChannel() { @@ -43,33 +45,93 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() { notificationManager.createNotificationChannel(channel) } - protected fun createErrorNotification(error: Throwable): Notification { + protected fun IntentJobContext.showResultNotification( + fileUri: Uri?, + result: CompositeResult, + ) { + if (!applicationContext.checkNotificationPermission(CHANNEL_ID)) { + return + } val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) .setPriority(NotificationCompat.PRIORITY_HIGH) .setDefaults(0) .setSilent(true) .setAutoCancel(true) - .setContentText(error.getDisplayMessage(resources)) - .setSmallIcon(android.R.drawable.stat_notify_error) - ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent -> - notification.addAction( - R.drawable.ic_alert_outline, - applicationContext.getString(R.string.report), - reportIntent, - ) + .setSubText(fileUri?.let { contentResolver.getFileDisplayName(it) }) + when { + result.isAllSuccess -> { + if (isRestoreService) { + notification + .setContentTitle(getString(R.string.restoring_backup)) + .setContentText(getString(R.string.data_restored_success)) + } else { + notification + .setContentTitle(getString(R.string.backup_saved)) + .setContentText(fileUri?.let { contentResolver.getFileDisplayName(it) }) + .setSubText(null) + + } + notification.setSmallIcon(R.drawable.ic_stat_done) + } + + result.isAllFailed || !isRestoreService -> { + val title = getString(if (isRestoreService) R.string.data_not_restored else R.string.error_occurred) + val message = result.failures.joinToString("\n") { + it.getDisplayMessage(applicationContext.resources) + } + notification + .setContentText(if (isRestoreService) getString(R.string.data_not_restored_text) else message) + .setBigText(title, message) + .setSmallIcon(android.R.drawable.stat_notify_error) + result.failures.firstNotNullOfOrNull { error -> + ErrorReporterReceiver.getPendingIntent(applicationContext, error) + }?.let { reportIntent -> + notification.addAction( + R.drawable.ic_alert_outline, + applicationContext.getString(R.string.report), + reportIntent, + ) + } + } + + else -> { + notification + .setContentTitle(getString(R.string.restoring_backup)) + .setContentText(getString(R.string.data_restored_with_errors)) + .setSmallIcon(R.drawable.ic_stat_done) + } } notification.setContentIntent( PendingIntentCompat.getActivity( applicationContext, 0, - AppRouter.homeIntent(this), + AppRouter.homeIntent(this@BaseBackupRestoreService), 0, false, ), ) - return notification.build() + if (!isRestoreService && fileUri != null) { + val shareIntent = ShareCompat.IntentBuilder(this@BaseBackupRestoreService) + .setStream(fileUri) + .setType("application/zip") + .setChooserTitle(R.string.share_backup) + .createChooserIntent() + notification.addAction( + appcompatR.drawable.abc_ic_menu_share_mtrl_alpha, + getString(R.string.share), + PendingIntentCompat.getActivity(this@BaseBackupRestoreService, 0, shareIntent, 0, false), + ) + } + notificationManager.notify(notificationTag, startId, notification.build()) } + protected fun NotificationCompat.Builder.setBigText(title: String, text: CharSequence) = setStyle( + NotificationCompat.BigTextStyle() + .bigText(text) + .setSummaryText(text) + .setBigContentTitle(title), + ) + protected companion object { const val CHANNEL_ID = "backup_restore" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/backup/BackupService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/backup/BackupService.kt index 51cfb2914..30a8129fe 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/backup/BackupService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/backup/BackupService.kt @@ -8,8 +8,6 @@ import android.content.pm.ServiceInfo import android.net.Uri import androidx.annotation.CheckResult import androidx.core.app.NotificationCompat -import androidx.core.app.PendingIntentCompat -import androidx.core.app.ShareCompat import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.cancelAndJoin @@ -19,6 +17,7 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.backups.data.BackupRepository import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService import org.koitharu.kotatsu.core.nav.AppRouter +import org.koitharu.kotatsu.core.util.CompositeResult import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.powerManager import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug @@ -35,6 +34,7 @@ import androidx.appcompat.R as appcompatR class BackupService : BaseBackupRestoreService() { override val notificationTag = TAG + override val isRestoreService = false @Inject lateinit var repository: BackupRepository @@ -63,9 +63,7 @@ class BackupService : BaseBackupRestoreService() { } progressUpdateJob?.cancelAndJoin() contentResolver.notifyChange(destination, null) - if (checkNotificationPermission(CHANNEL_ID)) { - notificationManager.notify(notificationTag, startId, createResultNotification(destination)) - } + showResultNotification(destination, CompositeResult.success()) } } @@ -98,31 +96,6 @@ class BackupService : BaseBackupRestoreService() { ).build() } - private fun createResultNotification(uri: Uri): Notification { - val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setDefaults(0) - .setSilent(true) - .setAutoCancel(true) - .setContentText(getString(R.string.backup_saved)) - .setSmallIcon(R.drawable.ic_stat_done) - val shareIntent = ShareCompat.IntentBuilder(this) - .setStream(uri) - .setType(contentResolver.getType(uri) ?: "application/zip") - .setChooserTitle(R.string.share_backup) - .createChooserIntent() - notification.setContentIntent( - PendingIntentCompat.getActivity( - applicationContext, - 0, - shareIntent, - 0, - false, - ), - ) - return notification.build() - } - companion object { private const val TAG = "BACKUP" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/restore/RestoreService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/restore/RestoreService.kt index b09339547..125704db3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/restore/RestoreService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/restore/RestoreService.kt @@ -8,8 +8,6 @@ import android.content.pm.ServiceInfo import android.net.Uri import androidx.annotation.CheckResult import androidx.core.app.NotificationCompat -import androidx.core.app.PendingIntentCompat -import androidx.core.app.ShareCompat import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.cancelAndJoin @@ -37,6 +35,7 @@ import androidx.appcompat.R as appcompatR class RestoreService : BaseBackupRestoreService() { override val notificationTag = TAG + override val isRestoreService = true @Inject lateinit var repository: BackupRepository @@ -62,13 +61,11 @@ class RestoreService : BaseBackupRestoreService() { } else { null } - ZipInputStream(contentResolver.openInputStream(source)).use { input -> + val result = ZipInputStream(contentResolver.openInputStream(source)).use { input -> repository.restoreBackup(input, sections, progress) } progressUpdateJob?.cancelAndJoin() - if (checkNotificationPermission(CHANNEL_ID)) { - notificationManager.notify(notificationTag, startId, createResultNotification(source)) - } + showResultNotification(source, result) } } @@ -101,31 +98,6 @@ class RestoreService : BaseBackupRestoreService() { ).build() } - private fun createResultNotification(uri: Uri): Notification { - val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setDefaults(0) - .setSilent(true) - .setAutoCancel(true) - .setContentText(getString(R.string.backup_saved)) - .setSmallIcon(R.drawable.ic_stat_done) - val shareIntent = ShareCompat.IntentBuilder(this) - .setStream(uri) - .setType(contentResolver.getType(uri) ?: "application/zip") - .setChooserTitle(R.string.share_backup) - .createChooserIntent() - notification.setContentIntent( - PendingIntentCompat.getActivity( - applicationContext, - 0, - shareIntent, - 0, - false, - ), - ) - return notification.build() - } - companion object { private const val TAG = "RESTORE"