From 4ddb893d413ee88215a89c60825206478148e9d0 Mon Sep 17 00:00:00 2001 From: Josia Pietsch Date: Sun, 29 Dec 2024 01:21:38 +0100 Subject: [PATCH] improve json serialization --- .../de/jrpie/android/launcher/Application.kt | 15 ++++ .../de/jrpie/android/launcher/Functions.kt | 4 +- .../launcher/actions/LauncherAction.kt | 30 +++---- .../LauncherPreferences$Config.java | 87 ++----------------- .../launcher/preferences/Preferences.kt | 51 ++++++----- .../legacy/{MigrationFrom1.kt => Version1.kt} | 39 ++++++++- ...rationFromUnknown.kt => VersionUnknown.kt} | 9 ++ .../serialization/PreferenceSerializers.kt | 49 +++++++++++ .../jrpie/android/launcher/ui/HomeActivity.kt | 11 --- app/src/main/res/values/defaults.xml | 12 +-- 10 files changed, 169 insertions(+), 138 deletions(-) rename app/src/main/java/de/jrpie/android/launcher/preferences/legacy/{MigrationFrom1.kt => Version1.kt} (69%) rename app/src/main/java/de/jrpie/android/launcher/preferences/legacy/{MigrationFromUnknown.kt => VersionUnknown.kt} (97%) create mode 100644 app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt diff --git a/app/src/main/java/de/jrpie/android/launcher/Application.kt b/app/src/main/java/de/jrpie/android/launcher/Application.kt index e078cc9..e3e5f7c 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Application.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Application.kt @@ -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>() @@ -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) diff --git a/app/src/main/java/de/jrpie/android/launcher/Functions.kt b/app/src/main/java/de/jrpie/android/launcher/Functions.kt index 8f192d0..e8d3851 100644 --- a/app/src/main/java/de/jrpie/android/launcher/Functions.kt +++ b/app/src/main/java/de/jrpie/android/launcher/Functions.kt @@ -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) + }) } diff --git a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt index 1362d09..3c641ff 100644 --- a/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt +++ b/app/src/main/java/de/jrpie/android/launcher/actions/LauncherAction.kt @@ -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 { +private class LauncherActionSerializer : KSerializer { 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) + } } } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java index 866f0a1..5541b0a 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/LauncherPreferences$Config.java @@ -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> { - - @Override - public Set serialize(Set value) throws PreferenceSerializationException { - if (value == null) return null; - - var serialized = new HashSet(value.size()); - for (var app : value) { - serialized.add(app.serialize()); - } - - return serialized; - } - - @Override - public Set deserialize(Set value) throws PreferenceSerializationException { - if (value == null) return null; - - var deserialized = new HashSet(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, Set> { - - @Override - public Set serialize(HashMap value) throws PreferenceSerializationException { - if (value == null) return null; - - var serialized = new HashSet(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 deserialize(Set value) throws PreferenceSerializationException { - if (value == null) return null; - - var deserialized = new HashMap(); - - 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 {} \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt index 21a448f..bb59948 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/Preferences.kt @@ -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) } } diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/MigrationFrom1.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt similarity index 69% rename from app/src/main/java/de/jrpie/android/launcher/preferences/legacy/MigrationFrom1.kt rename to app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt index c6c143d..66723ad 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/MigrationFrom1.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/Version1.kt @@ -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 = 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).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) } \ No newline at end of file diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/MigrationFromUnknown.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt similarity index 97% rename from app/src/main/java/de/jrpie/android/launcher/preferences/legacy/MigrationFromUnknown.kt rename to app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt index 81c754c..1ecbd74 100644 --- a/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/MigrationFromUnknown.kt +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/legacy/VersionUnknown.kt @@ -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." diff --git a/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt new file mode 100644 index 0000000..4a745a2 --- /dev/null +++ b/app/src/main/java/de/jrpie/android/launcher/preferences/serialization/PreferenceSerializers.kt @@ -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?> { + @Throws(PreferenceSerializationException::class) + override fun serialize(value: java.util.Set?): java.util.Set? { + return value?.map(AppInfo::serialize)?.toHashSet() as java.util.Set + } + + @Throws(PreferenceSerializationException::class) + override fun deserialize(value: java.util.Set?): java.util.Set? { + return value?.map (java.lang.String::toString)?.map(AppInfo::deserialize)?.toHashSet() as? java.util.Set + } +} + +@Suppress("UNCHECKED_CAST") +class MapAppInfoStringPreferenceSerializer : + PreferenceSerializer?, java.util.Set?> { + + @Serializable() + private class MapEntry(val key: AppInfo, val value: String) + + @Throws(PreferenceSerializationException::class) + override fun serialize(value: java.util.HashMap?): java.util.Set? { + return value?.map { (key, value) -> + Json.encodeToString(MapEntry(key, value)) + }?.toHashSet() as? java.util.Set + } + + @Throws(PreferenceSerializationException::class) + override fun deserialize(value: java.util.Set?): java.util.HashMap? { + return value?.associateTo(HashMap()) { + val entry = Json.decodeFromString(it.toString()) + Pair(entry.key, entry.value) + } + } +} diff --git a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt index 958d6f9..5633113 100644 --- a/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt +++ b/app/src/main/java/de/jrpie/android/launcher/ui/HomeActivity.kt @@ -76,17 +76,6 @@ class HomeActivity : UIObject, AppCompatActivity(), super.onCreate(savedInstanceState) super.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) diff --git a/app/src/main/res/values/defaults.xml b/app/src/main/res/values/defaults.xml index fb71475..5ed9327 100644 --- a/app/src/main/res/values/defaults.xml +++ b/app/src/main/res/values/defaults.xml @@ -5,12 +5,12 @@ - {\"type\": \"action:launcher\", \"id\": \"choose\"} + {\"type\": \"action:launcher\", \"value\": \"choose\"} - {\"type\": \"action:launcher\", \"id\": \"choose_from_favorites\"} + {\"type\": \"action:launcher\", \"value\": \"choose_from_favorites\"} @@ -106,12 +106,12 @@ - {\"type\": \"action:launcher\", \"id\": \"volume_up\"} + {\"type\": \"action:launcher\", \"value\": \"volume_up\"} - {\"type\": \"action:launcher\", \"id\": \"volume_down\"} + {\"type\": \"action:launcher\", \"value\": \"volume_down\"} @@ -119,7 +119,7 @@ {\"type\": \"action:app\", \"app\": {\"packageName\": \"it.niedermann.owncloud.notes\", \"activityName\": null}} {\"type\": \"action:app\", \"app\": {\"packageName\": \"com.samsung.android.app.notes\", \"activityName\": null}} {\"type\": \"action:app\", \"app\": {\"packageName\": \"com.sec.android.widgetapp.diotek.smemo\", \"activityName\": null}} - {\"type\": \"action:launcher\", \"id\": \"lock_screen\"} + {\"type\": \"action:launcher\", \"value\": \"lock_screen\"} @@ -128,7 +128,7 @@ {\"type\": \"action:app\", \"app\": {\"packageName\": \"org.fedorahosted.freeotp\", \"activityName\": null}} {\"type\": \"action:app\", \"app\": {\"packageName\": \"proton.android.pass.fdroid\", \"activityName\": null}} {\"type\": \"action:app\", \"app\": {\"packageName\": \"com.kunzisoft.keepass.libre\", \"activityName\": null}} - {\"type\": \"action:launcher\", \"id\": \"settings\"} + {\"type\": \"action:launcher\", \"value\": \"settings\"}