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/schemas/app.revanced.manager.data.room.AppDatabase/1.json b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json index 0fb6425d..da13c490 100644 --- a/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json +++ b/app/schemas/app.revanced.manager.data.room.AppDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "802fa2fda94b930bf0ebb85d195f1022", + "identityHash": "c0c780e55e10c9b095c004733c846b67", "entities": [ { "tableName": "patch_bundles", @@ -231,7 +231,7 @@ }, { "tableName": "applied_patch", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "packageName", @@ -285,7 +285,7 @@ }, { "table": "patch_bundles", - "onDelete": "NO ACTION", + "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "bundle" @@ -407,7 +407,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '802fa2fda94b930bf0ebb85d195f1022')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c0c780e55e10c9b095c004733c846b67')" ] } } \ No newline at end of file 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/apps/installed/AppliedPatch.kt b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt index 6feb04ed..d2a498a3 100644 --- a/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt +++ b/app/src/main/java/app/revanced/manager/data/room/apps/installed/AppliedPatch.kt @@ -22,7 +22,8 @@ import kotlinx.parcelize.Parcelize ForeignKey( PatchBundleEntity::class, parentColumns = ["uid"], - childColumns = ["bundle"] + childColumns = ["bundle"], + onDelete = ForeignKey.CASCADE ) ], indices = [Index(value = ["bundle"], unique = false)] 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/manager/KeystoreManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt index bba8035f..c1c4700d 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/KeystoreManager.kt @@ -49,15 +49,17 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) { ) ) ) - keystorePath.outputStream().use { - ks.store(it, null) + withContext(Dispatchers.IO) { + keystorePath.outputStream().use { + ks.store(it, null) + } } updatePrefs(DEFAULT, DEFAULT) } suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean { - val keystoreData = keystore.readBytes() + val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() } try { val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null) diff --git a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt index 4f3ec2f5..f40d6c0b 100644 --- a/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt +++ b/app/src/main/java/app/revanced/manager/domain/repository/PatchBundleRepository.kt @@ -137,7 +137,7 @@ class PatchBundleRepository( private fun addBundle(patchBundle: PatchBundleSource) = _sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } } - suspend fun createLocal(patches: InputStream, integrations: InputStream?) { + suspend fun createLocal(patches: InputStream, integrations: InputStream?) = withContext(Dispatchers.Default) { val uid = persistenceRepo.create("", SourceInfo.Local).uid val bundle = LocalPatchBundle("", uid, directoryOf(uid)) @@ -145,7 +145,7 @@ class PatchBundleRepository( addBundle(bundle) } - suspend fun createRemote(url: String, autoUpdate: Boolean) { + suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) { val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate) addBundle(entity.load()) } 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