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, "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')"
] ]
} }
} }

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

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

View File

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

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

View File

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

View File

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

View File

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

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.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 {

View File

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

View File

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

View File

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