Restore backups in background
This commit is contained in:
@@ -55,7 +55,7 @@ class StrictModeNotifier(
|
|||||||
.setContentIntent(
|
.setContentIntent(
|
||||||
PendingIntentCompat.getActivity(
|
PendingIntentCompat.getActivity(
|
||||||
context,
|
context,
|
||||||
0,
|
violation.hashCode(),
|
||||||
ShareHelper(context).getShareTextIntent(violation.stackTraceToString()),
|
ShareHelper(context).getShareTextIntent(violation.stackTraceToString()),
|
||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -280,6 +280,10 @@
|
|||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
|
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
|
||||||
android:label="@string/local_manga_processing" />
|
android:label="@string/local_manga_processing" />
|
||||||
|
<service
|
||||||
|
android:name="org.koitharu.kotatsu.settings.backup.RestoreService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:label="@string/restore_backup" />
|
||||||
<service
|
<service
|
||||||
android:name="org.koitharu.kotatsu.local.ui.ImportService"
|
android:name="org.koitharu.kotatsu.local.ui.ImportService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package org.koitharu.kotatsu.core.backup
|
package org.koitharu.kotatsu.core.backup
|
||||||
|
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
|
import kotlinx.coroutines.flow.FlowCollector
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
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.core.util.progress.Progress
|
||||||
import org.koitharu.kotatsu.parsers.util.json.asTypedList
|
import org.koitharu.kotatsu.parsers.util.json.asTypedList
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||||
@@ -128,9 +130,11 @@ class BackupRepository @Inject constructor(
|
|||||||
return if (timestamp == 0L) null else Date(timestamp)
|
return if (timestamp == 0L) null else Date(timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
|
suspend fun restoreHistory(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
|
||||||
val result = CompositeResult()
|
val result = CompositeResult()
|
||||||
for (item in entry.data.asTypedList<JSONObject>()) {
|
val list = entry.data.asTypedList<JSONObject>()
|
||||||
|
outProgress?.emit(Progress(progress = 0, total = list.size))
|
||||||
|
for ((index, item) in list.withIndex()) {
|
||||||
val mangaJson = item.getJSONObject("manga")
|
val mangaJson = item.getJSONObject("manga")
|
||||||
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
||||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||||
@@ -144,6 +148,7 @@ class BackupRepository @Inject constructor(
|
|||||||
db.getHistoryDao().upsert(history)
|
db.getHistoryDao().upsert(history)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
outProgress?.emit(Progress(progress = index, total = list.size))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -159,9 +164,11 @@ class BackupRepository @Inject constructor(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun restoreFavourites(entry: BackupEntry): CompositeResult {
|
suspend fun restoreFavourites(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
|
||||||
val result = CompositeResult()
|
val result = CompositeResult()
|
||||||
for (item in entry.data.asTypedList<JSONObject>()) {
|
val list = entry.data.asTypedList<JSONObject>()
|
||||||
|
outProgress?.emit(Progress(progress = 0, total = list.size))
|
||||||
|
for ((index, item) in list.withIndex()) {
|
||||||
val mangaJson = item.getJSONObject("manga")
|
val mangaJson = item.getJSONObject("manga")
|
||||||
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
||||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||||
@@ -175,6 +182,7 @@ class BackupRepository @Inject constructor(
|
|||||||
db.getFavouritesDao().upsert(favourite)
|
db.getFavouritesDao().upsert(favourite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
outProgress?.emit(Progress(progress = index, total = list.size))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class CompositeResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operator fun plusAssign(error: Throwable) {
|
||||||
|
errors.add(error)
|
||||||
|
}
|
||||||
|
|
||||||
operator fun plusAssign(other: CompositeResult) {
|
operator fun plusAssign(other: CompositeResult) {
|
||||||
this.successCount += other.successCount
|
this.successCount += other.successCount
|
||||||
this.errors += other.errors
|
this.errors += other.errors
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet
|
|||||||
import org.koitharu.kotatsu.list.ui.config.ListConfigSection
|
import org.koitharu.kotatsu.list.ui.config.ListConfigSection
|
||||||
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
|
||||||
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
|
||||||
|
import org.koitharu.kotatsu.main.ui.MainActivity
|
||||||
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
@@ -512,6 +513,8 @@ class AppRouter private constructor(
|
|||||||
|
|
||||||
fun suggestionsIntent(context: Context) = Intent(context, SuggestionsActivity::class.java)
|
fun suggestionsIntent(context: Context) = Intent(context, SuggestionsActivity::class.java)
|
||||||
|
|
||||||
|
fun homeIntent(context: Context) = Intent(context, MainActivity::class.java)
|
||||||
|
|
||||||
fun mangaUpdatesIntent(context: Context) = Intent(context, UpdatesActivity::class.java)
|
fun mangaUpdatesIntent(context: Context) = Intent(context, UpdatesActivity::class.java)
|
||||||
|
|
||||||
fun readerSettingsIntent(context: Context) =
|
fun readerSettingsIntent(context: Context) =
|
||||||
@@ -561,9 +564,14 @@ class AppRouter private constructor(
|
|||||||
.putExtra(KEY_SOURCE, source.name)
|
.putExtra(KEY_SOURCE, source.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const val KEY_DATA = "data"
|
||||||
|
const val KEY_ENTRIES = "entries"
|
||||||
|
const val KEY_ERROR = "error"
|
||||||
const val KEY_EXCLUDE = "exclude"
|
const val KEY_EXCLUDE = "exclude"
|
||||||
|
const val KEY_FILE = "file"
|
||||||
const val KEY_FILTER = "filter"
|
const val KEY_FILTER = "filter"
|
||||||
const val KEY_ID = "id"
|
const val KEY_ID = "id"
|
||||||
|
const val KEY_INDEX = "index"
|
||||||
const val KEY_LIST_SECTION = "list_section"
|
const val KEY_LIST_SECTION = "list_section"
|
||||||
const val KEY_MANGA = "manga"
|
const val KEY_MANGA = "manga"
|
||||||
const val KEY_MANGA_LIST = "manga_list"
|
const val KEY_MANGA_LIST = "manga_list"
|
||||||
@@ -573,12 +581,8 @@ class AppRouter private constructor(
|
|||||||
const val KEY_SOURCE = "source"
|
const val KEY_SOURCE = "source"
|
||||||
const val KEY_TAB = "tab"
|
const val KEY_TAB = "tab"
|
||||||
const val KEY_TITLE = "title"
|
const val KEY_TITLE = "title"
|
||||||
const val KEY_USER_AGENT = "user_agent"
|
|
||||||
const val KEY_URL = "url"
|
const val KEY_URL = "url"
|
||||||
const val KEY_ERROR = "error"
|
const val KEY_USER_AGENT = "user_agent"
|
||||||
const val KEY_FILE = "file"
|
|
||||||
const val KEY_INDEX = "index"
|
|
||||||
const val KEY_DATA = "data"
|
|
||||||
|
|
||||||
const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY"
|
const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY"
|
||||||
const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS"
|
const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.core.app.ServiceCompat
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -42,6 +43,8 @@ abstract class CoroutineIntentService : BaseService() {
|
|||||||
intentJobContext.processIntent(intent)
|
intentJobContext.processIntent(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
intentJobContext.onError(e)
|
intentJobContext.onError(e)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.storage.StorageManager
|
import android.os.storage.StorageManager
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.net.toFile
|
||||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||||
import org.koitharu.kotatsu.parsers.util.removeSuffix
|
import org.koitharu.kotatsu.parsers.util.removeSuffix
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -31,6 +34,21 @@ fun Uri.resolveFile(context: Context): File? {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ContentResolver.getFileDisplayName(uri: Uri): String? = runCatching {
|
||||||
|
if (uri.isFileUri()) {
|
||||||
|
return@runCatching uri.toFile().name
|
||||||
|
}
|
||||||
|
query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
}.getOrNull()
|
||||||
|
|
||||||
private fun getVolumePath(volumeId: String, context: Context): String? {
|
private fun getVolumePath(volumeId: String, context: Context): String? {
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
getVolumePathForAndroid11AndAbove(volumeId, context)
|
getVolumePathForAndroid11AndAbove(volumeId, context)
|
||||||
@@ -63,7 +81,7 @@ private fun getVolumePathBeforeAndroid11(volumeId: String, context: Context): St
|
|||||||
it.printStackTraceDebug()
|
it.printStackTraceDebug()
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun getVolumePathForAndroid11AndAbove(volumeId: String, context: Context): String? = runCatching {
|
private fun getVolumePathForAndroid11AndAbove(volumeId: String, context: Context): String? = runCatching {
|
||||||
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||||
storageManager.storageVolumes.firstNotNullOfOrNull { volume ->
|
storageManager.storageVolumes.firstNotNullOfOrNull { volume ->
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util.progress
|
||||||
|
|
||||||
|
data class Progress(
|
||||||
|
val progress: Int,
|
||||||
|
val total: Int,
|
||||||
|
) : Comparable<Progress> {
|
||||||
|
|
||||||
|
val percent: Float
|
||||||
|
get() = if (total == 0) 0f else progress / total.toFloat()
|
||||||
|
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = progress == 0
|
||||||
|
|
||||||
|
val isFull: Boolean
|
||||||
|
get() = progress == total
|
||||||
|
|
||||||
|
override fun compareTo(other: Progress): Int = if (total == other.total) {
|
||||||
|
progress.compareTo(other.progress)
|
||||||
|
} else {
|
||||||
|
percent.compareTo(other.percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun inc() = if (isFull) {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
copy(
|
||||||
|
progress = progress + 1,
|
||||||
|
total = total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun dec() = if (isEmpty) {
|
||||||
|
this
|
||||||
|
} else {
|
||||||
|
copy(
|
||||||
|
progress = progress - 1,
|
||||||
|
total = total,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plus(child: Progress) = Progress(
|
||||||
|
progress = progress * child.total + child.progress,
|
||||||
|
total = total * child.total,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun percentSting() = (percent * 100f).toInt().toString()
|
||||||
|
}
|
||||||
@@ -97,9 +97,9 @@ class AppBackupAgent : BackupAgent() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it) }
|
backup.getEntry(BackupEntry.Name.HISTORY)?.let { repository.restoreHistory(it, null) }
|
||||||
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { repository.restoreCategories(it) }
|
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let { repository.restoreCategories(it) }
|
||||||
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it) }
|
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let { repository.restoreFavourites(it, null) }
|
||||||
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
|
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let { repository.restoreBookmarks(it) }
|
||||||
backup.getEntry(BackupEntry.Name.SOURCES)?.let { repository.restoreSources(it) }
|
backup.getEntry(BackupEntry.Name.SOURCES)?.let { repository.restoreSources(it) }
|
||||||
backup.getEntry(BackupEntry.Name.SETTINGS)?.let { repository.restoreSettings(it) }
|
backup.getEntry(BackupEntry.Name.SETTINGS)?.let { repository.restoreSettings(it) }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
@@ -11,7 +12,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.combine
|
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.nav.router
|
import org.koitharu.kotatsu.core.nav.router
|
||||||
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.ui.list.OnListItemClickListener
|
||||||
@@ -23,7 +23,6 @@ import org.koitharu.kotatsu.databinding.DialogRestoreBinding
|
|||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupEntryModel>,
|
class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnListItemClickListener<BackupEntryModel>,
|
||||||
@@ -43,8 +42,6 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
|
|||||||
binding.buttonCancel.setOnClickListener(this)
|
binding.buttonCancel.setOnClickListener(this)
|
||||||
binding.buttonRestore.setOnClickListener(this)
|
binding.buttonRestore.setOnClickListener(this)
|
||||||
viewModel.availableEntries.observe(viewLifecycleOwner, adapter)
|
viewModel.availableEntries.observe(viewLifecycleOwner, adapter)
|
||||||
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
|
|
||||||
viewModel.onRestoreDone.observeEvent(viewLifecycleOwner, this::onRestoreDone)
|
|
||||||
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
|
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
|
||||||
combine(
|
combine(
|
||||||
viewModel.isLoading,
|
viewModel.isLoading,
|
||||||
@@ -63,7 +60,15 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
|
|||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
when (v.id) {
|
when (v.id) {
|
||||||
R.id.button_cancel -> dismiss()
|
R.id.button_cancel -> dismiss()
|
||||||
R.id.button_restore -> viewModel.restore()
|
R.id.button_restore -> {
|
||||||
|
if (startRestoreService()) {
|
||||||
|
Toast.makeText(v.context, R.string.backup_restored_background, Toast.LENGTH_SHORT).show()
|
||||||
|
router.closeWelcomeSheet()
|
||||||
|
dismiss()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(v.context, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +92,14 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startRestoreService(): Boolean {
|
||||||
|
return RestoreService.start(
|
||||||
|
context ?: return false,
|
||||||
|
viewModel.uri ?: return false,
|
||||||
|
viewModel.getCheckedEntries(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun Date.formatBackupDate(): String {
|
private fun Date.formatBackupDate(): String {
|
||||||
return getString(
|
return getString(
|
||||||
R.string.backup_date_,
|
R.string.backup_date_,
|
||||||
@@ -102,46 +115,4 @@ class RestoreDialogFragment : AlertDialogFragment<DialogRestoreBinding>(), OnLis
|
|||||||
.show()
|
.show()
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProgressChanged(value: Float) {
|
|
||||||
with(requireViewBinding().progressBar) {
|
|
||||||
isVisible = true
|
|
||||||
val wasIndeterminate = isIndeterminate
|
|
||||||
isIndeterminate = value < 0
|
|
||||||
if (value >= 0) {
|
|
||||||
setProgressCompat((value * max).roundToInt(), !wasIndeterminate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onRestoreDone(result: CompositeResult) {
|
|
||||||
val builder = MaterialAlertDialogBuilder(context ?: return)
|
|
||||||
when {
|
|
||||||
result.isEmpty -> {
|
|
||||||
builder.setTitle(R.string.data_not_restored)
|
|
||||||
.setMessage(R.string.data_not_restored_text)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.isAllSuccess -> {
|
|
||||||
builder.setTitle(R.string.data_restored)
|
|
||||||
.setMessage(R.string.data_restored_success)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.isAllFailed -> builder.setTitle(R.string.error)
|
|
||||||
.setMessage(
|
|
||||||
result.failures.map {
|
|
||||||
it.getDisplayMessage(resources)
|
|
||||||
}.distinct().joinToString("\n"),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> builder.setTitle(R.string.data_restored)
|
|
||||||
.setMessage(R.string.data_restored_with_errors)
|
|
||||||
}
|
|
||||||
builder.setPositiveButton(android.R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
if (!result.isEmpty && !result.isAllFailed) {
|
|
||||||
router.closeWelcomeSheet()
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,287 @@
|
|||||||
|
package org.koitharu.kotatsu.settings.backup
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.app.PendingIntentCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.ErrorReporterReceiver
|
||||||
|
import org.koitharu.kotatsu.core.backup.BackupEntry
|
||||||
|
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||||
|
import org.koitharu.kotatsu.core.backup.BackupZipInput
|
||||||
|
import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||||
|
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||||
|
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getFileDisplayName
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||||
|
import org.koitharu.kotatsu.core.util.progress.Progress
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapToArray
|
||||||
|
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.util.EnumSet
|
||||||
|
import javax.inject.Inject
|
||||||
|
import com.google.android.material.R as materialR
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class RestoreService : CoroutineIntentService() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var repository: BackupRepository
|
||||||
|
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||||
|
startForeground(this)
|
||||||
|
val uri = intent.getStringExtra(AppRouter.KEY_DATA)?.toUriOrNull() ?: throw FileNotFoundException()
|
||||||
|
val displayName = contentResolver.getFileDisplayName(uri)
|
||||||
|
val entries = intent.getIntArrayExtra(AppRouter.KEY_ENTRIES)
|
||||||
|
?.mapTo(EnumSet.noneOf(BackupEntry.Name::class.java)) { BackupEntry.Name.entries[it] }
|
||||||
|
if (entries.isNullOrEmpty()) {
|
||||||
|
throw IllegalArgumentException("No entries specified")
|
||||||
|
}
|
||||||
|
val result = runInterruptible(Dispatchers.IO) {
|
||||||
|
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||||
|
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackupZipInput.from(tempFile)
|
||||||
|
}.use { backupInput ->
|
||||||
|
restoreImpl(displayName, backupInput, entries)
|
||||||
|
}
|
||||||
|
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||||
|
val notification = buildNotification(displayName, result)
|
||||||
|
notificationManager.notify(TAG, startId, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun IntentJobContext.onError(error: Throwable) {
|
||||||
|
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||||
|
val result = CompositeResult()
|
||||||
|
result += error
|
||||||
|
val notification = buildNotification(null, result)
|
||||||
|
notificationManager.notify(TAG, startId, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun IntentJobContext.restoreImpl(
|
||||||
|
displayName: String?,
|
||||||
|
input: BackupZipInput,
|
||||||
|
entries: Set<BackupEntry.Name>
|
||||||
|
): CompositeResult {
|
||||||
|
val result = CompositeResult()
|
||||||
|
val showNotification = applicationContext.checkNotificationPermission(CHANNEL_ID)
|
||||||
|
var progress = Progress(0, entries.size)
|
||||||
|
|
||||||
|
fun notify(childProgress: Progress? = null) {
|
||||||
|
if (showNotification) {
|
||||||
|
val p = childProgress?.let { progress + it } ?: progress
|
||||||
|
notificationManager.notify(FOREGROUND_NOTIFICATION_ID, buildNotification(displayName, p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify()
|
||||||
|
|
||||||
|
if (BackupEntry.Name.HISTORY in entries) {
|
||||||
|
input.getEntry(BackupEntry.Name.HISTORY)?.let {
|
||||||
|
flow {
|
||||||
|
result += repository.restoreHistory(it, this)
|
||||||
|
}.collect { p ->
|
||||||
|
notify(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress++
|
||||||
|
}
|
||||||
|
|
||||||
|
notify()
|
||||||
|
|
||||||
|
if (BackupEntry.Name.CATEGORIES in entries) {
|
||||||
|
input.getEntry(BackupEntry.Name.CATEGORIES)?.let {
|
||||||
|
result += repository.restoreCategories(it)
|
||||||
|
}
|
||||||
|
progress++
|
||||||
|
}
|
||||||
|
|
||||||
|
notify()
|
||||||
|
|
||||||
|
if (BackupEntry.Name.FAVOURITES in entries) {
|
||||||
|
input.getEntry(BackupEntry.Name.FAVOURITES)?.let {
|
||||||
|
flow {
|
||||||
|
result += repository.restoreFavourites(it, this)
|
||||||
|
}.collect { p ->
|
||||||
|
notify(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify()
|
||||||
|
|
||||||
|
if (BackupEntry.Name.BOOKMARKS in entries) {
|
||||||
|
input.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
|
||||||
|
result += repository.restoreBookmarks(it)
|
||||||
|
}
|
||||||
|
progress++
|
||||||
|
}
|
||||||
|
|
||||||
|
notify()
|
||||||
|
|
||||||
|
if (BackupEntry.Name.SOURCES in entries) {
|
||||||
|
input.getEntry(BackupEntry.Name.SOURCES)?.let {
|
||||||
|
result += repository.restoreSources(it)
|
||||||
|
}
|
||||||
|
progress++
|
||||||
|
}
|
||||||
|
|
||||||
|
notify()
|
||||||
|
|
||||||
|
if (BackupEntry.Name.SETTINGS in entries) {
|
||||||
|
input.getEntry(BackupEntry.Name.SETTINGS)?.let {
|
||||||
|
result += repository.restoreSettings(it)
|
||||||
|
}
|
||||||
|
progress++
|
||||||
|
}
|
||||||
|
|
||||||
|
notify()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
private fun startForeground(jobContext: IntentJobContext) {
|
||||||
|
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||||
|
.setName(getString(R.string.restoring_backup))
|
||||||
|
.setShowBadge(true)
|
||||||
|
.setVibrationEnabled(false)
|
||||||
|
.setSound(null, null)
|
||||||
|
.setLightsEnabled(false)
|
||||||
|
.build()
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
|
||||||
|
val notification = jobContext.buildNotification(null, null)
|
||||||
|
|
||||||
|
jobContext.setForeground(
|
||||||
|
FOREGROUND_NOTIFICATION_ID,
|
||||||
|
notification,
|
||||||
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun IntentJobContext.buildNotification(fileName: String?, progress: Progress?): Notification {
|
||||||
|
return NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||||
|
.setContentTitle(getString(R.string.restoring_backup))
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setDefaults(0)
|
||||||
|
.setSilent(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setProgress(progress?.total ?: 0, progress?.progress ?: 0, progress == null)
|
||||||
|
.setContentText(
|
||||||
|
concatStrings(
|
||||||
|
context = this@RestoreService,
|
||||||
|
a = fileName,
|
||||||
|
b = progress?.run { getString(R.string.percent_string_pattern, percentSting()) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||||
|
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||||
|
.addAction(
|
||||||
|
materialR.drawable.material_ic_clear_black_24dp,
|
||||||
|
applicationContext.getString(android.R.string.cancel),
|
||||||
|
getCancelIntent(),
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(fileName: String?, result: CompositeResult): Notification {
|
||||||
|
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setDefaults(0)
|
||||||
|
.setSilent(true)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setSubText(fileName)
|
||||||
|
|
||||||
|
when {
|
||||||
|
result.isEmpty -> notification.setContentTitle(getString(R.string.data_not_restored))
|
||||||
|
.setContentText(getString(R.string.data_not_restored_text))
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
|
||||||
|
result.isAllSuccess -> notification.setContentTitle(getString(R.string.data_restored))
|
||||||
|
.setContentText(getString(R.string.data_restored_success))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_done)
|
||||||
|
|
||||||
|
result.isAllFailed -> notification.setContentTitle(getString(R.string.error))
|
||||||
|
.setContentText(
|
||||||
|
result.failures.map { it.getDisplayMessage(resources) }.distinct().joinToString("\n"),
|
||||||
|
)
|
||||||
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
|
|
||||||
|
else -> notification.setContentTitle(getString(R.string.data_restored))
|
||||||
|
.setContentText(getString(R.string.data_restored_with_errors))
|
||||||
|
.setSmallIcon(R.drawable.ic_stat_done)
|
||||||
|
}
|
||||||
|
result.failures.firstOrNull()?.let { error ->
|
||||||
|
ErrorReporterReceiver.getPendingIntent(applicationContext, error)?.let { reportIntent ->
|
||||||
|
notification.addAction(
|
||||||
|
R.drawable.ic_alert_outline,
|
||||||
|
applicationContext.getString(R.string.report),
|
||||||
|
reportIntent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notification.setContentIntent(
|
||||||
|
PendingIntentCompat.getActivity(
|
||||||
|
applicationContext,
|
||||||
|
0,
|
||||||
|
AppRouter.homeIntent(this),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return notification.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun concatStrings(context: Context, a: String?, b: String?): String? = when {
|
||||||
|
a.isNullOrEmpty() && b.isNullOrEmpty() -> null
|
||||||
|
a.isNullOrEmpty() -> b?.nullIfEmpty()
|
||||||
|
b.isNullOrEmpty() -> a.nullIfEmpty()
|
||||||
|
else -> context.getString(R.string.download_summary_pattern, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "restore"
|
||||||
|
private const val CHANNEL_ID = "restore_backup"
|
||||||
|
private const val FOREGROUND_NOTIFICATION_ID = 39
|
||||||
|
|
||||||
|
fun start(context: Context, uri: Uri, entries: Set<BackupEntry.Name>): Boolean = try {
|
||||||
|
val intent = Intent(context, RestoreService::class.java)
|
||||||
|
intent.putExtra(AppRouter.KEY_DATA, uri.toString())
|
||||||
|
intent.putExtra(AppRouter.KEY_ENTRIES, entries.mapToArray { it.ordinal }.toIntArray())
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTraceDebug()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,14 +10,9 @@ import kotlinx.coroutines.runInterruptible
|
|||||||
import org.koitharu.kotatsu.core.backup.BackupEntry
|
import org.koitharu.kotatsu.core.backup.BackupEntry
|
||||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||||
import org.koitharu.kotatsu.core.backup.BackupZipInput
|
import org.koitharu.kotatsu.core.backup.BackupZipInput
|
||||||
import org.koitharu.kotatsu.core.backup.CompositeResult
|
|
||||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
|
||||||
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -32,30 +27,28 @@ class RestoreViewModel @Inject constructor(
|
|||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
private val backupInput = suspendLazy {
|
val uri = savedStateHandle.get<String>(AppRouter.KEY_FILE)?.toUriOrNull()
|
||||||
val uri = savedStateHandle.get<String>(AppRouter.KEY_FILE)
|
private val contentResolver = context.contentResolver
|
||||||
?.toUriOrNull() ?: throw FileNotFoundException()
|
|
||||||
val contentResolver = context.contentResolver
|
|
||||||
runInterruptible(Dispatchers.IO) {
|
|
||||||
val tempFile = File.createTempFile("backup_", ".tmp")
|
|
||||||
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
|
|
||||||
tempFile.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BackupZipInput.from(tempFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val progress = MutableStateFlow(-1f)
|
|
||||||
val onRestoreDone = MutableEventFlow<CompositeResult>()
|
|
||||||
|
|
||||||
val availableEntries = MutableStateFlow<List<BackupEntryModel>>(emptyList())
|
val availableEntries = MutableStateFlow<List<BackupEntryModel>>(emptyList())
|
||||||
val backupDate = MutableStateFlow<Date?>(null)
|
val backupDate = MutableStateFlow<Date?>(null)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
launchLoadingJob(Dispatchers.Default) {
|
launchLoadingJob(Dispatchers.Default) {
|
||||||
val backup = backupInput.get()
|
loadBackupInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadBackupInfo() {
|
||||||
|
runInterruptible(Dispatchers.IO) {
|
||||||
|
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||||
|
(uri?.let { contentResolver.openInputStream(it) } ?: throw FileNotFoundException()).use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackupZipInput.from(tempFile)
|
||||||
|
}.use { backup ->
|
||||||
val entries = backup.entries()
|
val entries = backup.entries()
|
||||||
availableEntries.value = BackupEntry.Name.entries.mapNotNull { entry ->
|
availableEntries.value = BackupEntry.Name.entries.mapNotNull { entry ->
|
||||||
if (entry == BackupEntry.Name.INDEX || entry !in entries) {
|
if (entry == BackupEntry.Name.INDEX || entry !in entries) {
|
||||||
@@ -71,15 +64,6 @@ class RestoreViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
runCatching {
|
|
||||||
backupInput.peek()?.closeAndDelete()
|
|
||||||
}.onFailure {
|
|
||||||
it.printStackTraceDebug()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onItemClick(item: BackupEntryModel) {
|
fun onItemClick(item: BackupEntryModel) {
|
||||||
val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
|
val map = availableEntries.value.associateByTo(EnumMap(BackupEntry.Name::class.java)) { it.name }
|
||||||
map[item.name] = item.copy(isChecked = !item.isChecked)
|
map[item.name] = item.copy(isChecked = !item.isChecked)
|
||||||
@@ -87,61 +71,10 @@ class RestoreViewModel @Inject constructor(
|
|||||||
availableEntries.value = map.values.sortedBy { it.name.ordinal }
|
availableEntries.value = map.values.sortedBy { it.name.ordinal }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restore() {
|
fun getCheckedEntries(): Set<BackupEntry.Name> = availableEntries.value
|
||||||
launchLoadingJob {
|
.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
|
||||||
val backup = backupInput.get()
|
|
||||||
val checkedItems = availableEntries.value.mapNotNullTo(EnumSet.noneOf(BackupEntry.Name::class.java)) {
|
|
||||||
if (it.isChecked) it.name else null
|
if (it.isChecked) it.name else null
|
||||||
}
|
}
|
||||||
val result = CompositeResult()
|
|
||||||
val step = 1f / 6f
|
|
||||||
|
|
||||||
progress.value = 0f
|
|
||||||
if (BackupEntry.Name.HISTORY in checkedItems) {
|
|
||||||
backup.getEntry(BackupEntry.Name.HISTORY)?.let {
|
|
||||||
result += repository.restoreHistory(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.value += step
|
|
||||||
if (BackupEntry.Name.CATEGORIES in checkedItems) {
|
|
||||||
backup.getEntry(BackupEntry.Name.CATEGORIES)?.let {
|
|
||||||
result += repository.restoreCategories(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.value += step
|
|
||||||
if (BackupEntry.Name.FAVOURITES in checkedItems) {
|
|
||||||
backup.getEntry(BackupEntry.Name.FAVOURITES)?.let {
|
|
||||||
result += repository.restoreFavourites(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.value += step
|
|
||||||
if (BackupEntry.Name.BOOKMARKS in checkedItems) {
|
|
||||||
backup.getEntry(BackupEntry.Name.BOOKMARKS)?.let {
|
|
||||||
result += repository.restoreBookmarks(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.value += step
|
|
||||||
if (BackupEntry.Name.SOURCES in checkedItems) {
|
|
||||||
backup.getEntry(BackupEntry.Name.SOURCES)?.let {
|
|
||||||
result += repository.restoreSources(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.value += step
|
|
||||||
if (BackupEntry.Name.SETTINGS in checkedItems) {
|
|
||||||
backup.getEntry(BackupEntry.Name.SETTINGS)?.let {
|
|
||||||
result += repository.restoreSettings(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progress.value = 1f
|
|
||||||
onRestoreDone.call(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for inconsistent user selection
|
* Check for inconsistent user selection
|
||||||
|
|||||||
@@ -793,4 +793,6 @@
|
|||||||
<string name="enable_all_sources_summary">All available manga sources will be enabled permanently</string>
|
<string name="enable_all_sources_summary">All available manga sources will be enabled permanently</string>
|
||||||
<string name="all_sources_enabled">All sources are enabled</string>
|
<string name="all_sources_enabled">All sources are enabled</string>
|
||||||
<string name="reader_info_bar_transparent">Transparent reader information bar</string>
|
<string name="reader_info_bar_transparent">Transparent reader information bar</string>
|
||||||
|
<string name="backup_restored_background">The backup will be restored in the background</string>
|
||||||
|
<string name="restoring_backup">Restoring backup</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ ksp = "2.0.21-1.0.28"
|
|||||||
leakcanary = "3.0-alpha-8"
|
leakcanary = "3.0-alpha-8"
|
||||||
lifecycle = "2.8.7"
|
lifecycle = "2.8.7"
|
||||||
markwon = "4.6.2"
|
markwon = "4.6.2"
|
||||||
material = "1.13.0-alpha09"
|
material = "1.13.0-alpha10"
|
||||||
moshi = "1.15.2"
|
moshi = "1.15.2"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
okio = "3.10.2"
|
okio = "3.10.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user