diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 279a681d..c44dd2da 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -188,6 +188,9 @@ dependencies { // Scrollbars implementation(libs.scrollbars) + // Reorderable lists + implementation(libs.reorderable) + // Compose Icons implementation(libs.compose.icons.fontawesome) } diff --git a/app/src/main/java/app/revanced/manager/data/room/Converters.kt b/app/src/main/java/app/revanced/manager/data/room/Converters.kt index 7de50382..a9437f86 100644 --- a/app/src/main/java/app/revanced/manager/data/room/Converters.kt +++ b/app/src/main/java/app/revanced/manager/data/room/Converters.kt @@ -2,7 +2,7 @@ package app.revanced.manager.data.room import androidx.room.TypeConverter import app.revanced.manager.data.room.bundles.Source -import io.ktor.http.* +import app.revanced.manager.data.room.options.Option.SerializedValue import java.io.File class Converters { @@ -17,4 +17,10 @@ class Converters { @TypeConverter fun fileToString(file: File): String = file.path + + @TypeConverter + fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value) + + @TypeConverter + fun serializedOptionToString(value: SerializedValue) = value.toJsonString() } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt index 3a70a9a5..b59dbd16 100644 --- a/app/src/main/java/app/revanced/manager/data/room/options/Option.kt +++ b/app/src/main/java/app/revanced/manager/data/room/options/Option.kt @@ -3,6 +3,23 @@ package app.revanced.manager.data.room.options import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import app.revanced.manager.patcher.patch.Option +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.add +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.float +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlin.reflect.KClass @Entity( tableName = "options", @@ -19,5 +36,74 @@ data class Option( @ColumnInfo(name = "patch_name") val patchName: String, @ColumnInfo(name = "key") val key: String, // Encoded as Json. - @ColumnInfo(name = "value") val value: String, -) \ No newline at end of file + @ColumnInfo(name = "value") val value: SerializedValue, +) { + @Serializable + data class SerializedValue(val raw: JsonElement) { + fun toJsonString() = json.encodeToString(raw) + fun deserializeFor(option: Option<*>): Any? { + if (raw is JsonNull) return null + + val errorMessage = "Cannot deserialize value as ${option.type}" + try { + if (option.type.endsWith("Array")) { + val elementType = option.type.removeSuffix("Array") + return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) } + } + + return deserializeBasicType(option.type, raw.jsonPrimitive) + } catch (e: IllegalArgumentException) { + throw SerializationException(errorMessage, e) + } catch (e: IllegalStateException) { + throw SerializationException(errorMessage, e) + } catch (e: kotlinx.serialization.SerializationException) { + throw SerializationException(errorMessage, e) + } + } + + companion object { + private val json = Json { + // Patcher does not forbid the use of these values, so we should support them. + allowSpecialFloatingPointValues = true + } + + private fun deserializeBasicType(type: String, value: JsonPrimitive) = when (type) { + "Boolean" -> value.boolean + "Int" -> value.int + "Long" -> value.long + "Float" -> value.float + "String" -> value.content.also { if (!value.isString) throw SerializationException("Expected value to be a string: $value") } + else -> throw SerializationException("Unknown type: $type") + } + + fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value)) + fun fromValue(value: Any?) = SerializedValue(when (value) { + null -> JsonNull + is Number -> JsonPrimitive(value) + is Boolean -> JsonPrimitive(value) + is String -> JsonPrimitive(value) + is List<*> -> buildJsonArray { + var elementClass: KClass? = null + + value.forEach { + when (it) { + null -> throw SerializationException("List elements must not be null") + is Number -> add(it) + is Boolean -> add(it) + is String -> add(it) + else -> throw SerializationException("Unknown element type: ${it::class.simpleName}") + } + + if (elementClass == null) elementClass = it::class + else if (elementClass != it::class) throw SerializationException("List elements must have the same type") + } + } + + else -> throw SerializationException("Unknown type: ${value::class.simpleName}") + }) + } + } + + class SerializationException(message: String, cause: Throwable? = null) : + Exception(message, cause) +} diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt index 43ca3273..9fe5fdc2 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchOptionsRepository.kt @@ -1,18 +1,14 @@ package app.revanced.manager.domain.repository +import android.util.Log import app.revanced.manager.data.room.AppDatabase import app.revanced.manager.data.room.options.Option import app.revanced.manager.data.room.options.OptionGroup +import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.util.Options +import app.revanced.manager.util.tag import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.booleanOrNull -import kotlinx.serialization.json.floatOrNull -import kotlinx.serialization.json.intOrNull class PatchOptionsRepository(db: AppDatabase) { private val dao = db.optionDao() @@ -24,19 +20,37 @@ class PatchOptionsRepository(db: AppDatabase) { packageName = packageName ).also { dao.createOptionGroup(it) }.uid - suspend fun getOptions(packageName: String): Options { + suspend fun getOptions( + packageName: String, + bundlePatches: Map> + ): Options { val options = dao.getOptions(packageName) // Bundle -> Patches return buildMap>>(options.size) { options.forEach { (sourceUid, bundlePatchOptionsList) -> // Patches -> Patch options - this[sourceUid] = bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, option -> - val patchOptions = bundlePatchOptions.getOrPut(option.patchName, ::mutableMapOf) + this[sourceUid] = + bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, dbOption -> + val deserializedPatchOptions = + bundlePatchOptions.getOrPut(dbOption.patchName, ::mutableMapOf) - patchOptions[option.key] = deserialize(option.value) + val option = + bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.key == dbOption.key } + if (option != null) { + try { + deserializedPatchOptions[option.key] = + dbOption.value.deserializeFor(option) + } catch (e: Option.SerializationException) { + Log.w( + tag, + "Option ${dbOption.patchName}:${option.key} could not be deserialized", + e + ) + } + } - bundlePatchOptions - } + bundlePatchOptions + } } } } @@ -47,8 +61,12 @@ class PatchOptionsRepository(db: AppDatabase) { groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) -> patchOptions.mapNotNull { (key, value) -> - val serialized = serialize(value) - ?: return@mapNotNull null // Don't save options that we can't serialize. + val serialized = try { + Option.SerializedValue.fromValue(value) + } catch (e: Option.SerializationException) { + Log.e(tag, "Option $patchName:$key could not be serialized", e) + return@mapNotNull null + } Option(groupId, patchName, key, serialized) } @@ -61,29 +79,4 @@ class PatchOptionsRepository(db: AppDatabase) { suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName) suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid) suspend fun reset() = dao.reset() - - private companion object { - fun deserialize(value: String): Any? { - val primitive = Json.decodeFromString(value) - - return when { - primitive.isString -> primitive.content - primitive is JsonNull -> null - else -> primitive.booleanOrNull ?: primitive.intOrNull ?: primitive.floatOrNull - } - } - - fun serialize(value: Any?): String? { - val primitive = when (value) { - null -> JsonNull - is String -> JsonPrimitive(value) - is Int -> JsonPrimitive(value) - is Float -> JsonPrimitive(value) - is Boolean -> JsonPrimitive(value) - else -> return null - } - - return Json.encodeToString(primitive) - } - } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt index 0754756b..31e707ba 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt @@ -15,7 +15,7 @@ data class PatchInfo( val description: String?, val include: Boolean, val compatiblePackages: ImmutableList?, - val options: ImmutableList