Periodical backup improvements

This commit is contained in:
Koitharu
2025-07-10 09:54:45 +03:00
parent b70c1da54b
commit 6319997716
7 changed files with 105 additions and 32 deletions

View File

@@ -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)
}
}
}

View File

@@ -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"
}
}

View File

@@ -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?) {

View File

@@ -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"

View File

@@ -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<Uri?, *>
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)

View File

@@ -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()

View File

@@ -855,4 +855,5 @@
<string name="theme_name_totoro">Totoro</string>
<string name="book_effect">Yellowish background (blue filter)</string>
<string name="local_storage_cleanup">Local storage cleanup</string>
<string name="packup_creation_failed">Failed to create backup</string>
</resources>