Merge branch 'feature/backup-settings' of github.com:javlonrahimov/Kotatsu into javlonrahimov-feature/backup-settings

pull/440/head
Koitharu 3 years ago
commit f86d873361
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -4,6 +4,7 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" /> <option name="gradleJvm" value="jbr-17" />
<option name="modules"> <option name="modules">

@ -13,5 +13,6 @@ class BackupEntry(
const val HISTORY = "history" const val HISTORY = "history"
const val CATEGORIES = "categories" const val CATEGORIES = "categories"
const val FAVOURITES = "favourites" const val FAVOURITES = "favourites"
const val SETTINGS = "settings"
} }
} }

@ -5,6 +5,7 @@ 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.parsers.util.json.JSONIterator import org.koitharu.kotatsu.parsers.util.json.JSONIterator
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
@ -12,7 +13,10 @@ import javax.inject.Inject
private const val PAGE_SIZE = 10 private const val PAGE_SIZE = 10
class BackupRepository @Inject constructor(private val db: MangaDatabase) { class BackupRepository @Inject constructor(
private val db: MangaDatabase,
private val settings: AppSettings,
) {
suspend fun dumpHistory(): BackupEntry { suspend fun dumpHistory(): BackupEntry {
var offset = 0 var offset = 0
@ -67,6 +71,13 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
return entry return entry
} }
fun dumpSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray())
val json = JsonSerializer(settings.getAllValues()).toJson()
entry.data.put(json)
return entry
}
fun createIndex(): BackupEntry { fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray()) val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val json = JSONObject() val json = JSONObject()
@ -127,4 +138,14 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
} }
return result return result
} }
fun restoreSettings(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
result += runCatchingCancellable {
settings.restoreValuesFromMap(JsonDeserializer(item).toMap())
}
}
return result
}
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
@ -34,14 +35,14 @@ class JsonDeserializer(private val json: JSONObject) {
largeCoverUrl = json.getStringOrNull("large_cover_url"), largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"), state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"), author = json.getStringOrNull("author"),
source = json.getString("source") source = json.getString("source"),
) )
fun toTagEntity() = TagEntity( fun toTagEntity() = TagEntity(
id = json.getLong("id"), id = json.getLong("id"),
title = json.getString("title"), title = json.getString("title"),
key = json.getString("key"), key = json.getString("key"),
source = json.getString("source") source = json.getString("source"),
) )
fun toHistoryEntity() = HistoryEntity( fun toHistoryEntity() = HistoryEntity(
@ -65,4 +66,29 @@ class JsonDeserializer(private val json: JSONObject) {
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true), isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
deletedAt = 0L, deletedAt = 0L,
) )
fun toMap(): Map<String, Any?> {
val map = mutableMapOf<String, Any?>()
val keys = json.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = json.get(key)
map[key] = value
}
return map
}
} }
fun <K, T> JSONArray.mapJSONToSet(block: (K) -> T): Set<T> {
val len = length()
val result = androidx.collection.ArraySet<T>(len)
for (i in 0 until len) {
val jo = get(i) as K
result.add(block(jo))
}
return result
}

@ -15,7 +15,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("category_id", e.categoryId) put("category_id", e.categoryId)
put("sort_key", e.sortKey) put("sort_key", e.sortKey)
put("created_at", e.createdAt) put("created_at", e.createdAt)
} },
) )
constructor(e: FavouriteCategoryEntity) : this( constructor(e: FavouriteCategoryEntity) : this(
@ -27,7 +27,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("order", e.order) put("order", e.order)
put("track", e.track) put("track", e.track)
put("show_in_lib", e.isVisibleInLibrary) put("show_in_lib", e.isVisibleInLibrary)
} },
) )
constructor(e: HistoryEntity) : this( constructor(e: HistoryEntity) : this(
@ -39,7 +39,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("page", e.page) put("page", e.page)
put("scroll", e.scroll) put("scroll", e.scroll)
put("percent", e.percent) put("percent", e.percent)
} },
) )
constructor(e: TagEntity) : this( constructor(e: TagEntity) : this(
@ -48,7 +48,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("title", e.title) put("title", e.title)
put("key", e.key) put("key", e.key)
put("source", e.source) put("source", e.source)
} },
) )
constructor(e: MangaEntity) : this( constructor(e: MangaEntity) : this(
@ -65,7 +65,11 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("state", e.state) put("state", e.state)
put("author", e.author) put("author", e.author)
put("source", e.source) put("source", e.source)
} },
)
constructor(m: Map<String, *>) : this(
JSONObject(m),
) )
fun toJson(): JSONObject = json fun toJson(): JSONObject = json

@ -13,7 +13,9 @@ import androidx.core.content.edit
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.json.JSONArray
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.backup.mapJSONToSet
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.connectivityManager
@ -380,6 +382,23 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
fun observe() = prefs.observe() fun observe() = prefs.observe()
fun getAllValues(): Map<String, *> = prefs.all
fun restoreValuesFromMap(m: Map<String, *>) {
prefs.edit {
m.forEach { e ->
when (e.value) {
is Boolean -> putBoolean(e.key, e.value as Boolean)
is Int -> putInt(e.key, e.value as Int)
is Long -> putLong(e.key, e.value as Long)
is Float -> putFloat(e.key, e.value as Float)
is String -> putString(e.key, e.value as String)
is JSONArray -> putStringSet(e.key, (e.value as JSONArray).mapJSONToSet<String, String> { it })
}
}
}
}
private fun isBackgroundNetworkRestricted(): Boolean { private fun isBackgroundNetworkRestricted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.content.SharedPreferences
import androidx.annotation.FloatRange import androidx.annotation.FloatRange
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import java.util.UUID import java.util.UUID
@ -40,3 +41,9 @@ fun CharSequence.sanitize(): CharSequence {
} }
fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF' fun Char.isReplacement() = this in '\uFFF0'..'\uFFFF'
fun <E : Enum<E>> String.getEnumValue(defaultValue: E): E {
return defaultValue.javaClass.enumConstants?.find {
it.name == this
} ?: defaultValue
}

@ -8,6 +8,8 @@ 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.appcompat.app.AppCompatDelegate
import androidx.core.view.postDelayed
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.TwoStatePreference import androidx.preference.TwoStatePreference
@ -24,6 +26,7 @@ import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.os.AppShortcutManager import org.koitharu.kotatsu.core.os.AppShortcutManager
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.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.FileSize import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ext.awaitStateAtLeast import org.koitharu.kotatsu.core.util.ext.awaitStateAtLeast
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
@ -61,6 +64,9 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
@Inject @Inject
lateinit var appShortcutManager: AppShortcutManager lateinit var appShortcutManager: AppShortcutManager
@Inject
lateinit var activityRecreationHandle: ActivityRecreationHandle
private val backupSelectCall = registerForActivityResult( private val backupSelectCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(), ActivityResultContracts.OpenDocument(),
this, this,
@ -180,6 +186,19 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP) findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty() ?.isChecked = !settings.appPassword.isNullOrEmpty()
} }
AppSettings.KEY_THEME -> {
AppCompatDelegate.setDefaultNightMode(settings.theme)
}
AppSettings.KEY_COLOR_THEME,
AppSettings.KEY_THEME_AMOLED -> {
postRestart()
}
AppSettings.KEY_APP_LOCALE -> {
AppCompatDelegate.setApplicationLocales(settings.appLocales)
}
} }
} }
@ -273,4 +292,11 @@ class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privac
} }
}.show() }.show()
} }
private fun postRestart() {
view?.postDelayed(400) {
activityRecreationHandle.recreateAll()
}
}
} }

@ -13,6 +13,7 @@ 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.BackupZipOutput import org.koitharu.kotatsu.core.backup.BackupZipOutput
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import java.io.* import java.io.*
class AppBackupAgent : BackupAgent() { class AppBackupAgent : BackupAgent() {
@ -31,7 +32,8 @@ class AppBackupAgent : BackupAgent() {
override fun onFullBackup(data: FullBackupDataOutput) { override fun onFullBackup(data: FullBackupDataOutput) {
super.onFullBackup(data) super.onFullBackup(data)
val file = createBackupFile(this, BackupRepository(MangaDatabase(applicationContext))) val file =
createBackupFile(this, BackupRepository(MangaDatabase(applicationContext), AppSettings(applicationContext)))
try { try {
fullBackupFile(file, data) fullBackupFile(file, data)
} finally { } finally {
@ -48,7 +50,7 @@ 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))) 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)
@ -62,6 +64,7 @@ class AppBackupAgent : BackupAgent() {
backup.put(repository.dumpHistory()) backup.put(repository.dumpHistory())
backup.put(repository.dumpCategories()) backup.put(repository.dumpCategories())
backup.put(repository.dumpFavourites()) backup.put(repository.dumpFavourites())
backup.put(repository.dumpSettings())
backup.finish() backup.finish()
backup.file backup.file
} }
@ -81,6 +84,7 @@ class AppBackupAgent : BackupAgent() {
repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY)) repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES)) repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES)) repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
repository.restoreSettings(backup.getEntry(BackupEntry.SETTINGS))
} }
} finally { } finally {
backup.close() backup.close()

@ -29,13 +29,15 @@ class BackupViewModel @Inject constructor(
progress.value = 0f progress.value = 0f
backup.put(repository.dumpHistory()) backup.put(repository.dumpHistory())
progress.value = 0.3f progress.value = 0.25f
backup.put(repository.dumpCategories()) backup.put(repository.dumpCategories())
progress.value = 0.6f progress.value = 0.5f
backup.put(repository.dumpFavourites()) backup.put(repository.dumpFavourites())
progress.value = 0.9f progress.value = 0.75f
backup.put(repository.dumpSettings())
backup.finish() backup.finish()
progress.value = 1f progress.value = 1f
backup.close() backup.close()

@ -22,6 +22,7 @@ import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() { class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private val viewModel: RestoreViewModel by viewModels() private val viewModel: RestoreViewModel by viewModels()
override fun onCreateViewBinding( override fun onCreateViewBinding(
@ -67,8 +68,10 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private fun onRestoreDone(result: CompositeResult) { private fun onRestoreDone(result: CompositeResult) {
val builder = MaterialAlertDialogBuilder(context ?: return) val builder = MaterialAlertDialogBuilder(context ?: return)
when { when {
result.isAllSuccess -> builder.setTitle(R.string.data_restored) result.isAllSuccess -> {
builder.setTitle(R.string.data_restored)
.setMessage(R.string.data_restored_success) .setMessage(R.string.data_restored_success)
}
result.isAllFailed -> builder.setTitle(R.string.error) result.isAllFailed -> builder.setTitle(R.string.error)
.setMessage( .setMessage(
@ -85,6 +88,7 @@ class RestoreDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
dismiss() dismiss()
} }
companion object { companion object {
const val ARG_FILE = "file" const val ARG_FILE = "file"

@ -50,12 +50,15 @@ class RestoreViewModel @Inject constructor(
progress.value = 0f progress.value = 0f
result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY)) result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
progress.value = 0.3f progress.value = 0.25f
result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES)) result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
progress.value = 0.6f progress.value = 0.5f
result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES)) result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
progress.value = 0.75f
result += repository.restoreSettings(backup.getEntry(BackupEntry.SETTINGS))
progress.value = 1f progress.value = 1f
onRestoreDone.call(result) onRestoreDone.call(result)
} finally { } finally {

Loading…
Cancel
Save