From 63199977161c6d7887b263bcfd894db30feb0475 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 10 Jul 2025 09:54:45 +0300 Subject: [PATCH] Periodical backup improvements --- .../backups/ui/BaseBackupRestoreService.kt | 27 ++++----- .../ui/periodical/PeriodicalBackupService.kt | 55 ++++++++++++++++++- .../PeriodicalBackupSettingsFragment.kt | 8 +++ .../koitharu/kotatsu/core/nav/AppRouter.kt | 5 ++ .../kotatsu/core/os/OpenDocumentTreeHelper.kt | 39 +++++++------ .../kotatsu/settings/SettingsActivity.kt | 2 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 105 insertions(+), 32 deletions(-) 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 d7ed489fb..fcde8ae6a 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,5 +1,6 @@ package org.koitharu.kotatsu.backups.ui +import android.content.Context import android.net.Uri import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat @@ -27,24 +28,13 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() { override fun onCreate() { super.onCreate() notificationManager = NotificationManagerCompat.from(applicationContext) - createNotificationChannel() + createNotificationChannel(this) } override fun IntentJobContext.onError(error: Throwable) { showResultNotification(null, CompositeResult.failure(error)) } - private fun createNotificationChannel() { - val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH) - .setName(getString(R.string.backup_restore)) - .setShowBadge(true) - .setVibrationEnabled(false) - .setSound(null, null) - .setLightsEnabled(false) - .build() - notificationManager.createNotificationChannel(channel) - } - protected fun IntentJobContext.showResultNotification( fileUri: Uri?, result: CompositeResult, @@ -128,8 +118,19 @@ abstract class BaseBackupRestoreService : CoroutineIntentService() { .setBigContentTitle(title), ) - protected companion object { + companion object { const val CHANNEL_ID = "backup_restore" + + fun createNotificationChannel(context: Context) { + val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_HIGH) + .setName(context.getString(R.string.backup_restore)) + .setShowBadge(true) + .setVibrationEnabled(false) + .setSound(null, null) + .setLightsEnabled(false) + .build() + NotificationManagerCompat.from(context).createNotificationChannel(channel) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupService.kt index b87a6dfb8..f23578fbb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupService.kt @@ -1,12 +1,21 @@ package org.koitharu.kotatsu.backups.ui.periodical import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.PendingIntentCompat import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R import org.koitharu.kotatsu.backups.data.BackupRepository import org.koitharu.kotatsu.backups.domain.BackupUtils import org.koitharu.kotatsu.backups.domain.ExternalBackupStorage +import org.koitharu.kotatsu.backups.ui.BaseBackupRestoreService +import org.koitharu.kotatsu.core.ErrorReporterReceiver +import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.CoroutineIntentService +import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import java.util.zip.ZipOutputStream import javax.inject.Inject @@ -48,5 +57,49 @@ class PeriodicalBackupService : CoroutineIntentService() { } } - override fun IntentJobContext.onError(error: Throwable) = Unit + override fun IntentJobContext.onError(error: Throwable) { + if (!applicationContext.checkNotificationPermission(CHANNEL_ID)) { + return + } + BaseBackupRestoreService.createNotificationChannel(applicationContext) + val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setDefaults(0) + .setSilent(true) + .setAutoCancel(true) + val title = getString(R.string.periodic_backups) + val message = getString( + R.string.inline_preference_pattern, + getString(R.string.packup_creation_failed), + error.getDisplayMessage(resources), + ) + notification + .setContentText(message) + .setSmallIcon(android.R.drawable.stat_notify_error) + .setStyle( + NotificationCompat.BigTextStyle() + .bigText(message) + .setSummaryText(getString(R.string.packup_creation_failed)) + .setBigContentTitle(title), + ) + ErrorReporterReceiver.getNotificationAction(applicationContext, error, startId, TAG)?.let { action -> + notification.addAction(action) + } + notification.setContentIntent( + PendingIntentCompat.getActivity( + applicationContext, + 0, + AppRouter.periodicBackupSettingsIntent(applicationContext), + 0, + false, + ), + ) + NotificationManagerCompat.from(applicationContext).notify(TAG, startId, notification.build()) + } + + private companion object { + + const val CHANNEL_ID = BaseBackupRestoreService.CHANNEL_ID + const val TAG = "periodical_backup" + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupSettingsFragment.kt index af8678757..916421f53 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/backups/ui/periodical/PeriodicalBackupSettingsFragment.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.text.format.DateUtils import android.view.View import androidx.activity.result.ActivityResultCallback +import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.preference.EditTextPreference import androidx.preference.Preference @@ -84,6 +85,13 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi "" -> null else -> path } + preference.icon = if (path == null) { + ContextCompat.getDrawable(preference.context, R.drawable.ic_alert_outline)?.also { + it.setTint(ContextCompat.getColor(preference.context, R.color.warning)) + } + } else { + null + } } private fun bindLastBackupInfo(lastBackupDate: Date?) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt index 2985821cf..fcab7686f 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt @@ -741,6 +741,10 @@ class AppRouter private constructor( Intent(context, SettingsActivity::class.java) .setAction(ACTION_TRACKER) + fun periodicBackupSettingsIntent(context: Context) = + Intent(context, SettingsActivity::class.java) + .setAction(ACTION_PERIODIC_BACKUP) + fun proxySettingsIntent(context: Context) = Intent(context, SettingsActivity::class.java) .setAction(ACTION_PROXY) @@ -825,6 +829,7 @@ class AppRouter private constructor( const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES" const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS" const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER" + const val ACTION_PERIODIC_BACKUP = "${BuildConfig.APPLICATION_ID}.action.MANAGE_PERIODIC_BACKUP" private const val ACCOUNT_KEY = "account" private const val ACTION_ACCOUNT_SYNC_SETTINGS = "android.settings.ACCOUNT_SYNC_SETTINGS" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt index 8a9dc368e..919a757ca 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/os/OpenDocumentTreeHelper.kt @@ -13,7 +13,6 @@ import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.core.app.ActivityOptionsCompat -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug // https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr class OpenDocumentTreeHelper( @@ -28,38 +27,42 @@ class OpenDocumentTreeHelper( callback, ) - private val pickFileTreeLauncherQ = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - activityResultCaller.registerForActivityResult(OpenDocumentTreeContractQ(flags), callback) + private val pickFileTreeLauncherPrimaryStorage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + activityResultCaller.registerForActivityResult(OpenDocumentTreeContractPrimaryStorage(flags), callback) } else { null } - private val pickFileTreeLauncherLegacy = activityResultCaller.registerForActivityResult( - contract = OpenDocumentTreeContractLegacy(flags), + private val pickFileTreeLauncherDefault = activityResultCaller.registerForActivityResult( + contract = OpenDocumentTreeContractDefault(flags), callback = callback, ) override fun launch(input: Uri?, options: ActivityOptionsCompat?) { - if (pickFileTreeLauncherQ == null) { - pickFileTreeLauncherLegacy.launch(input, options) - return - } try { - pickFileTreeLauncherQ.launch(input, options) + pickFileTreeLauncherDefault.launch(input, options) } catch (e: Exception) { - e.printStackTraceDebug() - pickFileTreeLauncherLegacy.launch(input, options) + if (pickFileTreeLauncherPrimaryStorage != null) { + try { + pickFileTreeLauncherPrimaryStorage.launch(input, options) + } catch (e2: Exception) { + e.addSuppressed(e2) + throw e + } + } else { + throw e + } } } override fun unregister() { - pickFileTreeLauncherQ?.unregister() - pickFileTreeLauncherLegacy.unregister() + pickFileTreeLauncherPrimaryStorage?.unregister() + pickFileTreeLauncherDefault.unregister() } override val contract: ActivityResultContract - get() = pickFileTreeLauncherQ?.contract ?: pickFileTreeLauncherLegacy.contract + get() = pickFileTreeLauncherPrimaryStorage?.contract ?: pickFileTreeLauncherDefault.contract - private open class OpenDocumentTreeContractLegacy( + private open class OpenDocumentTreeContractDefault( private val flags: Int, ) : ActivityResultContracts.OpenDocumentTree() { @@ -71,9 +74,9 @@ class OpenDocumentTreeHelper( } @RequiresApi(Build.VERSION_CODES.Q) - private class OpenDocumentTreeContractQ( + private class OpenDocumentTreeContractPrimaryStorage( private val flags: Int, - ) : OpenDocumentTreeContractLegacy(flags) { + ) : OpenDocumentTreeContractDefault(flags) { override fun createIntent(context: Context, input: Uri?): Intent { val intent = (context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt index f4cf5a0b8..9f6aa10d7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/SettingsActivity.kt @@ -18,6 +18,7 @@ import androidx.preference.PreferenceFragmentCompat import com.google.android.material.appbar.AppBarLayout import dagger.hilt.android.AndroidEntryPoint import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragment import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.ui.BaseActivity @@ -146,6 +147,7 @@ class SettingsActivity : AppRouter.ACTION_SUGGESTIONS -> SuggestionsSettingsFragment() AppRouter.ACTION_HISTORY -> UserDataSettingsFragment() AppRouter.ACTION_TRACKER -> TrackerSettingsFragment() + AppRouter.ACTION_PERIODIC_BACKUP -> PeriodicalBackupSettingsFragment() AppRouter.ACTION_SOURCES -> SourcesSettingsFragment() AppRouter.ACTION_PROXY -> ProxySettingsFragment() AppRouter.ACTION_MANAGE_DOWNLOADS -> DownloadsSettingsFragment() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 35b5d844b..1f610ca3a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -855,4 +855,5 @@ Totoro Yellowish background (blue filter) Local storage cleanup + Failed to create backup