better setting export
This commit is contained in:
parent
54b53dbe56
commit
aa8d41eecf
5 changed files with 135 additions and 88 deletions
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue