From aa8d41eecf32e46f7fbe2394eaa04d4fd0c8eb77 Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Sat, 3 Feb 2024 00:43:20 -0600 Subject: [PATCH] better setting export --- app/src/main/java/ani/dantotsu/Functions.kt | 20 ++-- .../java/ani/dantotsu/connections/mal/MAL.kt | 4 +- .../ani/dantotsu/settings/SettingsActivity.kt | 100 +++++++----------- .../dantotsu/settings/saving/PrefManager.kt | 17 +-- .../saving/internal/PreferencePackager.kt | 82 ++++++++++++++ 5 files changed, 135 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/settings/saving/internal/PreferencePackager.kt diff --git a/app/src/main/java/ani/dantotsu/Functions.kt b/app/src/main/java/ani/dantotsu/Functions.kt index 6c0805f4..21e30610 100644 --- a/app/src/main/java/ani/dantotsu/Functions.kt +++ b/app/src/main/java/ani/dantotsu/Functions.kt @@ -590,13 +590,13 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) { ) } -fun savePrefsToDownloads(title: String, map: Map, context: Context, password: CharArray? = null) { +fun savePrefsToDownloads(title: String, serialized: String, context: Context, password: CharArray? = null) { FileProvider.getUriForFile( context, "$APPLICATION_ID.provider", if (password != null) { savePrefs( - map, + serialized, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath, title, context, @@ -604,7 +604,7 @@ fun savePrefsToDownloads(title: String, map: Map, context: Context, p ) ?: return } else { savePrefs( - map, + serialized, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath, title, context @@ -613,7 +613,7 @@ fun savePrefsToDownloads(title: String, map: Map, context: Context, p ) } -fun savePrefs(map: Map, path: String, title: String, context: Context): File? { +fun savePrefs(serialized: String, path: String, title: String, context: Context): File? { var file = File(path, "$title.ani") var counter = 1 while (file.exists()) { @@ -622,9 +622,7 @@ fun savePrefs(map: Map, path: String, title: String, context: Context } return try { - val gson = Gson() - val json = gson.toJson(map) - file.writeText(json) + file.writeText(serialized) scanFile(file.absolutePath, context) toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath))) file @@ -634,20 +632,18 @@ fun savePrefs(map: Map, path: String, title: String, context: Context } } -fun savePrefs(map: Map, path: String, title: String, context: Context, password: CharArray): File? { +fun savePrefs(serialized: String, path: String, title: String, context: Context, password: CharArray): File? { var file = File(path, "$title.ani") var counter = 1 while (file.exists()) { - file = File(path, "${title}_${counter}.ani") + file = File(path, "${title}_${counter}.sani") counter++ } val salt = generateSalt() return try { - val gson = Gson() - val json = gson.toJson(map) - val encryptedData = PreferenceKeystore.encryptWithPassword(password, json, salt) + val encryptedData = PreferenceKeystore.encryptWithPassword(password, serialized, salt) // Combine salt and encrypted data val dataToSave = salt + encryptedData diff --git a/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt b/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt index 71fdfedb..1acc6b7e 100644 --- a/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt +++ b/app/src/main/java/ani/dantotsu/connections/mal/MAL.kt @@ -48,7 +48,7 @@ object MAL { private suspend fun refreshToken(): ResponseToken? { return tryWithSuspend { - val token = PrefManager.getNullableVal(PrefName.MALToken, null) + val token = PrefManager.getNullableVal(PrefName.MALToken, null) ?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed)) val res = client.post( "https://myanimelist.net/v1/oauth2/token", @@ -66,7 +66,7 @@ object MAL { suspend fun getSavedToken(context: FragmentActivity): Boolean { return tryWithSuspend(false) { - var res: ResponseToken = PrefManager.getNullableVal(PrefName.MALToken, null) + var res: ResponseToken = PrefManager.getNullableVal(PrefName.MALToken, null) ?: return@tryWithSuspend false if (System.currentTimeMillis() > res.expiresIn) res = refreshToken() diff --git a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt index df64eb2f..3b4acc1a 100644 --- a/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt +++ b/app/src/main/java/ani/dantotsu/settings/SettingsActivity.kt @@ -21,6 +21,7 @@ import androidx.annotation.OptIn import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.offline.DownloadService @@ -41,6 +42,7 @@ import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.internal.Location import ani.dantotsu.settings.saving.internal.PreferenceKeystore +import ani.dantotsu.settings.saving.internal.PreferencePackager import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime @@ -84,52 +86,34 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene initActivity(this) - var selectedImpExp = "" val openDocumentLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> if (uri != null) { try { val jsonString = contentResolver.openInputStream(uri)?.readBytes() ?: throw Exception("Error reading file") - val location: Location = - Location.entries.find { it.name.lowercase() == selectedImpExp.lowercase() } - ?: return@registerForActivityResult - val decryptedJson = if (location == Location.Protected) { - val password = tempPassword ?: return@registerForActivityResult - tempPassword = null - val salt = jsonString.copyOfRange(0, 16) - val encrypted = jsonString.copyOfRange(16, jsonString.size) - PreferenceKeystore.decryptWithPassword( - password, - encrypted, - salt - ) - } else { - jsonString.toString(Charsets.UTF_8) - } - - val gson = Gson() - val type = object : TypeToken>>() {}.type - val rawMap: Map> = gson.fromJson(decryptedJson, type) - - val deserializedMap = mutableMapOf() - - rawMap.forEach { (key, typeValueMap) -> - val typeName = typeValueMap["type"] as? String - val value = typeValueMap["value"] - - deserializedMap[key] = when (typeName) { //wierdly null sometimes so cast to string - "kotlin.Int" -> (value as? Double)?.toInt() - "kotlin.String" -> value.toString() - "kotlin.Boolean" -> value as? Boolean - "kotlin.Float" -> value.toString().toFloatOrNull() - "kotlin.Long" -> (value as? Double)?.toLong() - "java.util.HashSet" -> value as? ArrayList<*> - else -> null + val name = DocumentFile.fromSingleUri(this, uri)?.name ?: "settings" + //.sani is encrypted, .ani is not + if (name.endsWith(".sani")) { + passwordAlertDialog(false) { password -> + if (password != null) { + val salt = jsonString.copyOfRange(0, 16) + val encrypted = jsonString.copyOfRange(16, jsonString.size) + val decryptedJson = PreferenceKeystore.decryptWithPassword( + password, + encrypted, + salt + ) + if(PreferencePackager.unpack(decryptedJson)) + restartApp() + } else { + toast("Password cannot be empty") + } } + } else { + val decryptedJson = jsonString.toString(Charsets.UTF_8) + if(PreferencePackager.unpack(decryptedJson)) + restartApp() } - - if(PrefManager.importAllPrefs(deserializedMap, location)) - restartApp() } catch (e: Exception) { e.printStackTrace() toast("Error importing settings") @@ -280,39 +264,31 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene } binding.importExportSettings.setOnClickListener { - var i = 0 - selectedImpExp = Location.entries[i].name + val selectedArray = mutableListOf(false) val filteredLocations = Location.entries.filter { it.exportable } + selectedArray.addAll(List(filteredLocations.size - 1) { false }) val dialog = AlertDialog.Builder(this, R.style.MyPopup) .setTitle("Import/Export Settings") - .setSingleChoiceItems( filteredLocations.map { it.name }.toTypedArray(), i) { dialog, which -> - selectedImpExp = filteredLocations[which].name - i = which + .setMultiChoiceItems( filteredLocations.map { it.name }.toTypedArray(), selectedArray.toBooleanArray()) { _, which, isChecked -> + selectedArray[which] = isChecked } .setPositiveButton("Import...") { dialog, _ -> - if (filteredLocations[i] == Location.Protected) { - passwordAlertDialog(false) { password -> - if (password != null) { - tempPassword = password - openDocumentLauncher.launch(arrayOf("*/*")) - } else { - toast("Password cannot be empty") - } - } - } else { - openDocumentLauncher.launch(arrayOf("*/*")) - } + openDocumentLauncher.launch(arrayOf("*/*")) dialog.dismiss() } .setNegativeButton("Export...") { dialog, _ -> - if (i < 0) return@setNegativeButton + if (!selectedArray.contains(true)) { + toast("No location selected") + return@setNegativeButton + } dialog.dismiss() - if (filteredLocations[i] == Location.Protected) { + val selected = filteredLocations.filterIndexed { index, _ -> selectedArray[index] } + if (selected.contains(Location.Protected)) { passwordAlertDialog(true) { password -> if (password != null) { savePrefsToDownloads( - filteredLocations[i].name, - PrefManager.exportAllPrefs(filteredLocations[i]), + "DantotsuSettings", + PrefManager.exportAllPrefs(selected), this@SettingsActivity, password ) @@ -322,8 +298,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene } } else { savePrefsToDownloads( - filteredLocations[i].name, - PrefManager.exportAllPrefs(filteredLocations[i]), + "DantotsuSettings", + PrefManager.exportAllPrefs(selected), this@SettingsActivity, null ) diff --git a/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt b/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt index dc03a113..da390441 100644 --- a/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt +++ b/app/src/main/java/ani/dantotsu/settings/saving/PrefManager.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.util.Base64 import ani.dantotsu.settings.saving.internal.Compat import ani.dantotsu.settings.saving.internal.Location +import ani.dantotsu.settings.saving.internal.PreferencePackager import ani.dantotsu.snackString import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -237,18 +238,10 @@ object PrefManager { fun getAnimeDownloadPreferences(): SharedPreferences = animeDownloadsPreferences!! //needs to be used externally - fun exportAllPrefs(prefLocation: Location): Map{ - val pref = getPrefLocation(prefLocation) - val typedMap = mutableMapOf() - pref.all.forEach { (key, value) -> - val typeValueMap = mapOf( - "type" to value?.javaClass?.kotlin?.qualifiedName, - "value" to value - ) - typedMap[key] = typeValueMap - } - - return typedMap + fun exportAllPrefs(prefLocation: List): String { + return PreferencePackager.pack( + prefLocation.associateWith { getPrefLocation(it) } + ) } @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/java/ani/dantotsu/settings/saving/internal/PreferencePackager.kt b/app/src/main/java/ani/dantotsu/settings/saving/internal/PreferencePackager.kt new file mode 100644 index 00000000..cad3a22e --- /dev/null +++ b/app/src/main/java/ani/dantotsu/settings/saving/internal/PreferencePackager.kt @@ -0,0 +1,82 @@ +package ani.dantotsu.settings.saving.internal + +import android.content.SharedPreferences +import ani.dantotsu.settings.saving.PrefManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +class PreferencePackager { + //map one or more preference maps for import/export + + companion object { + + fun pack(map: Map): String { + val prefsMap = packagePreferences(map) + val gson = Gson() + return gson.toJson(prefsMap) + } + fun unpack(decryptedJson: String): Boolean { + val gson = Gson() + val type = object : TypeToken>>>() {}.type //oh god... + val rawPrefsMap: Map>> = gson.fromJson(decryptedJson, type) + + + val deserializedMap = mutableMapOf>() + + rawPrefsMap.forEach { (prefName, prefValueMap) -> + val innerMap = mutableMapOf() + + prefValueMap.forEach { (key, typeValueMap) -> + + val typeName = typeValueMap["type"] as? String + val value = typeValueMap["value"] + + innerMap[key] = + when (typeName) { //wierdly null sometimes so cast to string + "kotlin.Int" -> (value as? Double)?.toInt() + "kotlin.String" -> value.toString() + "kotlin.Boolean" -> value as? Boolean + "kotlin.Float" -> value.toString().toFloatOrNull() + "kotlin.Long" -> (value as? Double)?.toLong() + "java.util.HashSet" -> value as? ArrayList<*> + else -> null + } + } + deserializedMap[prefName] = innerMap + } + return unpackagePreferences(deserializedMap) + } + + private fun packagePreferences(map: Map): Map> { + val result = mutableMapOf>() + for ((location, preferences) in map) { + val prefMap = mutableMapOf() + preferences.all.forEach { (key, value) -> + val typeValueMap = mapOf( + "type" to value?.javaClass?.kotlin?.qualifiedName, + "value" to value + ) + prefMap[key] = typeValueMap + } + result[location.name] = prefMap + } + return result + } + + + private fun unpackagePreferences(map: Map>): Boolean { + var failed = false + map.forEach { (location, prefMap) -> + val locationEnum = locationFromString(location) + if (!PrefManager.importAllPrefs(prefMap, locationEnum)) + failed = true + } + return failed + } + + private fun locationFromString(location: String): Location { + val loc = Location.entries.find { it.name == location } + return loc ?: throw IllegalArgumentException("Location not found") + } + } +} \ No newline at end of file