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_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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user