Select which data will be restored from backup

pull/593/head
Koitharu 2 years ago
parent db3db4637c
commit c27586231a
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -82,7 +82,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:97338f31c5') { implementation('com.github.KotatsuApp:kotatsu-parsers:87f99addbb') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }

@ -3,18 +3,20 @@ package org.koitharu.kotatsu.core.backup
import org.json.JSONArray import org.json.JSONArray
class BackupEntry( class BackupEntry(
val name: String, val name: Name,
val data: JSONArray val data: JSONArray
) { ) {
companion object Names { enum class Name(
val key: String,
) {
const val INDEX = "index" INDEX("index"),
const val HISTORY = "history" HISTORY("history"),
const val CATEGORIES = "categories" CATEGORIES("categories"),
const val FAVOURITES = "favourites" FAVOURITES("favourites"),
const val SETTINGS = "settings" SETTINGS("settings"),
const val BOOKMARKS = "bookmarks" BOOKMARKS("bookmarks"),
const val SOURCES = "sources" SOURCES("sources"),
} }
} }

@ -7,8 +7,10 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.json.JSONIterator import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
private const val PAGE_SIZE = 10 private const val PAGE_SIZE = 10
@ -20,7 +22,7 @@ class BackupRepository @Inject constructor(
suspend fun dumpHistory(): BackupEntry { suspend fun dumpHistory(): BackupEntry {
var offset = 0 var offset = 0
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray()) val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray())
while (true) { while (true) {
val history = db.getHistoryDao().findAll(offset, PAGE_SIZE) val history = db.getHistoryDao().findAll(offset, PAGE_SIZE)
if (history.isEmpty()) { if (history.isEmpty()) {
@ -41,7 +43,7 @@ class BackupRepository @Inject constructor(
} }
suspend fun dumpCategories(): BackupEntry { suspend fun dumpCategories(): BackupEntry {
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray()) val entry = BackupEntry(BackupEntry.Name.CATEGORIES, JSONArray())
val categories = db.getFavouriteCategoriesDao().findAll() val categories = db.getFavouriteCategoriesDao().findAll()
for (item in categories) { for (item in categories) {
entry.data.put(JsonSerializer(item).toJson()) entry.data.put(JsonSerializer(item).toJson())
@ -51,7 +53,7 @@ class BackupRepository @Inject constructor(
suspend fun dumpFavourites(): BackupEntry { suspend fun dumpFavourites(): BackupEntry {
var offset = 0 var offset = 0
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray()) val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray())
while (true) { while (true) {
val favourites = db.getFavouritesDao().findAll(offset, PAGE_SIZE) val favourites = db.getFavouritesDao().findAll(offset, PAGE_SIZE)
if (favourites.isEmpty()) { if (favourites.isEmpty()) {
@ -72,7 +74,7 @@ class BackupRepository @Inject constructor(
} }
suspend fun dumpBookmarks(): BackupEntry { suspend fun dumpBookmarks(): BackupEntry {
val entry = BackupEntry(BackupEntry.BOOKMARKS, JSONArray()) val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray())
val all = db.getBookmarksDao().findAll() val all = db.getBookmarksDao().findAll()
for ((m, b) in all) { for ((m, b) in all) {
val json = JSONObject() val json = JSONObject()
@ -90,7 +92,7 @@ class BackupRepository @Inject constructor(
} }
fun dumpSettings(): BackupEntry { fun dumpSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray()) val entry = BackupEntry(BackupEntry.Name.SETTINGS, JSONArray())
val settingsDump = settings.getAllValues().toMutableMap() val settingsDump = settings.getAllValues().toMutableMap()
settingsDump.remove(AppSettings.KEY_APP_PASSWORD) settingsDump.remove(AppSettings.KEY_APP_PASSWORD)
settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD) settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD)
@ -102,7 +104,7 @@ class BackupRepository @Inject constructor(
} }
suspend fun dumpSources(): BackupEntry { suspend fun dumpSources(): BackupEntry {
val entry = BackupEntry(BackupEntry.SOURCES, JSONArray()) val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
val all = db.getSourcesDao().findAll() val all = db.getSourcesDao().findAll()
for (source in all) { for (source in all) {
val json = JsonSerializer(source).toJson() val json = JsonSerializer(source).toJson()
@ -112,7 +114,7 @@ class BackupRepository @Inject constructor(
} }
fun createIndex(): BackupEntry { fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray()) val entry = BackupEntry(BackupEntry.Name.INDEX, JSONArray())
val json = JSONObject() val json = JSONObject()
json.put("app_id", BuildConfig.APPLICATION_ID) json.put("app_id", BuildConfig.APPLICATION_ID)
json.put("app_version", BuildConfig.VERSION_CODE) json.put("app_version", BuildConfig.VERSION_CODE)
@ -121,6 +123,11 @@ class BackupRepository @Inject constructor(
return entry return entry
} }
fun getBackupDate(entry: BackupEntry?): Date? {
val timestamp = entry?.data?.optJSONObject(0)?.getLongOrDefault("created_at", 0) ?: 0
return if (timestamp == 0L) null else Date(timestamp)
}
suspend fun restoreHistory(entry: BackupEntry): CompositeResult { suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
val result = CompositeResult() val result = CompositeResult()
for (item in entry.data.JSONIterator()) { for (item in entry.data.JSONIterator()) {

@ -1,25 +1,44 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okio.Closeable import okio.Closeable
import org.json.JSONArray import org.json.JSONArray
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import java.io.File import java.io.File
import java.util.EnumSet
import java.util.zip.ZipFile import java.util.zip.ZipFile
class BackupZipInput(val file: File) : Closeable { class BackupZipInput(val file: File) : Closeable {
private val zipFile = ZipFile(file) private val zipFile = ZipFile(file)
suspend fun getEntry(name: String): BackupEntry? = runInterruptible(Dispatchers.IO) { suspend fun getEntry(name: BackupEntry.Name): BackupEntry? = runInterruptible(Dispatchers.IO) {
val entry = zipFile.getEntry(name) ?: return@runInterruptible null val entry = zipFile.getEntry(name.key) ?: return@runInterruptible null
val json = zipFile.getInputStream(entry).use { val json = zipFile.getInputStream(entry).use {
JSONArray(it.bufferedReader().readText()) JSONArray(it.bufferedReader().readText())
} }
BackupEntry(name, json) BackupEntry(name, json)
} }
suspend fun entries(): Set<BackupEntry.Name> = runInterruptible(Dispatchers.IO) {
zipFile.entries().toList().mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { ze ->
BackupEntry.Name.entries.find { it.key == ze.name }
}
}
override fun close() { override fun close() {
zipFile.close() zipFile.close()
} }
fun cleanupAsync() {
processLifecycleScope.launch(Dispatchers.IO, CoroutineStart.ATOMIC) {
runCatching {
close()
file.delete()
}
}
}
} }

@ -17,7 +17,7 @@ class BackupZipOutput(val file: File) : Closeable {
private val output = ZipOutput(file, Deflater.BEST_COMPRESSION) private val output = ZipOutput(file, Deflater.BEST_COMPRESSION)
suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) { suspend fun put(entry: BackupEntry) = runInterruptible(Dispatchers.IO) {
output.put(entry.name, entry.data.toString(2)) output.put(entry.name.key, entry.data.toString(2))
} }
suspend fun finish() = runInterruptible(Dispatchers.IO) { suspend fun finish() = runInterruptible(Dispatchers.IO) {

@ -123,5 +123,11 @@ class WelcomeSheet : BaseAdaptiveSheet<SheetWelcomeBinding>(), ChipsView.OnChipC
private const val TAG = "WelcomeSheet" private const val TAG = "WelcomeSheet"
fun show(fm: FragmentManager) = WelcomeSheet().showDistinct(fm, TAG) fun show(fm: FragmentManager) = WelcomeSheet().showDistinct(fm, TAG)
fun dismiss(fm: FragmentManager): Boolean {
val sheet = fm.findFragmentByTag(TAG) as? WelcomeSheet ?: return false
sheet.dismissAllowingStateLoss()
return true
}
} }
} }

@ -54,7 +54,11 @@ class AppBackupAgent : BackupAgent() {
mtime: Long mtime: Long
) { ) {
if (destination?.name?.endsWith(".bk.zip") == true) { if (destination?.name?.endsWith(".bk.zip") == true) {
restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext))) restoreBackupFile(
data.fileDescriptor,
size,
BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)),
)
destination.delete() destination.delete()
} else { } else {
super.onRestoreFile(data, size, destination, type, mode, mtime) super.onRestoreFile(data, size, destination, type, mode, mtime)
@ -87,12 +91,12 @@ class AppBackupAgent : BackupAgent() {
val backup = BackupZipInput(tempFile) val backup = BackupZipInput(tempFile)
try { try {
runBlocking { runBlocking {
backup.getEntry(BackupEntry.HISTORY)?.let { repository.restoreHistory(it) } backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it) }
backup.getEntry(BackupEntry.CATEGORIES)?.let { repository.restoreCategories(it) } backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { repository.restoreCategories(it) }
backup.getEntry(BackupEntry.FAVOURITES)?.let { repository.restoreFavourites(it) } backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it) }
backup.getEntry(BackupEntry.BOOKMARKS)?.let { repository.restoreBookmarks(it) } backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
backup.getEntry(BackupEntry.SOURCES)?.let { repository.restoreSources(it) } backup.getEntry(BackupEntry.Name.SOURCES)?.let { repository.restoreSources(it) }
backup.getEntry(BackupEntry.SETTINGS)?.let { repository.restoreSettings(it) } backup.getEntry(BackupEntry.Name.SETTINGS)?.let { repository.restoreSettings(it) }
} }
} finally { } finally {
backup.close() backup.close()

@ -0,0 +1,37 @@
package org.koitharu.kotatsu.settings.backup
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.setChecked
import org.koitharu.kotatsu.databinding.ItemCheckableMultipleBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_CHECKED_CHANGED
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
class BackupEntriesAdapter(
clickListener: OnListItemClickListener<BackupEntryModel>,
) : BaseListAdapter<BackupEntryModel>() {
init {
addDelegate(ListItemType.NAV_ITEM, backupEntryAD(clickListener))
}
}
private fun backupEntryAD(
clickListener: OnListItemClickListener<BackupEntryModel>,
) = adapterDelegateViewBinding<BackupEntryModel, BackupEntryModel, ItemCheckableMultipleBinding>(
{ layoutInflater, parent -> ItemCheckableMultipleBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener { v ->
clickListener.onItemClick(item, v)
}
bind { payloads ->
with(binding.root) {
setText(item.titleResId)
setChecked(item.isChecked, PAYLOAD_CHECKED_CHANGED in payloads)
isEnabled = item.isEnabled
}
}
}

@ -0,0 +1,43 @@
package org.koitharu.kotatsu.settings.backup
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.BackupEntry
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
data class BackupEntryModel(
val name: BackupEntry.Name,
val isChecked: Boolean,
val isEnabled: Boolean,
) : ListModel {
@get:StringRes
val titleResId: Int
get() = when (name) {
BackupEntry.Name.INDEX -> 0 // should not appear here
BackupEntry.Name.HISTORY -> R.string.history
BackupEntry.Name.CATEGORIES -> R.string.favourites_categories
BackupEntry.Name.FAVOURITES -> R.string.favourites
BackupEntry.Name.SETTINGS -> R.string.settings
BackupEntry.Name.BOOKMARKS -> R.string.bookmarks
BackupEntry.Name.SOURCES -> R.string.remote_sources
}
override fun areItemsTheSame(other: ListModel): Boolean {
return other is BackupEntryModel && other.name == name
}
override fun getChangePayload(previousState: ListModel): Any? {
if (previousState !is BackupEntryModel) {
return null
}
return if (previousState.isEnabled != isEnabled) {
ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
} else if (previousState.isChecked != isChecked) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
super.getChangePayload(previousState)
}
}
}

@ -3,40 +3,58 @@ package org.koitharu.kotatsu.settings.backup
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.backup.CompositeResult import org.koitharu.kotatsu.core.backup.CompositeResult
import org.koitharu.kotatsu.core.ui.AlertDialogFragment import org.koitharu.kotatsu.core.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.DialogProgressBinding import org.koitharu.kotatsu.databinding.DialogRestoreBinding
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.math.roundToInt import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() { class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupEntryModel>,
View.OnClickListener {
private val viewModel: RestoreViewModel by viewModels() private val viewModel: RestoreViewModel by viewModels()
override fun onCreateViewBinding( override fun onCreateViewBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
) = DialogProgressBinding.inflate(inflater, container, false) ) = DialogRestoreBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: DialogProgressBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: DialogRestoreBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
binding.textViewTitle.setText(R.string.restore_backup) val adapter = BackupEntriesAdapter(this)
binding.textViewSubtitle.setText(R.string.preparing_) binding.recyclerView.adapter = adapter
binding.buttonCancel.setOnClickListener(this)
binding.buttonRestore.setOnClickListener(this)
viewModel.availableEntries.observe(viewLifecycleOwner, adapter)
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged) viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone) viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone)
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError) viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
combine(
viewModel.isLoading,
viewModel.availableEntries,
viewModel.backupDate,
::Triple,
).observe(viewLifecycleOwner, this::onLoadingChanged)
} }
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
@ -44,6 +62,40 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
.setCancelable(false) .setCancelable(false)
} }
override fun onClick(v: View) {
when (v.id) {
R.id.button_cancel -> dismiss()
R.id.button_restore -> viewModel.restore()
}
}
override fun onItemClick(item: BackupEntryModel, view: View) {
viewModel.onItemClick(item)
}
private fun onLoadingChanged(value: Triple<Boolean, List<BackupEntryModel>, Date?>) {
val (isLoading, entries, backupDate) = value
val hasEntries = entries.isNotEmpty()
with(requireViewBinding()) {
progressBar.isVisible = isLoading
recyclerView.isGone = isLoading
textViewSubtitle.textAndVisible =
when {
!isLoading -> backupDate?.formatBackupDate()
hasEntries -> getString(R.string.processing_)
else -> getString(R.string.loading_)
}
buttonRestore.isEnabled = !isLoading && entries.any { it.isChecked }
}
}
private fun Date.formatBackupDate(): String {
return getString(
R.string.backup_date_,
SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(this),
)
}
private fun onError(e: Throwable) { private fun onError(e: Throwable) {
MaterialAlertDialogBuilder(context ?: return) MaterialAlertDialogBuilder(context ?: return)
.setNegativeButton(R.string.close, null) .setNegativeButton(R.string.close, null)
@ -89,6 +141,9 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
} }
builder.setPositiveButton(android.R.string.ok, null) builder.setPositiveButton(android.R.string.ok, null)
.show() .show()
if (!result.isEmpty && !result.isAllFailed) {
WelcomeSheet.dismiss(parentFragmentManager)
}
dismiss() dismiss()
} }

@ -15,8 +15,12 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.util.Date
import java.util.EnumMap
import java.util.EnumSet
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -26,16 +30,11 @@ class RestoreViewModel @Inject constructor(
@ApplicationContext context: Context, @ApplicationContext context: Context,
) : BaseViewModel() { ) : BaseViewModel() {
val progress = MutableStateFlow(-1f) private val backupInput = SuspendLazy {
val onRestoreDone = MutableEventFlow<CompositeResult>()
init {
launchLoadingJob {
val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE) val uri = savedStateHandle.get<String>(RestoreDialogFragment.ARG_FILE)
?.toUriOrNull() ?: throw FileNotFoundException() ?.toUriOrNull() ?: throw FileNotFoundException()
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
runInterruptible(Dispatchers.IO) {
val backup = runInterruptible(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp") val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input -> (contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
tempFile.outputStream().use { output -> tempFile.outputStream().use { output ->
@ -44,45 +43,114 @@ class RestoreViewModel @Inject constructor(
} }
BackupZipInput(tempFile) BackupZipInput(tempFile)
} }
try { }
val progress = MutableStateFlow(-1f)
val onRestoreDone = MutableEventFlow<CompositeResult>()
val availableEntries = MutableStateFlow<List<BackupEntryModel>>(emptyList())
val backupDate = MutableStateFlow<Date?>(null)
init {
launchLoadingJob(Dispatchers.Default) {
val backup = backupInput.get()
val entries = backup.entries()
availableEntries.value = BackupEntry.Name.entries.mapNotNull { entry ->
if (entry == BackupEntry.Name.INDEX || entry !in entries) {
return@mapNotNull null
}
BackupEntryModel(
name = entry,
isChecked = true,
isEnabled = true,
)
}
backupDate.value = repository.getBackupDate(backup.getEntry(BackupEntry.Name.INDEX))
}
}
override fun onCleared() {
super.onCleared()
backupInput.peek()?.cleanupAsync()
}
fun onItemClick(item: BackupEntryModel) {
val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
map[item.name] = item.copy(isChecked = !item.isChecked)
map.validate()
availableEntries.value = map.values.sortedBy { it.name.ordinal }
}
fun restore() {
launchLoadingJob {
val backup = backupInput.get()
val checkedItems = availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
if (it.isChecked) it.name else null
}
val result = CompositeResult() val result = CompositeResult()
val step = 1f/6f val step = 1f / 6f
progress.value = 0f progress.value = 0f
backup.getEntry(BackupEntry.HISTORY)?.let { if (BackupEntry.Name.HISTORY in checkedItems) {
backup.getEntry(BackupEntry.Name.HISTORY)?.let {
result += repository.restoreHistory(it) result += repository.restoreHistory(it)
} }
}
progress.value += step progress.value += step
backup.getEntry(BackupEntry.CATEGORIES)?.let { if (BackupEntry.Name.CATEGORIES in checkedItems) {
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let {
result += repository.restoreCategories(it) result += repository.restoreCategories(it)
} }
}
progress.value += step progress.value += step
backup.getEntry(BackupEntry.FAVOURITES)?.let { if (BackupEntry.Name.FAVOURITES in checkedItems) {
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let {
result += repository.restoreFavourites(it) result += repository.restoreFavourites(it)
} }
}
progress.value += step progress.value += step
backup.getEntry(BackupEntry.BOOKMARKS)?.let { if (BackupEntry.Name.BOOKMARKS in checkedItems) {
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
result += repository.restoreBookmarks(it) result += repository.restoreBookmarks(it)
} }
}
progress.value += step progress.value += step
backup.getEntry(BackupEntry.SOURCES)?.let { if (BackupEntry.Name.SOURCES in checkedItems) {
backup.getEntry(BackupEntry.Name.SOURCES)?.let {
result += repository.restoreSources(it) result += repository.restoreSources(it)
} }
}
progress.value += step progress.value += step
backup.getEntry(BackupEntry.SETTINGS)?.let { if (BackupEntry.Name.SETTINGS in checkedItems) {
backup.getEntry(BackupEntry.Name.SETTINGS)?.let {
result += repository.restoreSettings(it) result += repository.restoreSettings(it)
} }
}
progress.value = 1f progress.value = 1f
onRestoreDone.call(result) onRestoreDone.call(result)
} finally { }
backup.close() }
backup.file.delete()
/**
* Check for inconsistent user selection
* Favorites cannot be restored without categories
*/
private fun MutableMap<BackupEntry.Name, BackupEntryModel>.validate() {
val favorites = this[BackupEntry.Name.FAVOURITES] ?: return
val categories = this[BackupEntry.Name.CATEGORIES]
if (categories?.isChecked == true) {
if (!favorites.isEnabled) {
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = true)
}
} else {
if (favorites.isEnabled) {
this[BackupEntry.Name.FAVOURITES] = favorites.copy(isEnabled = false, isChecked = false)
} }
} }
} }

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical"
android:paddingVertical="?dialogPreferredPadding">
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:paddingBottom="@dimen/margin_normal"
android:text="@string/restore_backup"
android:textAppearance="?textAppearanceTitleLarge" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:indeterminate="true"
android:max="100"
android:visibility="gone"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollIndicators="top|bottom"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="6"
tools:listitem="@layout/item_checkable_multiple"
tools:visibility="visible" />
<TextView
android:id="@+id/textView_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:layout_marginTop="@dimen/margin_small"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?android:textColorSecondary"
tools:text="@tools:sample/lorem[10]" />
<LinearLayout
style="?buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="?dialogPreferredPadding"
android:layout_marginTop="@dimen/margin_normal"
android:gravity="end"
android:orientation="horizontal">
<Button
android:id="@+id/button_cancel"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel" />
<Button
android:id="@+id/button_restore"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/restore" />
</LinearLayout>
</LinearLayout>

@ -5,6 +5,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall" android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:drawablePadding="?android:listPreferredItemPaddingStart" android:drawablePadding="?android:listPreferredItemPaddingStart"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart" android:paddingStart="?android:listPreferredItemPaddingStart"

@ -5,6 +5,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeightSmall" android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:drawablePadding="?android:listPreferredItemPaddingStart" android:drawablePadding="?android:listPreferredItemPaddingStart"
android:gravity="center_vertical" android:gravity="center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart" android:paddingStart="?android:listPreferredItemPaddingStart"

@ -546,4 +546,6 @@
<string name="disable_battery_optimization_summary_downloads">Might help with getting the download started if you have any issues with it</string> <string name="disable_battery_optimization_summary_downloads">Might help with getting the download started if you have any issues with it</string>
<string name="welcome_text">Please select which content sources you would like to enable. This can also be configured later in settings</string> <string name="welcome_text">Please select which content sources you would like to enable. This can also be configured later in settings</string>
<string name="sync_auth">Login to sync account</string> <string name="sync_auth">Login to sync account</string>
<string name="restore">Restore</string>
<string name="backup_date_">Backup date: %s</string>
</resources> </resources>

Loading…
Cancel
Save