Fix periodical backups to external directory
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user