Periodic backups
parent
beb17ef442
commit
beba818f57
@ -0,0 +1,74 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.SharedPreferences
|
||||
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.preference.Preference
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.tryLaunch
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
|
||||
class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodic_backups),
|
||||
ActivityResultCallback<Uri?>, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val outputSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree(),
|
||||
this,
|
||||
)
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_backup_periodic)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
bindOutputSummary()
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> outputSelectCall.tryLaunch(null)
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
settings.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
if (result != null) {
|
||||
settings.periodicalBackupOutput = result
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT -> bindOutputSummary()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindOutputSummary() {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT) ?: return
|
||||
viewLifecycleScope.launch {
|
||||
preference.summary = withContext(Dispatchers.Default) {
|
||||
val value = settings.periodicalBackupOutput
|
||||
value?.toString() ?: preference.context.run {
|
||||
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
|
||||
}.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.await
|
||||
import dagger.Reusable
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
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.writeAllCancellable
|
||||
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltWorker
|
||||
class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val repository: BackupRepository,
|
||||
private val settings: AppSettings,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val file = BackupZipOutput(applicationContext).use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSettings())
|
||||
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()
|
||||
}
|
||||
|
||||
@Reusable
|
||||
class Scheduler @Inject constructor(
|
||||
private val workManager: WorkManager,
|
||||
private val settings: AppSettings,
|
||||
) : PeriodicWorkScheduler {
|
||||
|
||||
override suspend fun schedule() {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresStorageNotLow(true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
constraints.setRequiresDeviceIdle(true)
|
||||
}
|
||||
val request = PeriodicWorkRequestBuilder<SuggestionsWorker>(
|
||||
settings.periodicalBackupFrequency,
|
||||
TimeUnit.HOURS,
|
||||
).setConstraints(constraints.build())
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
workManager
|
||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun unschedule() {
|
||||
workManager
|
||||
.cancelUniqueWork(TAG)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun isScheduled(): Boolean {
|
||||
return workManager
|
||||
.awaitUniqueWorkInfoByName(TAG)
|
||||
.any { !it.state.isFinished }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TAG = "backups"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="backup_periodic"
|
||||
android:layout="@layout/preference_toggle_header"
|
||||
android:title="@string/periodic_backups_enable" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="7"
|
||||
android:dependency="backup_periodic"
|
||||
android:entries="@array/backup_frequency"
|
||||
android:entryValues="@array/values_backup_frequency"
|
||||
android:key="backup_periodic_freq"
|
||||
android:title="@string/backup_frequency"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<Preference
|
||||
android:dependency="backup_periodic"
|
||||
android:key="backup_periodic_output"
|
||||
android:title="@string/backups_output_directory" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
Loading…
Reference in New Issue