Fix periodical backups to external directory

This commit is contained in:
Koitharu
2023-10-28 16:14:47 +03:00
parent 577cc848ee
commit d9acc4ec18
5 changed files with 77 additions and 23 deletions

View File

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

View File

@@ -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<Uri?>, SharedPreferences.OnSharedPreferenceChangeListener {
ActivityResultCallback<Uri?> {
@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<Preference>(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()
}
}

View File

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

View File

@@ -509,4 +509,5 @@
<string name="frequency_once_per_month">Once per month</string>
<string name="periodic_backups_enable">Enable periodic backups</string>
<string name="backups_output_directory">Backups output directory</string>
<string name="last_successful_backup">Last successful backup: %s</string>
</resources>

View File

@@ -23,4 +23,13 @@
android:key="backup_periodic_output"
android:title="@string/backups_output_directory" />
<Preference
android:dependency="backup_periodic"
android:icon="@drawable/ic_info_outline"
android:key="backup_periodic_last"
android:persistent="false"
android:selectable="false"
app:allowDividerAbove="true"
app:isPreferenceVisible="false" />
</androidx.preference.PreferenceScreen>