feat: remember patch options (#1449)

This commit is contained in:
Ax333l 2023-10-31 21:16:02 +01:00 committed by GitHub
parent 123ae37524
commit 7fe4724e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 511 additions and 36 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "371c7a84b122a2de8b660b35e6e9ce14",
"identityHash": "802fa2fda94b930bf0ebb85d195f1022",
"entities": [
{
"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": [],
"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')"
]
}
}

View File

@ -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()

View File

@ -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,
)

View File

@ -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)
}
}

View File

@ -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
)

View File

@ -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) })
}

View File

@ -17,6 +17,7 @@ val repositoryModule = module {
singleOf(::NetworkInfo)
singleOf(::PatchBundlePersistenceRepository)
singleOf(::PatchSelectionRepository)
singleOf(::PatchOptionsRepository)
singleOf(::PatchBundleRepository)
singleOf(::WorkerRepository)
singleOf(::DownloadedAppRepository)

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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())
}
)
}

View File

@ -49,9 +49,13 @@ fun SelectedAppInfoScreen(
vm: SelectedAppInfoViewModel
) {
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())
val allowExperimental by vm.prefs.allowExperimental.getAsState()
val patches by remember {
derivedStateOf {
@ -86,7 +90,7 @@ fun SelectedAppInfoScreen(
onPatchClick(
vm.selectedApp,
patches,
vm.patchOptions
vm.getOptionsFiltered(bundles)
)
},
onPatchSelectorClick = {
@ -97,7 +101,7 @@ fun SelectedAppInfoScreen(
bundles,
allowExperimental
),
vm.patchOptions
vm.options
)
)
},
@ -107,8 +111,8 @@ fun SelectedAppInfoScreen(
onBackClick = onBackClick,
availablePatchCount = availablePatchCount,
selectedPatchCount = selectedPatchCount,
packageName = vm.selectedApp.packageName,
version = vm.selectedApp.version,
packageName = packageName,
version = version,
packageInfo = vm.selectedAppInfo,
)
@ -118,13 +122,12 @@ fun SelectedAppInfoScreen(
vm.selectedApp = it
navController.pop()
},
viewModel = getViewModel { parametersOf(vm.selectedApp.packageName) }
viewModel = getViewModel { parametersOf(packageName) }
)
is SelectedAppInfoDestination.PatchesSelector -> PatchesSelectorScreen(
onSave = { patches, options ->
vm.setCustomPatches(patches)
vm.patchOptions = options
vm.updateConfiguration(patches, options, bundles)
navController.pop()
},
onBackClick = navController::pop,
@ -133,7 +136,7 @@ fun SelectedAppInfoScreen(
PatchesSelectorViewModel.Params(
destination.app,
destination.currentSelection,
destination.options
destination.options,
)
)
}

View File

@ -6,7 +6,10 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.rememberScrollState
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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@ -53,8 +57,10 @@ fun ImportExportSettingsScreen(
it?.let(vm::exportKeystore)
}
val patchBundles by vm.patchBundles.collectAsStateWithLifecycle(initialValue = emptyList())
val packagesWithOptions by vm.packagesWithOptions.collectAsStateWithLifecycle(initialValue = emptySet())
vm.selectionAction?.let { action ->
val sources by vm.sources.collectAsStateWithLifecycle(initialValue = emptyList())
val launcher = rememberLauncherForActivityResult(action.activityContract) { uri ->
if (uri == null) {
vm.clearSelectionAction()
@ -64,7 +70,7 @@ fun ImportExportSettingsScreen(
}
if (vm.selectedBundle == null) {
BundleSelector(sources) {
BundleSelector(patchBundles) {
if (it == null) {
vm.clearSelectionAction()
} else {
@ -137,11 +143,110 @@ fun ImportExportSettingsScreen(
headline = R.string.backup_patches_selection,
description = R.string.backup_patches_selection_description
)
// TODO: allow resetting selection for specific bundle or package name.
GroupItem(
onClick = vm::resetSelection,
headline = R.string.clear_patches_selection,
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
)
}
}
}
}
}

View File

@ -16,6 +16,7 @@ import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SerializedSelection
import app.revanced.manager.domain.repository.PatchBundleRepository
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.toast
import app.revanced.manager.util.uiSafe
@ -38,10 +39,11 @@ class ImportExportViewModel(
private val app: Application,
private val keystoreManager: KeystoreManager,
private val selectionRepository: PatchSelectionRepository,
private val optionsRepository: PatchOptionsRepository,
patchBundleRepository: PatchBundleRepository
) : ViewModel() {
private val contentResolver = app.contentResolver
val sources = patchBundleRepository.sources
val patchBundles = patchBundleRepository.sources
var selectedBundle by mutableStateOf<PatchBundleSource?>(null)
private set
var selectionAction by mutableStateOf<SelectionAction?>(null)
@ -49,6 +51,20 @@ class ImportExportViewModel(
private var keystoreImportPath by mutableStateOf<Path?>(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 {
val path = withContext(Dispatchers.IO) {
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {

View File

@ -17,7 +17,6 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import app.revanced.manager.R
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.patcher.patch.PatchInfo
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.snapshotStateMapSaver
import app.revanced.manager.util.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import kotlinx.collections.immutable.*
import kotlinx.coroutines.withContext
import java.util.Optional
@Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel(input: Params) : ViewModel(), KoinComponent {
private val app: Application = get()
private val selectionRepository: PatchSelectionRepository = get()
private val savedStateHandle: SavedStateHandle = 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() } }
}
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 setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {

View File

@ -13,6 +13,7 @@ import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
import androidx.lifecycle.viewmodel.compose.saveable
import app.revanced.manager.domain.manager.PreferencesManager
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.ui.model.BundleInfo
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 {
val bundlesRepo: PatchBundleRepository = get()
private val selectionRepository: PatchSelectionRepository = get()
private val optionsRepository: PatchOptionsRepository = get()
private val pm: PM = get()
private val savedStateHandle: SavedStateHandle = get()
val prefs: PreferencesManager = get()
private val persistConfiguration = input.patches == null
private var _selectedApp by savedStateHandle.saveable {
mutableStateOf(input.app)
}
@ -52,9 +56,18 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
invalidateSelectedAppInfo()
}
var patchOptions: Options by savedStateHandle.saveable {
mutableStateOf(emptyMap())
var options: Options by savedStateHandle.saveable {
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 {
if (input.patches != null) {
@ -87,6 +100,8 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
selectedAppInfo = info
}
fun getOptionsFiltered(bundles: List<BundleInfo>) = options.filtered(bundles)
fun getPatches(bundles: List<BundleInfo>, allowUnsupported: Boolean) =
selectionState.patches(bundles, allowUnsupported)
@ -96,14 +111,56 @@ class SelectedAppInfoViewModel(input: Params) : ViewModel(), KoinComponent {
): PatchesSelection? =
(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
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(
val app: SelectedApp,
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 {

View File

@ -90,6 +90,13 @@
<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_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_description">Prefer split apks instead of full apks</string>
<string name="prefer_universal">Prefer universal apks</string>