Skip to content

Commit

Permalink
feat: remember patch options (ReVanced#1449)
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 committed Oct 31, 2023
1 parent 123ae37 commit 7fe4724
Show file tree
Hide file tree
Showing 16 changed files with 511 additions and 36 deletions.
111 changes: 109 additions & 2 deletions app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "371c7a84b122a2de8b660b35e6e9ce14",
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
"entities": [
{
"tableName": "patch_bundles",
Expand Down Expand Up @@ -295,12 +295,119 @@
]
}
]
},
{
"tableName": "option_groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchBundle",
"columnName": "patch_bundle",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"uid"
]
},
"indices": [
{
"name": "index_option_groups_patch_bundle_package_name",
"unique": true,
"columnNames": [
"patch_bundle",
"package_name"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
}
],
"foreignKeys": [
{
"table": "patch_bundles",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"patch_bundle"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "options",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "group",
"columnName": "group",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "patchName",
"columnName": "patch_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"group",
"patch_name",
"key"
]
},
"indices": [],
"foreignKeys": [
{
"table": "option_groups",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"group"
],
"referencedColumns": [
"uid"
]
}
]
}
],
"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, '371c7a84b122a2de8b660b35e6e9ce14')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '802fa2fda94b930bf0ebb85d195f1022')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ import app.revanced.manager.data.room.selection.SelectedPatch
import app.revanced.manager.data.room.selection.SelectionDao
import app.revanced.manager.data.room.bundles.PatchBundleDao
import app.revanced.manager.data.room.bundles.PatchBundleEntity
import app.revanced.manager.data.room.options.Option
import app.revanced.manager.data.room.options.OptionDao
import app.revanced.manager.data.room.options.OptionGroup
import kotlin.random.Random

@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class], version = 1)
@Database(entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun patchBundleDao(): PatchBundleDao
abstract fun selectionDao(): SelectionDao
abstract fun downloadedAppDao(): DownloadedAppDao
abstract fun installedAppDao(): InstalledAppDao
abstract fun optionDao(): OptionDao

companion object {
fun generateUid() = Random.Default.nextInt()
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/app/revanced/manager/data/room/options/Option.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package app.revanced.manager.data.room.options

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey

@Entity(
tableName = "options",
primaryKeys = ["group", "patch_name", "key"],
foreignKeys = [ForeignKey(
OptionGroup::class,
parentColumns = ["uid"],
childColumns = ["group"],
onDelete = ForeignKey.CASCADE
)]
)
data class Option(
@ColumnInfo(name = "group") val group: Int,
@ColumnInfo(name = "patch_name") val patchName: String,
@ColumnInfo(name = "key") val key: String,
// Encoded as Json.
@ColumnInfo(name = "value") val value: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package app.revanced.manager.data.room.options

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.MapInfo
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow

@Dao
abstract class OptionDao {
@Transaction
@MapInfo(keyColumn = "patch_bundle")
@Query(
"SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" +
" LEFT JOIN options ON uid = options.`group`" +
" WHERE package_name = :packageName"
)
abstract suspend fun getOptions(packageName: String): Map<Int, List<Option>>

@Query("SELECT uid FROM option_groups WHERE patch_bundle = :bundleUid AND package_name = :packageName")
abstract suspend fun getGroupId(bundleUid: Int, packageName: String): Int?

@Query("SELECT package_name FROM option_groups")
abstract fun getPackagesWithOptions(): Flow<List<String>>

@Insert
abstract suspend fun createOptionGroup(group: OptionGroup)

@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
abstract suspend fun clearForPatchBundle(uid: Int)

@Query("DELETE FROM option_groups WHERE package_name = :packageName")
abstract suspend fun clearForPackage(packageName: String)

@Query("DELETE FROM option_groups")
abstract suspend fun reset()

@Insert
protected abstract suspend fun insertOptions(patches: List<Option>)

@Query("DELETE FROM options WHERE `group` = :groupId")
protected abstract suspend fun clearGroup(groupId: Int)

@Transaction
open suspend fun updateOptions(options: Map<Int, List<Option>>) =
options.forEach { (groupId, options) ->
clearGroup(groupId)
insertOptions(options)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package app.revanced.manager.data.room.options

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import app.revanced.manager.data.room.bundles.PatchBundleEntity

@Entity(
tableName = "option_groups",
foreignKeys = [ForeignKey(
PatchBundleEntity::class,
parentColumns = ["uid"],
childColumns = ["patch_bundle"],
onDelete = ForeignKey.CASCADE
)],
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
)
data class OptionGroup(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
@ColumnInfo(name = "package_name") val packageName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ abstract class SelectionDao {

@Transaction
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
selections.map { (selectionUid, patches) ->
selections.forEach { (selectionUid, patches) ->
clearSelection(selectionUid)
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ val repositoryModule = module {
singleOf(::NetworkInfo)
singleOf(::PatchBundlePersistenceRepository)
singleOf(::PatchSelectionRepository)
singleOf(::PatchOptionsRepository)
singleOf(::PatchBundleRepository)
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package app.revanced.manager.domain.repository

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.util.Options
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()

private suspend fun getOrCreateGroup(bundleUid: Int, packageName: String) =
dao.getGroupId(bundleUid, packageName) ?: OptionGroup(
uid = AppDatabase.generateUid(),
patchBundle = bundleUid,
packageName = packageName
).also { dao.createOptionGroup(it) }.uid

suspend fun getOptions(packageName: String): Options {
val options = dao.getOptions(packageName)
// Bundle -> Patches
return buildMap<Int, MutableMap<String, MutableMap<String, Any?>>>(options.size) {
options.forEach { (sourceUid, bundlePatchOptionsList) ->
// Patches -> Patch options
this[sourceUid] = bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, option ->
val patchOptions = bundlePatchOptions.getOrPut(option.patchName, ::mutableMapOf)

patchOptions[option.key] = deserialize(option.value)

bundlePatchOptions
}
}
}
}

suspend fun saveOptions(packageName: String, options: Options) =
dao.updateOptions(options.entries.associate { (sourceUid, bundlePatchOptions) ->
val groupId = getOrCreateGroup(sourceUid, packageName)

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.

Option(groupId, patchName, key, serialized)
}
}
})

fun getPackagesWithSavedOptions() =
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()

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<JsonPrimitive>(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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,11 @@ class PatcherWorker(
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }

// Set all patch options.
args.options.forEach { (bundle, configuredPatchOptions) ->
args.options.forEach { (bundle, bundlePatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach
configuredPatchOptions.forEach { (patchName, options) ->
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
val patchOptions = patches.single { it.name == patchName }.options
options.forEach { (key, value) ->
configuredPatchOptions.forEach { (key, value) ->
patchOptions[key] = value
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,10 +301,7 @@ fun PatchesSelectorScreen(
icon = { Icon(Icons.Outlined.Save, null) },
onClick = {
// TODO: only allow this if all required options have been set.
composableScope.launch {
vm.saveSelection()
onSave(vm.getCustomSelection(), vm.getOptions())
}
onSave(vm.getCustomSelection(), vm.getOptions())
}
)
}
Expand Down
Loading

0 comments on commit 7fe4724

Please sign in to comment.