From d9acc4ec18eee1f3cfa1f989c1c12db27c2a1333 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 28 Oct 2023 16:14:47 +0300 Subject: [PATCH] Fix periodical backups to external directory --- .../kotatsu/core/prefs/AppSettings.kt | 1 + .../PeriodicalBackupSettingsFragment.kt | 54 +++++++++++++------ .../settings/backup/PeriodicalBackupWorker.kt | 35 +++++++++--- app/src/main/res/values/strings.xml | 1 + app/src/main/res/xml/pref_backup_periodic.xml | 9 ++++ 5 files changed, 77 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index dd039b641..14ae7bf61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -471,6 +471,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_BACKUP_PERIODICAL_ENABLED = "backup_periodic" const val KEY_BACKUP_PERIODICAL_FREQUENCY = "backup_periodic_freq" const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output" + const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last" const val KEY_HISTORY_GROUPING = "history_grouping" const val KEY_READING_INDICATORS = "reading_indicators" const val KEY_REVERSE_CHAPTERS = "reverse_chapters" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt index cdbff719a..3e5dfd316 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt @@ -1,12 +1,15 @@ package org.koitharu.kotatsu.settings.backup -import android.content.SharedPreferences +import android.content.Context +import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View import androidx.activity.result.ActivityResultCallback import androidx.activity.result.contract.ActivityResultContracts +import androidx.documentfile.provider.DocumentFile import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -14,12 +17,19 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.backup.DIR_BACKUPS import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.util.ext.resolveFile import org.koitharu.kotatsu.core.util.ext.tryLaunch import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import java.io.File +import java.text.SimpleDateFormat +import javax.inject.Inject +@AndroidEntryPoint class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups), - ActivityResultCallback, SharedPreferences.OnSharedPreferenceChangeListener { + ActivityResultCallback { + + @Inject + lateinit var scheduler: PeriodicalBackupWorker.Scheduler private val outputSelectCall = registerForActivityResult( ActivityResultContracts.OpenDocumentTree(), @@ -32,8 +42,8 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - settings.subscribe(this) bindOutputSummary() + bindLastBackupInfo() } override fun onPreferenceTreeClick(preference: Preference): Boolean { @@ -43,20 +53,12 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi } } - override fun onDestroyView() { - super.onDestroyView() - settings.unsubscribe(this) - } - override fun onActivityResult(result: Uri?) { if (result != null) { + val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + context?.contentResolver?.takePersistableUriPermission(result, takeFlags) settings.periodicalBackupOutput = result - } - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - when (key) { - AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> bindOutputSummary() + bindOutputSummary() } } @@ -65,10 +67,32 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi viewLifecycleScope.launch { preference.summary = withContext(Dispatchers.Default) { val value = settings.periodicalBackupOutput - value?.toString() ?: preference.context.run { + value?.toUserFriendlyString(preference.context) ?: preference.context.run { getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS) }.path } } } + + private fun bindLastBackupInfo() { + val preference = findPreference(AppSettings.KEY_BACKUP_PERIODICAL_LAST) ?: return + viewLifecycleScope.launch { + val lastDate = withContext(Dispatchers.Default) { + scheduler.getLastSuccessfulBackup() + } + preference.summary = lastDate?.let { + val format = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.MEDIUM, SimpleDateFormat.SHORT) + preference.context.getString(R.string.last_successful_backup, format.format(it)) + } + preference.isVisible = lastDate != null + } + } + + private fun Uri.toUserFriendlyString(context: Context): String { + val df = DocumentFile.fromTreeUri(context, this) + if (df?.canWrite() != true) { + return context.getString(R.string.invalid_value_message) + } + return resolveFile(context)?.path ?: toString() + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupWorker.kt index 8f929ec3e..77f55ab3c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupWorker.kt @@ -2,14 +2,17 @@ package org.koitharu.kotatsu.settings.backup import android.content.Context import android.os.Build +import androidx.documentfile.provider.DocumentFile import androidx.hilt.work.HiltWorker import androidx.work.Constraints import androidx.work.CoroutineWorker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.await +import androidx.work.workDataOf import dagger.Reusable import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -20,8 +23,10 @@ import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupZipOutput import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName +import org.koitharu.kotatsu.core.util.ext.deleteAwait import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler +import java.util.Date import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -34,6 +39,7 @@ class PeriodicalBackupWorker @AssistedInject constructor( ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { + val resultData = workDataOf(DATA_TIMESTAMP to Date().time) val file = BackupZipOutput(applicationContext).use { backup -> backup.put(repository.createIndex()) backup.put(repository.dumpHistory()) @@ -44,14 +50,17 @@ class PeriodicalBackupWorker @AssistedInject constructor( backup.finish() backup.file } - return settings.periodicalBackupOutput?.let { - applicationContext.contentResolver.openOutputStream(it)?.use { output -> - file.source().use { input -> - output.sink().buffer().writeAllCancellable(input) - } - Result.success() - } ?: Result.failure() - } ?: Result.success() + val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData) + val target = DocumentFile.fromTreeUri(applicationContext, dirUri) + ?.createFile("application/zip", file.name) + ?.uri ?: return Result.failure() + applicationContext.contentResolver.openOutputStream(target)?.use { output -> + file.source().use { input -> + output.sink().buffer().writeAllCancellable(input) + } + } ?: return Result.failure() + file.deleteAwait() + return Result.success(resultData) } @Reusable @@ -88,10 +97,20 @@ class PeriodicalBackupWorker @AssistedInject constructor( .awaitUniqueWorkInfoByName(TAG) .any { !it.state.isFinished } } + + suspend fun getLastSuccessfulBackup(): Date? { + return workManager + .awaitUniqueWorkInfoByName(TAG) + .lastOrNull { x -> x.state == WorkInfo.State.SUCCEEDED } + ?.outputData + ?.getLong(DATA_TIMESTAMP, 0) + ?.let { if (it != 0L) Date(it) else null } + } } private companion object { const val TAG = "backups" + const val DATA_TIMESTAMP = "ts" } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 632dc226e..045f17d0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -509,4 +509,5 @@ Once per month Enable periodic backups Backups output directory + Last successful backup: %s diff --git a/app/src/main/res/xml/pref_backup_periodic.xml b/app/src/main/res/xml/pref_backup_periodic.xml index d536a5aea..a205e8657 100644 --- a/app/src/main/res/xml/pref_backup_periodic.xml +++ b/app/src/main/res/xml/pref_backup_periodic.xml @@ -23,4 +23,13 @@ android:key="backup_periodic_output" android:title="@string/backups_output_directory" /> + +