mirror of
https://github.com/revanced/revanced-manager.git
synced 2025-05-03 15:24:25 +02:00
feat: remember patch options (#1449)
This commit is contained in:
parent
123ae37524
commit
7fe4724e10
@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "371c7a84b122a2de8b660b35e6e9ce14",
|
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "patch_bundles",
|
"tableName": "patch_bundles",
|
||||||
@ -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": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.selection.SelectionDao
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
||||||
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
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
|
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)
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
abstract fun patchBundleDao(): PatchBundleDao
|
abstract fun patchBundleDao(): PatchBundleDao
|
||||||
abstract fun selectionDao(): SelectionDao
|
abstract fun selectionDao(): SelectionDao
|
||||||
abstract fun downloadedAppDao(): DownloadedAppDao
|
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||||
abstract fun installedAppDao(): InstalledAppDao
|
abstract fun installedAppDao(): InstalledAppDao
|
||||||
|
abstract fun optionDao(): OptionDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun generateUid() = Random.Default.nextInt()
|
fun generateUid() = Random.Default.nextInt()
|
||||||
|
@ -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,
|
||||||
|
)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -49,7 +49,7 @@ abstract class SelectionDao {
|
|||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
|
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
|
||||||
selections.map { (selectionUid, patches) ->
|
selections.forEach { (selectionUid, patches) ->
|
||||||
clearSelection(selectionUid)
|
clearSelection(selectionUid)
|
||||||
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
|
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ val repositoryModule = module {
|
|||||||
singleOf(::NetworkInfo)
|
singleOf(::NetworkInfo)
|
||||||
singleOf(::PatchBundlePersistenceRepository)
|
singleOf(::PatchBundlePersistenceRepository)
|
||||||
singleOf(::PatchSelectionRepository)
|
singleOf(::PatchSelectionRepository)
|
||||||
|
singleOf(::PatchOptionsRepository)
|
||||||
singleOf(::PatchBundleRepository)
|
singleOf(::PatchBundleRepository)
|
||||||
singleOf(::WorkerRepository)
|
singleOf(::WorkerRepository)
|
||||||
singleOf(::DownloadedAppRepository)
|
singleOf(::DownloadedAppRepository)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -179,11 +179,11 @@ class PatcherWorker(
|
|||||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||||
|
|
||||||
// Set all patch options.
|
// Set all patch options.
|
||||||
args.options.forEach { (bundle, configuredPatchOptions) ->
|
args.options.forEach { (bundle, bundlePatchOptions) ->
|
||||||
val patches = allPatches[bundle] ?: return@forEach
|
val patches = allPatches[bundle] ?: return@forEach
|
||||||
configuredPatchOptions.forEach { (patchName, options) ->
|
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||||
val patchOptions = patches.single { it.name == patchName }.options
|
val patchOptions = patches.single { it.name == patchName }.options
|
||||||
options.forEach { (key, value) ->
|
configuredPatchOptions.forEach { (key, value) ->
|
||||||
patchOptions[key] = value
|
patchOptions[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,10 +301,7 @@ fun PatchesSelectorScreen(
|
|||||||
icon = { Icon(Icons.Outlined.Save, null) },
|
icon = { Icon(Icons.Outlined.Save, null) },
|
||||||
onClick = {
|
onClick = {
|
||||||
// TODO: only allow this if all required options have been set.
|
// TODO: only allow this if all required options have been set.
|
||||||
composableScope.launch {
|
onSave(vm.getCustomSelection(), vm.getOptions())
|
||||||
vm.saveSelection()
|
|
||||||
onSave(vm.getCustomSelection(), vm.getOptions())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -49,9 +49,13 @@ fun SelectedAppInfoScreen(
|
|||||||
vm: SelectedAppInfoViewModel
|
vm: SelectedAppInfoViewModel
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val bundles by remember(vm.selectedApp.packageName, vm.selectedApp.version) {
|
|
||||||
vm.bundlesRepo.bundleInfoFlow(vm.selectedApp.packageName, vm.selectedApp.version)
|
val packageName = vm.selectedApp.packageName
|
||||||
|
val version = vm.selectedApp.version
|
||||||
|
val bundles by remember(packageName, version) {
|
||||||
|
vm.bundlesRepo.bundleInfoFlow(packageName, version)
|
||||||
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
}.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
val allowExperimental by vm.prefs.allowExperimental.getAsState()
|
val allowExperimental by vm.prefs.allowExperimental.getAsState()
|
||||||
val patches by remember {
|
val patches by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@ -86,7 +90,7 @@ fun SelectedAppInfoScreen(
|
|||||||
onPatchClick(
|
onPatchClick(
|
||||||
vm.selectedApp,
|
vm.selectedApp,
|
||||||
patches,
|
patches,
|
||||||
vm.patchOptions
|
vm.getOptionsFiltered(bundles)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onPatchSelectorClick = {
|
onPatchSelectorClick = {
|
||||||
@ -97,7 +101,7 @@ fun SelectedAppInfoScreen(
|
|||||||
bundles,
|
bundles,
|
||||||
allowExperimental
|
allowExperimental
|
||||||
),
|
),
|
||||||
vm.patchOptions
|
vm.options
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -107,8 +111,8 @@ fun SelectedAppInfoScreen(
|
|||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
availablePatchCount = availablePatchCount,
|
availablePatchCount = availablePatchCount,
|
||||||
selectedPatchCount = selectedPatchCount,
|
selectedPatchCount = selectedPatchCount,
|
||||||
packageName = vm.selectedApp.packageName,
|
packageName = packageName,
|
||||||
version = vm.selectedApp.version,
|
version = version,
|
||||||
packageInfo = vm.selectedAppInfo,
|
packageInfo = vm.selectedAppInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -118,13 +122,12 @@ fun SelectedAppInfoScreen(
|
|||||||
vm.selectedApp = it
|
vm.selectedApp = it
|
||||||
navController.pop()
|
navController.pop()
|
||||||
},
|
},
|
||||||
viewModel = getViewModel { parametersOf(vm.selectedApp.packageName) }
|
viewModel = getViewModel { parametersOf(packageName) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
|
||||||
onSave = { patches, options ->
|
onSave = { patches, options ->
|
||||||
vm.setCustomPatches(patches)
|
vm.updateConfiguration(patches, options, bundles)
|
||||||
vm.patchOptions = options
|
|
||||||
navController.pop()
|
navController.pop()
|
||||||
},
|
},
|
||||||
onBackClick = navController::pop,
|
onBackClick = navController::pop,
|
||||||
@ -133,7 +136,7 @@ fun SelectedAppInfoScreen(
|
|||||||
PatchesSelectorViewModel.Params(
|
PatchesSelectorViewModel.Params(
|
||||||
destination.app,
|
destination.app,
|
||||||
destination.currentSelection,
|
destination.currentSelection,
|
||||||
destination.options
|
destination.options,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,10 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@ -14,6 +17,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.outlined.Key
|
import androidx.compose.material.icons.outlined.Key
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
@ -53,8 +57,10 @@ fun ImportExportSettingsScreen(
|
|||||||
it?.let(vm::exportKeystore)
|
it?.let(vm::exportKeystore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
|
||||||
|
|
||||||
vm.selectionAction?.let { action ->
|
vm.selectionAction?.let { action ->
|
||||||
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
|
|
||||||
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
vm.clearSelectionAction()
|
vm.clearSelectionAction()
|
||||||
@ -64,7 +70,7 @@ fun ImportExportSettingsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (vm.selectedBundle == null) {
|
if (vm.selectedBundle == null) {
|
||||||
BundleSelector(sources) {
|
BundleSelector(patchBundles) {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
vm.clearSelectionAction()
|
vm.clearSelectionAction()
|
||||||
} else {
|
} else {
|
||||||
@ -137,11 +143,110 @@ fun ImportExportSettingsScreen(
|
|||||||
headline = R.string.backup_patches_selection,
|
headline = R.string.backup_patches_selection,
|
||||||
description = R.string.backup_patches_selection_description
|
description = R.string.backup_patches_selection_description
|
||||||
)
|
)
|
||||||
|
// TODO: allow resetting selection for specific bundle or package name.
|
||||||
GroupItem(
|
GroupItem(
|
||||||
onClick = vm::resetSelection,
|
onClick = vm::resetSelection,
|
||||||
headline = R.string.clear_patches_selection,
|
headline = R.string.clear_patches_selection,
|
||||||
description = R.string.clear_patches_selection_description
|
description = R.string.clear_patches_selection_description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var showPackageSelector by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
var showBundleSelector by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showPackageSelector)
|
||||||
|
PackageSelector(packages = packagesWithOptions) { selected ->
|
||||||
|
selected?.let(vm::clearOptionsForPackage)
|
||||||
|
|
||||||
|
showPackageSelector = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBundleSelector)
|
||||||
|
BundleSelector(bundles = patchBundles) { bundle ->
|
||||||
|
bundle?.let(vm::clearOptionsForBundle)
|
||||||
|
|
||||||
|
showBundleSelector = false
|
||||||
|
}
|
||||||
|
|
||||||
|
GroupHeader(stringResource(R.string.patch_options))
|
||||||
|
// TODO: patch options import/export.
|
||||||
|
GroupItem(
|
||||||
|
onClick = { showPackageSelector = true },
|
||||||
|
headline = R.string.patch_options_clear_package,
|
||||||
|
description = R.string.patch_options_clear_package_description
|
||||||
|
)
|
||||||
|
if (patchBundles.size > 1)
|
||||||
|
GroupItem(
|
||||||
|
onClick = { showBundleSelector = true },
|
||||||
|
headline = R.string.patch_options_clear_bundle,
|
||||||
|
description = R.string.patch_options_clear_bundle_description,
|
||||||
|
)
|
||||||
|
GroupItem(
|
||||||
|
onClick = vm::resetOptions,
|
||||||
|
headline = R.string.patch_options_clear_all,
|
||||||
|
description = R.string.patch_options_clear_all_description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PackageSelector(packages: Set<String>, onFinish: (String?) -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val noPackages = packages.isEmpty()
|
||||||
|
|
||||||
|
LaunchedEffect(noPackages) {
|
||||||
|
if (noPackages) {
|
||||||
|
context.toast("No packages available.")
|
||||||
|
onFinish(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noPackages) return
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { onFinish(null) }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Select package",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
packages.forEach {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(48.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onFinish(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import app.revanced.manager.domain.repository.PatchSelectionRepository
|
|||||||
import app.revanced.manager.domain.repository.SerializedSelection
|
import app.revanced.manager.domain.repository.SerializedSelection
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
import app.revanced.manager.util.JSON_MIMETYPE
|
import app.revanced.manager.util.JSON_MIMETYPE
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
@ -38,10 +39,11 @@ class ImportExportViewModel(
|
|||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val keystoreManager: KeystoreManager,
|
private val keystoreManager: KeystoreManager,
|
||||||
private val selectionRepository: PatchSelectionRepository,
|
private val selectionRepository: PatchSelectionRepository,
|
||||||
|
private val optionsRepository: PatchOptionsRepository,
|
||||||
patchBundleRepository: PatchBundleRepository
|
patchBundleRepository: PatchBundleRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val contentResolver = app.contentResolver
|
private val contentResolver = app.contentResolver
|
||||||
val sources = patchBundleRepository.sources
|
val patchBundles = patchBundleRepository.sources
|
||||||
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
|
||||||
private set
|
private set
|
||||||
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
var selectionAction by mutableStateOf<SelectionAction?>(null)
|
||||||
@ -49,6 +51,20 @@ class ImportExportViewModel(
|
|||||||
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
||||||
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
||||||
|
|
||||||
|
val packagesWithOptions = optionsRepository.getPackagesWithSavedOptions()
|
||||||
|
|
||||||
|
fun clearOptionsForPackage(packageName: String) = viewModelScope.launch {
|
||||||
|
optionsRepository.clearOptionsForPackage(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearOptionsForBundle(patchBundle: PatchBundleSource) = viewModelScope.launch {
|
||||||
|
optionsRepository.clearOptionsForPatchBundle(patchBundle.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetOptions() = viewModelScope.launch {
|
||||||
|
optionsRepository.reset()
|
||||||
|
}
|
||||||
|
|
||||||
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
|
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
|
||||||
val path = withContext(Dispatchers.IO) {
|
val path = withContext(Dispatchers.IO) {
|
||||||
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
||||||
|
@ -17,7 +17,6 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
|||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
import app.revanced.manager.patcher.patch.PatchInfo
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
import app.revanced.manager.ui.model.BundleInfo
|
import app.revanced.manager.ui.model.BundleInfo
|
||||||
@ -31,20 +30,17 @@ import app.revanced.manager.util.saver.persistentMapSaver
|
|||||||
import app.revanced.manager.util.saver.persistentSetSaver
|
import app.revanced.manager.util.saver.persistentSetSaver
|
||||||
import app.revanced.manager.util.saver.snapshotStateMapSaver
|
import app.revanced.manager.util.saver.snapshotStateMapSaver
|
||||||
import app.revanced.manager.util.toast
|
import app.revanced.manager.util.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import kotlinx.collections.immutable.*
|
import kotlinx.collections.immutable.*
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@OptIn(SavedStateHandleSaveableApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class)
|
||||||
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||||
private val app: Application = get()
|
private val app: Application = get()
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
private val prefs: PreferencesManager = get()
|
private val prefs: PreferencesManager = get()
|
||||||
|
|
||||||
@ -169,11 +165,6 @@ class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
return patchOptions.mapValues { (_, allPatches) -> allPatches.mapValues { (_, options) -> options.toMap() } }
|
return patchOptions.mapValues { (_, allPatches) -> allPatches.mapValues { (_, options) -> options.toMap() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveSelection() = withContext(Dispatchers.Default) {
|
|
||||||
customPatchesSelection?.let { selectionRepository.updateSelection(packageName, it) }
|
|
||||||
?: selectionRepository.clearSelection(packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
||||||
|
|
||||||
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
||||||
|
@ -13,6 +13,7 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
|||||||
import androidx.lifecycle.viewmodel.compose.saveable
|
import androidx.lifecycle.viewmodel.compose.saveable
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.domain.repository.PatchOptionsRepository
|
||||||
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||||
import app.revanced.manager.ui.model.BundleInfo
|
import app.revanced.manager.ui.model.BundleInfo
|
||||||
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
import app.revanced.manager.ui.model.BundleInfo.Extensions.toPatchSelection
|
||||||
@ -31,10 +32,13 @@ import org.koin.core.component.get
|
|||||||
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
||||||
val bundlesRepo: PatchBundleRepository = get()
|
val bundlesRepo: PatchBundleRepository = get()
|
||||||
private val selectionRepository: PatchSelectionRepository = get()
|
private val selectionRepository: PatchSelectionRepository = get()
|
||||||
|
private val optionsRepository: PatchOptionsRepository = get()
|
||||||
private val pm: PM = get()
|
private val pm: PM = get()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
val prefs: PreferencesManager = get()
|
val prefs: PreferencesManager = get()
|
||||||
|
|
||||||
|
private val persistConfiguration = input.patches == null
|
||||||
|
|
||||||
private var _selectedApp by savedStateHandle.saveable {
|
private var _selectedApp by savedStateHandle.saveable {
|
||||||
mutableStateOf(input.app)
|
mutableStateOf(input.app)
|
||||||
}
|
}
|
||||||
@ -52,9 +56,18 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
invalidateSelectedAppInfo()
|
invalidateSelectedAppInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
var patchOptions: Options by savedStateHandle.saveable {
|
var options: Options by savedStateHandle.saveable {
|
||||||
mutableStateOf(emptyMap())
|
val state = mutableStateOf<Options>(emptyMap())
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||||
|
|
||||||
|
state.value = optionsRepository.getOptions(selectedApp.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
}
|
}
|
||||||
|
private set
|
||||||
|
|
||||||
private var selectionState by savedStateHandle.saveable {
|
private var selectionState by savedStateHandle.saveable {
|
||||||
if (input.patches != null) {
|
if (input.patches != null) {
|
||||||
@ -87,6 +100,8 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
selectedAppInfo = info
|
selectedAppInfo = info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
|
||||||
|
|
||||||
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
|
||||||
selectionState.patches(bundles, allowUnsupported)
|
selectionState.patches(bundles, allowUnsupported)
|
||||||
|
|
||||||
@ -96,14 +111,56 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
|
|||||||
): PatchesSelection? =
|
): PatchesSelection? =
|
||||||
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
|
(selectionState as? SelectionState.Customized)?.patches(bundles, allowUnsupported)
|
||||||
|
|
||||||
fun setCustomPatches(selection: PatchesSelection?) {
|
fun updateConfiguration(
|
||||||
|
selection: PatchesSelection?,
|
||||||
|
options: Options,
|
||||||
|
bundles: List<BundleInfo>
|
||||||
|
) {
|
||||||
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
selectionState = selection?.let(SelectionState::Customized) ?: SelectionState.Default
|
||||||
|
|
||||||
|
val filteredOptions = options.filtered(bundles)
|
||||||
|
this.options = filteredOptions
|
||||||
|
|
||||||
|
if (!persistConfiguration) return
|
||||||
|
|
||||||
|
val packageName = selectedApp.packageName
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
selection?.let { selectionRepository.updateSelection(packageName, it) }
|
||||||
|
?: selectionRepository.clearSelection(packageName)
|
||||||
|
|
||||||
|
optionsRepository.saveOptions(packageName, filteredOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val app: SelectedApp,
|
val app: SelectedApp,
|
||||||
val patches: PatchesSelection?,
|
val patches: PatchesSelection?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
/**
|
||||||
|
* Returns a copy with all nonexistent options removed.
|
||||||
|
*/
|
||||||
|
private fun Options.filtered(bundles: List<BundleInfo>): Options = buildMap options@{
|
||||||
|
bundles.forEach bundles@{ bundle ->
|
||||||
|
val bundleOptions = this@filtered[bundle.uid] ?: return@bundles
|
||||||
|
|
||||||
|
val patches = bundle.all.associateBy { it.name }
|
||||||
|
|
||||||
|
this@options[bundle.uid] = buildMap bundleOptions@{
|
||||||
|
bundleOptions.forEach patch@{ (patchName, values) ->
|
||||||
|
// Get all valid option keys for the patch.
|
||||||
|
val validOptionKeys =
|
||||||
|
patches[patchName]?.options?.map { it.key }?.toSet() ?: return@patch
|
||||||
|
|
||||||
|
this@bundleOptions[patchName] = values.filterKeys { key ->
|
||||||
|
key in validOptionKeys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed interface SelectionState : Parcelable {
|
private sealed interface SelectionState : Parcelable {
|
||||||
|
@ -90,6 +90,13 @@
|
|||||||
<string name="backup_patches_selection_fail">Failed to backup patches selection: %s</string>
|
<string name="backup_patches_selection_fail">Failed to backup patches selection: %s</string>
|
||||||
<string name="clear_patches_selection">Clear patches selection</string>
|
<string name="clear_patches_selection">Clear patches selection</string>
|
||||||
<string name="clear_patches_selection_description">Clear all patches selection</string>
|
<string name="clear_patches_selection_description">Clear all patches selection</string>
|
||||||
|
<string name="patch_options">Patch options</string>
|
||||||
|
<string name="patch_options_clear_package">Clear patch options for package</string>
|
||||||
|
<string name="patch_options_clear_package_description">Resets patch options for a single package</string>
|
||||||
|
<string name="patch_options_clear_bundle">Clear patch options for bundle</string>
|
||||||
|
<string name="patch_options_clear_bundle_description">Resets patch options for all patches in a bundle</string>
|
||||||
|
<string name="patch_options_clear_all">Clear all patch options</string>
|
||||||
|
<string name="patch_options_clear_all_description">Resets all patch options</string>
|
||||||
<string name="prefer_splits">Prefer split apks</string>
|
<string name="prefer_splits">Prefer split apks</string>
|
||||||
<string name="prefer_splits_description">Prefer split apks instead of full apks</string>
|
<string name="prefer_splits_description">Prefer split apks instead of full apks</string>
|
||||||
<string name="prefer_universal">Prefer universal apks</string>
|
<string name="prefer_universal">Prefer universal apks</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user