better setting export

This commit is contained in:
rebelonion 2024-02-03 00:43:20 -06:00
parent 54b53dbe56
commit aa8d41eecf
5 changed files with 135 additions and 88 deletions

View file

@ -590,13 +590,13 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
) )
} }
fun savePrefsToDownloads(title: String, map: Map<String, *>, context: Context, password: CharArray? = null) { fun savePrefsToDownloads(title: String, serialized: String, context: Context, password: CharArray? = null) {
FileProvider.getUriForFile( FileProvider.getUriForFile(
context, context,
"$APPLICATION_ID.provider", "$APPLICATION_ID.provider",
if (password != null) { if (password != null) {
savePrefs( savePrefs(
map, serialized,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title, title,
context, context,
@ -604,7 +604,7 @@ fun savePrefsToDownloads(title: String, map: Map<String, *>, context: Context, p
) ?: return ) ?: return
} else { } else {
savePrefs( savePrefs(
map, serialized,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title, title,
context context
@ -613,7 +613,7 @@ fun savePrefsToDownloads(title: String, map: Map<String, *>, context: Context, p
) )
} }
fun savePrefs(map: Map<String, *>, 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 file = File(path, "$title.ani")
var counter = 1 var counter = 1
while (file.exists()) { while (file.exists()) {
@ -622,9 +622,7 @@ fun savePrefs(map: Map<String, *>, path: String, title: String, context: Context
} }
return try { return try {
val gson = Gson() file.writeText(serialized)
val json = gson.toJson(map)
file.writeText(json)
scanFile(file.absolutePath, context) scanFile(file.absolutePath, context)
toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath))) toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath)))
file file
@ -634,20 +632,18 @@ fun savePrefs(map: Map<String, *>, path: String, title: String, context: Context
} }
} }
fun savePrefs(map: Map<String, *>, 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 file = File(path, "$title.ani")
var counter = 1 var counter = 1
while (file.exists()) { while (file.exists()) {
file = File(path, "${title}_${counter}.ani") file = File(path, "${title}_${counter}.sani")
counter++ counter++
} }
val salt = generateSalt() val salt = generateSalt()
return try { return try {
val gson = Gson() val encryptedData = PreferenceKeystore.encryptWithPassword(password, serialized, salt)
val json = gson.toJson(map)
val encryptedData = PreferenceKeystore.encryptWithPassword(password, json, salt)
// Combine salt and encrypted data // Combine salt and encrypted data
val dataToSave = salt + encryptedData val dataToSave = salt + encryptedData

View file

@ -48,7 +48,7 @@ object MAL {
private suspend fun refreshToken(): ResponseToken? { private suspend fun refreshToken(): ResponseToken? {
return tryWithSuspend { return tryWithSuspend {
val token = PrefManager.getNullableVal<ResponseToken?>(PrefName.MALToken, null) val token = PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed)) ?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
val res = client.post( val res = client.post(
"https://myanimelist.net/v1/oauth2/token", "https://myanimelist.net/v1/oauth2/token",
@ -66,7 +66,7 @@ object MAL {
suspend fun getSavedToken(context: FragmentActivity): Boolean { suspend fun getSavedToken(context: FragmentActivity): Boolean {
return tryWithSuspend(false) { return tryWithSuspend(false) {
var res: ResponseToken = PrefManager.getNullableVal<ResponseToken?>(PrefName.MALToken, null) var res: ResponseToken = PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
?: return@tryWithSuspend false ?: return@tryWithSuspend false
if (System.currentTimeMillis() > res.expiresIn) if (System.currentTimeMillis() > res.expiresIn)
res = refreshToken() res = refreshToken()

View file

@ -21,6 +21,7 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService 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.PrefManager
import ani.dantotsu.settings.saving.internal.Location import ani.dantotsu.settings.saving.internal.Location
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime import ani.dantotsu.subcriptions.Subscription.Companion.defaultTime
@ -84,52 +86,34 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
initActivity(this) initActivity(this)
var selectedImpExp = ""
val openDocumentLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> val openDocumentLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) { if (uri != null) {
try { try {
val jsonString = contentResolver.openInputStream(uri)?.readBytes() val jsonString = contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file") ?: throw Exception("Error reading file")
val location: Location = val name = DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
Location.entries.find { it.name.lowercase() == selectedImpExp.lowercase() } //.sani is encrypted, .ani is not
?: return@registerForActivityResult if (name.endsWith(".sani")) {
val decryptedJson = if (location == Location.Protected) { passwordAlertDialog(false) { password ->
val password = tempPassword ?: return@registerForActivityResult if (password != null) {
tempPassword = null val salt = jsonString.copyOfRange(0, 16)
val salt = jsonString.copyOfRange(0, 16) val encrypted = jsonString.copyOfRange(16, jsonString.size)
val encrypted = jsonString.copyOfRange(16, jsonString.size) val decryptedJson = PreferenceKeystore.decryptWithPassword(
PreferenceKeystore.decryptWithPassword( password,
password, encrypted,
encrypted, salt
salt )
) if(PreferencePackager.unpack(decryptedJson))
} else { restartApp()
jsonString.toString(Charsets.UTF_8) } else {
} toast("Password cannot be empty")
}
val gson = Gson()
val type = object : TypeToken<Map<String, Map<String, Any>>>() {}.type
val rawMap: Map<String, Map<String, Any>> = gson.fromJson(decryptedJson, type)
val deserializedMap = mutableMapOf<String, Any?>()
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
} }
} else {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if(PreferencePackager.unpack(decryptedJson))
restartApp()
} }
if(PrefManager.importAllPrefs(deserializedMap, location))
restartApp()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
toast("Error importing settings") toast("Error importing settings")
@ -280,39 +264,31 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} }
binding.importExportSettings.setOnClickListener { binding.importExportSettings.setOnClickListener {
var i = 0 val selectedArray = mutableListOf(false)
selectedImpExp = Location.entries[i].name
val filteredLocations = Location.entries.filter { it.exportable } val filteredLocations = Location.entries.filter { it.exportable }
selectedArray.addAll(List(filteredLocations.size - 1) { false })
val dialog = AlertDialog.Builder(this, R.style.MyPopup) val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Import/Export Settings") .setTitle("Import/Export Settings")
.setSingleChoiceItems( filteredLocations.map { it.name }.toTypedArray(), i) { dialog, which -> .setMultiChoiceItems( filteredLocations.map { it.name }.toTypedArray(), selectedArray.toBooleanArray()) { _, which, isChecked ->
selectedImpExp = filteredLocations[which].name selectedArray[which] = isChecked
i = which
} }
.setPositiveButton("Import...") { dialog, _ -> .setPositiveButton("Import...") { dialog, _ ->
if (filteredLocations[i] == Location.Protected) { openDocumentLauncher.launch(arrayOf("*/*"))
passwordAlertDialog(false) { password ->
if (password != null) {
tempPassword = password
openDocumentLauncher.launch(arrayOf("*/*"))
} else {
toast("Password cannot be empty")
}
}
} else {
openDocumentLauncher.launch(arrayOf("*/*"))
}
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton("Export...") { dialog, _ -> .setNegativeButton("Export...") { dialog, _ ->
if (i < 0) return@setNegativeButton if (!selectedArray.contains(true)) {
toast("No location selected")
return@setNegativeButton
}
dialog.dismiss() dialog.dismiss()
if (filteredLocations[i] == Location.Protected) { val selected = filteredLocations.filterIndexed { index, _ -> selectedArray[index] }
if (selected.contains(Location.Protected)) {
passwordAlertDialog(true) { password -> passwordAlertDialog(true) { password ->
if (password != null) { if (password != null) {
savePrefsToDownloads( savePrefsToDownloads(
filteredLocations[i].name, "DantotsuSettings",
PrefManager.exportAllPrefs(filteredLocations[i]), PrefManager.exportAllPrefs(selected),
this@SettingsActivity, this@SettingsActivity,
password password
) )
@ -322,8 +298,8 @@ class SettingsActivity : AppCompatActivity(), SimpleDialog.OnDialogResultListene
} }
} else { } else {
savePrefsToDownloads( savePrefsToDownloads(
filteredLocations[i].name, "DantotsuSettings",
PrefManager.exportAllPrefs(filteredLocations[i]), PrefManager.exportAllPrefs(selected),
this@SettingsActivity, this@SettingsActivity,
null null
) )

View file

@ -5,6 +5,7 @@ import android.content.SharedPreferences
import android.util.Base64 import android.util.Base64
import ani.dantotsu.settings.saving.internal.Compat import ani.dantotsu.settings.saving.internal.Compat
import ani.dantotsu.settings.saving.internal.Location import ani.dantotsu.settings.saving.internal.Location
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.snackString import ani.dantotsu.snackString
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -237,18 +238,10 @@ object PrefManager {
fun getAnimeDownloadPreferences(): SharedPreferences = animeDownloadsPreferences!! //needs to be used externally fun getAnimeDownloadPreferences(): SharedPreferences = animeDownloadsPreferences!! //needs to be used externally
fun exportAllPrefs(prefLocation: Location): Map<String, *>{ fun exportAllPrefs(prefLocation: List<Location>): String {
val pref = getPrefLocation(prefLocation) return PreferencePackager.pack(
val typedMap = mutableMapOf<String, Any>() prefLocation.associateWith { getPrefLocation(it) }
pref.all.forEach { (key, value) -> )
val typeValueMap = mapOf(
"type" to value?.javaClass?.kotlin?.qualifiedName,
"value" to value
)
typedMap[key] = typeValueMap
}
return typedMap
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")

View file

@ -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<Location, SharedPreferences>): String {
val prefsMap = packagePreferences(map)
val gson = Gson()
return gson.toJson(prefsMap)
}
fun unpack(decryptedJson: String): Boolean {
val gson = Gson()
val type = object : TypeToken<Map<String, Map<String, Map<String, Any>>>>() {}.type //oh god...
val rawPrefsMap: Map<String, Map<String, Map<String, Any>>> = gson.fromJson(decryptedJson, type)
val deserializedMap = mutableMapOf<String, Map<String, Any?>>()
rawPrefsMap.forEach { (prefName, prefValueMap) ->
val innerMap = mutableMapOf<String, Any?>()
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<Location, SharedPreferences>): Map<String, Map<String, *>> {
val result = mutableMapOf<String, Map<String, *>>()
for ((location, preferences) in map) {
val prefMap = mutableMapOf<String, Any>()
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<String, Map<String, *>>): 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")
}
}
}