improve json serialization

This commit is contained in:
Josia Pietsch 2024-12-29 01:21:38 +01:00
parent 970c160f4a
commit 4ddb893d41
Signed by: jrpie
GPG key ID: E70B571D66986A2D
10 changed files with 169 additions and 138 deletions

View file

@ -13,6 +13,8 @@ import de.jrpie.android.launcher.actions.TorchManager
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.migratePreferencesToNewVersion
import de.jrpie.android.launcher.preferences.resetPreferences
class Application : android.app.Application() {
val apps = MutableLiveData<List<DetailedAppInfo>>()
@ -85,6 +87,19 @@ class Application : android.app.Application() {
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
LauncherPreferences.init(preferences, this.resources)
// Try to restore old preferences
migratePreferencesToNewVersion(this)
// First time opening the app: set defaults and start tutorial
if (!LauncherPreferences.internal().started()) {
resetPreferences(this)
LauncherPreferences.internal().started(true)
openTutorial(this)
}
LauncherPreferences.getSharedPreferences()
.registerOnSharedPreferenceChangeListener(listener)

View file

@ -75,7 +75,9 @@ fun openInBrowser(url: String, context: Context) {
}
fun openTutorial(context: Context) {
context.startActivity(Intent(context, TutorialActivity::class.java))
context.startActivity(Intent(context, TutorialActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
})
}

View file

@ -19,10 +19,13 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
@Serializable(with = LauncherActionSerializer::class)
@SerialName("action:launcher")
@ -248,30 +251,27 @@ fun openAppsList(context: Context, favorite: Boolean = false, hidden: Boolean =
context.startActivity(intent)
}
/**
* LauncherAction can't be serialized directly, since it needs a type annotation.
* Thus this hack is needed.
/* A custom serializer is required to store type information,
see https://github.com/Kotlin/kotlinx.serialization/issues/1486
*/
@Serializable
private class LauncherActionWrapper(val id: String)
private class LauncherActionSerializer() : KSerializer<LauncherAction> {
private class LauncherActionSerializer : KSerializer<LauncherAction> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor(
"action:launcher",
) {
element("id", LauncherActionWrapper.serializer().descriptor)
element("value", String.serializer().descriptor)
}
override fun deserialize(decoder: Decoder): LauncherAction {
val wrapper = decoder.decodeSerializableValue(LauncherActionWrapper.serializer())
return LauncherAction.byId(wrapper.id) ?: throw SerializationException()
val s = decoder.decodeStructure(descriptor) {
decodeElementIndex(descriptor)
decodeSerializableElement(descriptor, 0, String.serializer())
}
return LauncherAction.byId(s) ?: throw SerializationException()
}
override fun serialize(encoder: Encoder, value: LauncherAction) {
encoder.encodeSerializableValue(
LauncherActionWrapper.serializer(),
LauncherActionWrapper(value.id)
)
encoder.encodeStructure(descriptor) {
encodeSerializableElement(descriptor, 0, String.serializer(), value.id)
}
}
}

View file

@ -1,23 +1,18 @@
package de.jrpie.android.launcher.preferences;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import de.jrpie.android.launcher.R;
import de.jrpie.android.launcher.actions.lock.LockMethod;
import de.jrpie.android.launcher.apps.AppInfo;
import de.jrpie.android.launcher.preferences.serialization.SetAppInfoPreferenceSerializer;
import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer;
import de.jrpie.android.launcher.preferences.theme.Background;
import de.jrpie.android.launcher.preferences.theme.ColorTheme;
import de.jrpie.android.launcher.preferences.theme.Font;
import eu.jonahbauer.android.preference.annotations.Preference;
import eu.jonahbauer.android.preference.annotations.PreferenceGroup;
import eu.jonahbauer.android.preference.annotations.Preferences;
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException;
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer;
@Preferences(
name = "de.jrpie.android.launcher.preferences.LauncherPreferences",
@ -30,9 +25,9 @@ import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSeriali
@Preference(name = "version_code", type = int.class, defaultValue = "-1"),
}),
@PreferenceGroup(name = "apps", prefix = "settings_apps_", suffix = "_key", value = {
@Preference(name = "favorites", type = Set.class, serializer = LauncherPreferences$Config.AppInfoSetSerializer.class),
@Preference(name = "hidden", type = Set.class, serializer = LauncherPreferences$Config.AppInfoSetSerializer.class),
@Preference(name = "custom_names", type = HashMap.class, serializer = LauncherPreferences$Config.MapAppInfoStringSerializer.class),
@Preference(name = "favorites", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
@Preference(name = "hidden", type = Set.class, serializer = SetAppInfoPreferenceSerializer.class),
@Preference(name = "custom_names", type = HashMap.class, serializer = MapAppInfoStringPreferenceSerializer.class),
@Preference(name = "hide_bound_apps", type = boolean.class, defaultValue = "false"),
}),
@PreferenceGroup(name = "list", prefix = "settings_list_", suffix = "_key", value = {
@ -78,74 +73,4 @@ import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSeriali
@Preference(name = "lock_method", type = LockMethod.class, defaultValue = "DEVICE_ADMIN"),
}),
})
public final class LauncherPreferences$Config {
public static class AppInfoSetSerializer implements PreferenceSerializer<Set<AppInfo>, Set<String>> {
@Override
public Set<String> serialize(Set<AppInfo> value) throws PreferenceSerializationException {
if (value == null) return null;
var serialized = new HashSet<String>(value.size());
for (var app : value) {
serialized.add(app.serialize());
}
return serialized;
}
@Override
public Set<AppInfo> deserialize(Set<String> value) throws PreferenceSerializationException {
if (value == null) return null;
var deserialized = new HashSet<AppInfo>(value.size());
for (var s : value) {
deserialized.add(AppInfo.Companion.deserialize(s));
}
return deserialized;
}
}
// TODO migrate to version 2
public static class MapAppInfoStringSerializer implements PreferenceSerializer<HashMap<AppInfo, String>, Set<String>> {
@Override
public Set<String> serialize(HashMap<AppInfo, String> value) throws PreferenceSerializationException {
if (value == null) return null;
var serialized = new HashSet<String>(value.size());
for (var entry : value.entrySet()) {
JSONObject obj = new JSONObject();
try {
obj.put("key", entry.getKey().serialize());
obj.put("value", entry.getValue());
serialized.add(obj.toString());
} catch (JSONException ignored) {
}
}
return serialized;
}
@Override
public HashMap<AppInfo, String> deserialize(Set<String> value) throws PreferenceSerializationException {
if (value == null) return null;
var deserialized = new HashMap<AppInfo, String>();
for (var entry : value) {
try {
JSONObject obj = new JSONObject(entry);
AppInfo info = AppInfo.Companion.deserialize(obj.getString("key"));
String s = obj.getString("value");
deserialized.put(info, s);
} catch (JSONException ignored) {
}
}
return deserialized;
}
}
}
public final class LauncherPreferences$Config {}

View file

@ -6,8 +6,8 @@ import de.jrpie.android.launcher.BuildConfig
import de.jrpie.android.launcher.actions.Action
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.DetailedAppInfo
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersion1
import de.jrpie.android.launcher.preferences.legacy.migratePreferencesFromVersionUnknown
import de.jrpie.android.launcher.ui.HomeActivity
/* Current version of the structure of preferences.
@ -19,31 +19,40 @@ const val UNKNOWN_PREFERENCE_VERSION = -1
private const val TAG = "Launcher - Preferences"
/*
* Tries to detect preferences written by older versions of the app
* and migrate them to the current format.
*/
fun migratePreferencesToNewVersion(context: Context) {
when (LauncherPreferences.internal().versionCode()) {
// Check versions, make sure transitions between versions go well
PREFERENCE_VERSION -> { /* the version installed and used previously are the same */
}
try {
when (LauncherPreferences.internal().versionCode()) {
// Check versions, make sure transitions between versions go well
PREFERENCE_VERSION -> { /* the version installed and used previously are the same */
}
UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */
migratePreferencesFromVersionUnknown(context)
UNKNOWN_PREFERENCE_VERSION -> { /* still using the old preferences file */
migratePreferencesFromVersionUnknown(context)
Log.i(TAG, "migration of preferences complete.")
}
1 -> {
migratePreferencesFromVersion1()
Log.i(TAG, "migration of preferences complete.")
}
Log.i(TAG, "migration of preferences complete.")
}
else -> {
Log.w(
TAG,
"Shared preferences were written by a newer version of the app (${
LauncherPreferences.internal().versionCode()
})!"
)
1 -> {
migratePreferencesFromVersion1()
Log.i(TAG, "migration of preferences complete.")
}
else -> {
Log.w(
TAG,
"Shared preferences were written by a newer version of the app (${
LauncherPreferences.internal().versionCode()
})!"
)
}
}
} catch (e: Exception) {
Log.e(TAG, "Unable to restore preferences:\n${e.stackTrace}")
resetPreferences(context)
}
}

View file

@ -7,8 +7,12 @@ import de.jrpie.android.launcher.actions.LauncherAction
import de.jrpie.android.launcher.apps.AppInfo
import de.jrpie.android.launcher.apps.AppInfo.Companion.INVALID_USER
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import de.jrpie.android.launcher.preferences.serialization.MapAppInfoStringPreferenceSerializer
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.json.JSONException
import org.json.JSONObject
val oldLauncherActionIds: Map<String, LauncherAction> =
mapOf(
@ -44,7 +48,7 @@ private fun AppInfo.Companion.legacyDeserialize(serialized: String): AppInfo {
* @param id
* @param user a user id, ignored if the action is a [LauncherAction].
*/
private fun Action.Companion.fromId(id: String, user: Int?): Action? {
private fun Action.Companion.legacyFromId(id: String, user: Int?): Action? {
if (id.isEmpty()) {
return null
}
@ -68,7 +72,25 @@ private fun Action.Companion.legacyFromPreference(id: String): Action? {
)
u = if (u == AppInfo.INVALID_USER) null else u
return Action.fromId(actionId, u)
return Action.legacyFromId(actionId, u)
}
private fun migrateAppInfoStringMap(key: String) {
val preferences = LauncherPreferences.getSharedPreferences()
MapAppInfoStringPreferenceSerializer().serialize(
preferences.getStringSet(key, setOf())?.mapNotNull { entry ->
try {
val obj = JSONObject(entry);
val info = AppInfo.legacyDeserialize(obj.getString("key"))
val value = obj.getString("value");
Pair(info, value)
} catch (_: JSONException) {
null
}
}?.toMap(HashMap())
)?.let {
preferences.edit().putStringSet(key, it as Set<String>).apply()
}
}
private fun migrateAppInfoSet(key: String) {
@ -82,13 +104,24 @@ private fun migrateAppInfoSet(key: String) {
private fun migrateAction(key: String) {
Action.legacyFromPreference(key)?.let { action ->
LauncherPreferences.getSharedPreferences().edit()
.putString(key, Json.encodeToString(action)).apply()
.putString(key, Json.encodeToString(action))
.remove("$key.app")
.remove("$key.user")
.apply()
}
}
/**
* Migrate preferences from version 1 (used until version j-0.0.18) to the current format
* (see [PREFERENCE_VERSION])
*/
fun migratePreferencesFromVersion1() {
assert(PREFERENCE_VERSION == 2)
assert(LauncherPreferences.internal().versionCode() == 1)
Gesture.entries.forEach { g -> migrateAction(g.id) }
migrateAppInfoSet(LauncherPreferences.apps().keys().hidden())
migrateAppInfoSet(LauncherPreferences.apps().keys().favorites())
migrateAppInfoStringMap(LauncherPreferences.apps().keys().customNames())
LauncherPreferences.internal().versionCode(2)
}

View file

@ -4,9 +4,12 @@ import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import de.jrpie.android.launcher.preferences.LauncherPreferences
import de.jrpie.android.launcher.preferences.PREFERENCE_VERSION
import de.jrpie.android.launcher.preferences.theme.Background
import de.jrpie.android.launcher.preferences.theme.ColorTheme
private fun migrateStringPreference(
oldPrefs: SharedPreferences,
newPreferences: SharedPreferences.Editor,
@ -42,7 +45,13 @@ private fun migrateBooleanPreference(
private const val TAG = "Preferences ? -> 1"
/**
* Try to migrate from a very old preference version, where no version number was stored
* and a different file was used.
*/
fun migratePreferencesFromVersionUnknown(context: Context) {
assert(PREFERENCE_VERSION == 2)
Log.i(
TAG,
"Unknown preference version, trying to restore preferences from old version."

View file

@ -0,0 +1,49 @@
@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
package de.jrpie.android.launcher.preferences.serialization
import de.jrpie.android.launcher.apps.AppInfo
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializationException
import eu.jonahbauer.android.preference.annotations.serializer.PreferenceSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
// Serializers for [LauncherPreference$Config]
@Suppress("UNCHECKED_CAST")
class SetAppInfoPreferenceSerializer :
PreferenceSerializer<java.util.Set<AppInfo>?, java.util.Set<java.lang.String>?> {
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.Set<AppInfo>?): java.util.Set<java.lang.String>? {
return value?.map(AppInfo::serialize)?.toHashSet() as java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.Set<AppInfo>? {
return value?.map (java.lang.String::toString)?.map(AppInfo::deserialize)?.toHashSet() as? java.util.Set<AppInfo>
}
}
@Suppress("UNCHECKED_CAST")
class MapAppInfoStringPreferenceSerializer :
PreferenceSerializer<java.util.HashMap<AppInfo, String>?, java.util.Set<java.lang.String>?> {
@Serializable()
private class MapEntry(val key: AppInfo, val value: String)
@Throws(PreferenceSerializationException::class)
override fun serialize(value: java.util.HashMap<AppInfo, String>?): java.util.Set<java.lang.String>? {
return value?.map { (key, value) ->
Json.encodeToString(MapEntry(key, value))
}?.toHashSet() as? java.util.Set<java.lang.String>
}
@Throws(PreferenceSerializationException::class)
override fun deserialize(value: java.util.Set<java.lang.String>?): java.util.HashMap<AppInfo, String>? {
return value?.associateTo(HashMap()) {
val entry = Json.decodeFromString<MapEntry>(it.toString())
Pair(entry.key, entry.value)
}
}
}

View file

@ -76,17 +76,6 @@ class HomeActivity : UIObject, AppCompatActivity(),
super<AppCompatActivity>.onCreate(savedInstanceState)
super<UIObject>.onCreate()
// Try to restore old preferences
migratePreferencesToNewVersion(this)
// First time opening the app: set defaults and start tutorial
if (!LauncherPreferences.internal().started()) {
resetPreferences(this)
LauncherPreferences.internal().started(true)
openTutorial(this)
}
// Initialise layout
binding = HomeBinding.inflate(layoutInflater)
setContentView(binding.root)

View file

@ -5,12 +5,12 @@
<!-- Swipe up - Apps list -->
<string-array name="default_up">
<item>{\"type\": \"action:launcher\", \"id\": \"choose\"}</item> <!-- All Apps -->
<item>{\"type\": \"action:launcher\", \"value\": \"choose\"}</item> <!-- All Apps -->
</string-array>
<!-- Swipe up (left edge) - Favorite Apps -->
<string-array name="default_up_left">
<item>{\"type\": \"action:launcher\", \"id\": \"choose_from_favorites\"}</item>
<item>{\"type\": \"action:launcher\", \"value\": \"choose_from_favorites\"}</item>
</string-array>
<!-- Swipe up (right edge) - Maps -->
@ -106,12 +106,12 @@
<!-- Volume up -->
<string-array name="default_volume_up">
<item>{\"type\": \"action:launcher\", \"id\": \"volume_up\"}</item>
<item>{\"type\": \"action:launcher\", \"value\": \"volume_up\"}</item>
</string-array>
<!-- Volume down -->
<string-array name="default_volume_down">
<item>{\"type\": \"action:launcher\", \"id\": \"volume_down\"}</item>
<item>{\"type\": \"action:launcher\", \"value\": \"volume_down\"}</item>
</string-array>
<!-- Double click - Notes -->
@ -119,7 +119,7 @@
<item>{\"type\": \"action:app\", \"app\": {\"packageName\": \"it.niedermann.owncloud.notes\", \"activityName\": null}}</item>
<item>{\"type\": \"action:app\", \"app\": {\"packageName\": \"com.samsung.android.app.notes\", \"activityName\": null}}</item> <!-- Samsung Notes -->
<item>{\"type\": \"action:app\", \"app\": {\"packageName\": \"com.sec.android.widgetapp.diotek.smemo\", \"activityName\": null}}</item> <!-- S Memo (older devices) -->
<item>{\"type\": \"action:launcher\", \"id\": \"lock_screen\"}</item>
<item>{\"type\": \"action:launcher\", \"value\": \"lock_screen\"}</item>
</string-array>
<!-- Long click - Security -->
@ -128,7 +128,7 @@
<item>{\"type\": \"action:app\", \"app\": {\"packageName\": \"org.fedorahosted.freeotp\", \"activityName\": null}}</item>
<item>{\"type\": \"action:app\", \"app\": {\"packageName\": \"proton.android.pass.fdroid\", \"activityName\": null}}</item> <!-- Proton Pass -->
<item>{\"type\": \"action:app\", \"app\": {\"packageName\": \"com.kunzisoft.keepass.libre\", \"activityName\": null}}</item> <!-- KeePassDX -->
<item>{\"type\": \"action:launcher\", \"id\": \"settings\"}</item> <!-- Launcher Settings -->
<item>{\"type\": \"action:launcher\", \"value\": \"settings\"}</item> <!-- Launcher Settings -->
</string-array>
<!-- Time / Clock -->