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_ENABLED = "backup_periodic"
const val KEY_BACKUP_PERIODICAL_FREQUENCY = "backup_periodic_freq" const val KEY_BACKUP_PERIODICAL_FREQUENCY = "backup_periodic_freq"
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output" 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_HISTORY_GROUPING = "history_grouping"
const val KEY_READING_INDICATORS = "reading_indicators" const val KEY_READING_INDICATORS = "reading_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters" const val KEY_REVERSE_CHAPTERS = "reverse_chapters"

View File

@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.settings.backup package org.koitharu.kotatsu.settings.backup
import android.content.SharedPreferences import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.preference.Preference import androidx.preference.Preference
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.backup.DIR_BACKUPS
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment 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.tryLaunch
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
import java.io.File import java.io.File
import java.text.SimpleDateFormat
import javax.inject.Inject
@AndroidEntryPoint
class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups), class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups),
ActivityResultCallback<Uri?>, SharedPreferences.OnSharedPreferenceChangeListener { ActivityResultCallback<Uri?> {
@Inject
lateinit var scheduler: PeriodicalBackupWorker.Scheduler
private val outputSelectCall = registerForActivityResult( private val outputSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(), ActivityResultContracts.OpenDocumentTree(),
@@ -32,8 +42,8 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
settings.subscribe(this)
bindOutputSummary() bindOutputSummary()
bindLastBackupInfo()
} }
override fun onPreferenceTreeClick(preference: Preference): Boolean { 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?) { override fun onActivityResult(result: Uri?) {
if (result != null) { 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 settings.periodicalBackupOutput = result
} bindOutputSummary()
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> bindOutputSummary()
} }
} }
@@ -65,10 +67,32 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
viewLifecycleScope.launch { viewLifecycleScope.launch {
preference.summary = withContext(Dispatchers.Default) { preference.summary = withContext(Dispatchers.Default) {
val value = settings.periodicalBackupOutput val value = settings.periodicalBackupOutput
value?.toString() ?: preference.context.run { value?.toUserFriendlyString(preference.context) ?: preference.context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS) getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
}.path }.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.content.Context
import android.os.Build import android.os.Build
import androidx.documentfile.provider.DocumentFile
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await import androidx.work.await
import androidx.work.workDataOf
import dagger.Reusable import dagger.Reusable
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject 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.backup.BackupZipOutput
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName 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.core.util.ext.writeAllCancellable
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@@ -34,6 +39,7 @@ class PeriodicalBackupWorker @AssistedInject constructor(
) : CoroutineWorker(appContext, params) { ) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val resultData = workDataOf(DATA_TIMESTAMP to Date().time)
val file = BackupZipOutput(applicationContext).use { backup -> val file = BackupZipOutput(applicationContext).use { backup ->
backup.put(repository.createIndex()) backup.put(repository.createIndex())
backup.put(repository.dumpHistory()) backup.put(repository.dumpHistory())
@@ -44,14 +50,17 @@ class PeriodicalBackupWorker @AssistedInject constructor(
backup.finish() backup.finish()
backup.file backup.file
} }
return settings.periodicalBackupOutput?.let { val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData)
applicationContext.contentResolver.openOutputStream(it)?.use { output -> val target = DocumentFile.fromTreeUri(applicationContext, dirUri)
file.source().use { input -> ?.createFile("application/zip", file.name)
output.sink().buffer().writeAllCancellable(input) ?.uri ?: return Result.failure()
} applicationContext.contentResolver.openOutputStream(target)?.use { output ->
Result.success() file.source().use { input ->
} ?: Result.failure() output.sink().buffer().writeAllCancellable(input)
} ?: Result.success() }
} ?: return Result.failure()
file.deleteAwait()
return Result.success(resultData)
} }
@Reusable @Reusable
@@ -88,10 +97,20 @@ class PeriodicalBackupWorker @AssistedInject constructor(
.awaitUniqueWorkInfoByName(TAG) .awaitUniqueWorkInfoByName(TAG)
.any { !it.state.isFinished } .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 { private companion object {
const val TAG = "backups" 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="frequency_once_per_month">Once per month</string>
<string name="periodic_backups_enable">Enable periodic backups</string> <string name="periodic_backups_enable">Enable periodic backups</string>
<string name="backups_output_directory">Backups output directory</string> <string name="backups_output_directory">Backups output directory</string>
<string name="last_successful_backup">Last successful backup: %s</string>
</resources> </resources>

View File

@@ -23,4 +23,13 @@
android:key="backup_periodic_output" android:key="backup_periodic_output"
android:title="@string/backups_output_directory" /> 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> </androidx.preference.PreferenceScreen>