feat: patch options (#45)

This commit is contained in:
Ax333l 2023-07-03 11:12:34 +02:00 committed by GitHub
parent c324483485
commit a0ab7a0e26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 431 additions and 76 deletions

View File

@ -10,6 +10,9 @@
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<queries>

View File

@ -59,7 +59,8 @@ class MainActivity : ComponentActivity() {
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
dynamicColor = prefs.dynamicColor
) {
val navController = rememberNavController<Destination>(startDestination = Destination.Dashboard)
val navController =
rememberNavController<Destination>(startDestination = Destination.Dashboard)
NavBackHandler(navController)
@ -83,11 +84,12 @@ class MainActivity : ComponentActivity() {
is Destination.PatchesSelector -> PatchesSelectorScreen(
onBackClick = { navController.pop() },
onPatchClick = {
onPatchClick = { patches, options ->
navController.navigate(
Destination.Installer(
destination.input,
it
patches,
options
)
)
},
@ -101,12 +103,7 @@ class MainActivity : ComponentActivity() {
navigate(Destination.Dashboard)
}
},
vm = getViewModel {
parametersOf(
destination.input,
destination.selectedPatches
)
}
vm = getViewModel { parametersOf(destination) }
)
}
}

View File

@ -0,0 +1,27 @@
package app.revanced.manager.data.platform
import android.app.Application
import android.os.Build
import android.os.Environment
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import app.revanced.manager.util.RequestManageStorageContract
class FileSystem(private val app: Application) {
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
fun externalFilesDir() = Environment.getExternalStorageDirectory().toPath()
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
private val storagePermissionName = if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
val contract = if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
return contract to storagePermissionName
}
fun hasStoragePermission() = if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(storagePermissionName) == PackageManager.PERMISSION_GRANTED
}

View File

@ -1,5 +1,6 @@
package app.revanced.manager.di
import app.revanced.manager.data.platform.FileSystem
import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.api.ManagerAPI
@ -12,6 +13,7 @@ import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedRepository)
singleOf(::ManagerAPI)
singleOf(::FileSystem)
singleOf(::SourcePersistenceRepository)
singleOf(::PatchSelectionRepository)
singleOf(::SourceRepository)

View File

@ -44,6 +44,7 @@ data class CompatiblePackage(
constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList())
}
data class Option(val title: String, val key: String, val description: String, val required: Boolean) {
constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required)
@Immutable
data class Option(val title: String, val key: String, val description: String, val required: Boolean, val type: Class<out PatchOption<*>>, val defaultValue: Any?) {
constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required, option::class.java, option.value)
}

View File

@ -19,8 +19,10 @@ import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -41,6 +43,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
val input: String,
val output: String,
val selectedPatches: PatchesSelection,
val options: Options,
val packageName: String,
val packageVersion: String,
val progress: MutableStateFlow<ImmutableList<Step>>
@ -124,14 +127,31 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
}
return try {
val patchList = args.selectedPatches.flatMap { (bundleName, selected) ->
bundles[bundleName]?.patchClasses(args.packageName)
?.filter { selected.contains(it.patchName) }
?: throw IllegalArgumentException("Patch bundle $bundleName does not exist")
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
val selectedBundles = args.selectedPatches.keys
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
// Set all patch options.
args.options.forEach { (bundle, configuredPatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach
configuredPatchOptions.forEach { (patchName, options) ->
patches.single { it.patchName == patchName }.options?.let {
options.forEach { (key, value) ->
it[key] = value
}
}
}
}
val patches = args.selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { selected.contains(it.patchName) }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
}
// Ensure they are in the correct order so we can track progress properly.
progressManager.replacePatchesList(patchList.map { it.patchName })
progressManager.replacePatchesList(patches.map { it.patchName })
updateProgress(Progress.Unpacking)
@ -143,7 +163,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
) {
updateProgress(it)
}.use { session ->
session.run(File(args.output), patchList, integrations)
session.run(File(args.output), patches, integrations)
}
Log.i(tag, "Patching succeeded".logFmt())

View File

@ -7,7 +7,7 @@ import androidx.compose.material3.Button
import androidx.compose.runtime.Composable
@Composable
fun FileSelector(mime: String, onSelect: (Uri) -> Unit, content: @Composable () -> Unit) {
fun ContentSelector(mime: String, onSelect: (Uri) -> Unit, content: @Composable () -> Unit) {
val activityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let(onSelect)
}

View File

@ -0,0 +1,84 @@
package app.revanced.manager.ui.component.patches
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import app.revanced.manager.data.platform.FileSystem
import app.revanced.manager.patcher.patch.Option
import app.revanced.patcher.patch.PatchOption
import org.koin.compose.rememberKoinInject
/**
* [Composable] functions do not support function references, so we have to use composable lambdas instead.
*/
private typealias OptionField = @Composable (Any?, (Any?) -> Unit) -> Unit
private val StringField: OptionField = { value, setValue ->
val fs: FileSystem = rememberKoinInject()
var showFileDialog by rememberSaveable { mutableStateOf(false) }
val (contract, permissionName) = fs.permissionContract()
val permissionLauncher = rememberLauncherForActivityResult(contract = contract) {
showFileDialog = it
}
val current = value as? String
if (showFileDialog) {
PathSelectorDialog(
root = fs.externalFilesDir()
) {
showFileDialog = false
it?.let { path ->
setValue(path.toString())
}
}
}
Column {
TextField(value = current ?: "", onValueChange = setValue)
Button(onClick = {
if (fs.hasStoragePermission()) {
showFileDialog = true
} else {
permissionLauncher.launch(permissionName)
}
}) {
Icon(Icons.Filled.FileOpen, null)
Text("Select file or folder")
}
}
}
private val BooleanField: OptionField = { value, setValue ->
val current = value as? Boolean
Switch(checked = current ?: false, onCheckedChange = setValue)
}
private val UnknownField: OptionField = { _, _ ->
Text("This type has not been implemented")
}
@Composable
fun OptionField(option: Option, value: Any?, setValue: (Any?) -> Unit) {
val implementation = remember(option.type) {
when (option.type) {
// These are the only two types that are currently used by the official patches.
PatchOption.StringOption::class.java -> StringField
PatchOption.BooleanOption::class.java -> BooleanField
else -> UnknownField
}
}
implementation(value, setValue)
}

View File

@ -0,0 +1,110 @@
package app.revanced.manager.ui.component.patches
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import app.revanced.manager.R
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.util.PathSaver
import java.nio.file.Path
import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PathSelectorDialog(root: Path, onSelect: (Path?) -> Unit) {
var currentDirectory by rememberSaveable(root, stateSaver = PathSaver) { mutableStateOf(root) }
val notAtRootDir = remember(currentDirectory) {
currentDirectory != root
}
val everything = remember(currentDirectory) {
currentDirectory.listDirectoryEntries()
}
val directories = remember(everything) {
everything.filter { it.isDirectory() }
}
val files = remember(everything) {
everything.filter { it.isRegularFile() }
}
Dialog(
onDismissRequest = { onSelect(null) },
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(R.string.select_file),
onBackClick = { onSelect(null) }
)
}
) { paddingValues ->
BackHandler(enabled = notAtRootDir) {
currentDirectory = currentDirectory.parent
}
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
Text(text = currentDirectory.toString())
Row(
modifier = Modifier.clickable { onSelect(currentDirectory) }
) {
Text("(Use this directory)")
}
if (notAtRootDir) {
Row(
modifier = Modifier.clickable { currentDirectory = currentDirectory.parent }
) {
Text("Previous directory")
}
}
directories.forEach {
Row(
modifier = Modifier.clickable { currentDirectory = it }
) {
Icon(Icons.Filled.Folder, null)
Text(text = it.name)
}
}
files.forEach {
Row(
modifier = Modifier.clickable { onSelect(it) }
) {
Icon(Icons.Filled.FileOpen, null)
Text(text = it.name)
}
}
}
}
}
}

View File

@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import app.revanced.manager.ui.component.FileSelector
import app.revanced.manager.ui.component.ContentSelector
import app.revanced.manager.util.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE
@ -15,14 +15,14 @@ fun LocalBundleSelectors(onPatchesSelection: (Uri) -> Unit, onIntegrationsSelect
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
FileSelector(
ContentSelector(
mime = JAR_MIMETYPE,
onSelect = onPatchesSelection
) {
Text("Patches")
}
FileSelector(
ContentSelector(
mime = APK_MIMETYPE,
onSelect = onIntegrationsSelection
) {

View File

@ -2,8 +2,10 @@ package app.revanced.manager.ui.destination
import android.os.Parcelable
import app.revanced.manager.util.AppInfo
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
sealed interface Destination : Parcelable {
@ -20,5 +22,5 @@ sealed interface Destination : Parcelable {
data class PatchesSelector(val input: AppInfo) : Destination
@Parcelize
data class Installer(val input: AppInfo, val selectedPatches: PatchesSelection) : Destination
data class Installer(val app: AppInfo, val selectedPatches: PatchesSelection, val options: @RawValue Options) : Destination
}

View File

@ -13,12 +13,15 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
@ -40,21 +43,25 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.patches.OptionField
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun PatchesSelectorScreen(
onPatchClick: (PatchesSelection) -> Unit,
onPatchClick: (PatchesSelection, Options) -> Unit,
onBackClick: () -> Unit,
vm: PatchesSelectorViewModel
) {
@ -70,7 +77,15 @@ fun PatchesSelectorScreen(
onDismissRequest = vm::dismissDialogs
)
if (vm.showOptionsDialog) OptionsDialog(onDismissRequest = vm::dismissDialogs, onConfirm = {})
vm.optionsDialog?.let { (bundle, patch) ->
OptionsDialog(
onDismissRequest = vm::dismissDialogs,
patch = patch,
values = vm.getOptions(bundle, patch),
set = { key, value -> vm.setOption(bundle, patch, key, value) },
unset = { vm.unsetOption(bundle, patch, it) }
)
}
Scaffold(
topBar = {
@ -93,7 +108,8 @@ fun PatchesSelectorScreen(
icon = { Icon(Icons.Default.Build, null) },
onClick = {
composableScope.launch {
onPatchClick(vm.getAndSaveSelection())
// TODO: only allow this if all required options have been set.
onPatchClick(vm.getAndSaveSelection(), vm.getOptions())
}
}
)
@ -112,7 +128,13 @@ fun PatchesSelectorScreen(
bundles.forEachIndexed { index, bundle ->
Tab(
selected = pagerState.currentPage == index,
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
onClick = {
composableScope.launch {
pagerState.animateScrollToPage(
index
)
}
},
text = { Text(bundle.name) },
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
@ -177,8 +199,13 @@ fun PatchesSelectorScreen(
) { patch ->
PatchItem(
patch = patch,
onOptionsDialog = vm::openOptionsDialog,
selected = supported && vm.isSelected(bundle.uid, patch),
onOptionsDialog = {
vm.optionsDialog = bundle.uid to patch
},
selected = supported && vm.isSelected(
bundle.uid,
patch
),
onToggle = { vm.togglePatch(bundle.uid, patch) },
supported = supported
)
@ -299,24 +326,56 @@ fun UnsupportedDialog(
}
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OptionsDialog(
onDismissRequest: () -> Unit, onConfirm: () -> Unit
) = AlertDialog(
patch: PatchInfo,
values: Map<String, Any?>?,
unset: (String) -> Unit,
set: (String, Any?) -> Unit,
onDismissRequest: () -> Unit,
) = Dialog(
onDismissRequest = onDismissRequest,
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.cancel))
properties = DialogProperties(
usePlatformDefaultWidth = false,
dismissOnBackPress = true
)
) {
Scaffold(
topBar = {
AppTopBar(
title = patch.name,
onBackClick = onDismissRequest
)
}
},
confirmButton = {
TextButton(onClick = {
onConfirm()
onDismissRequest()
}) {
Text(stringResource(R.string.apply))
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
patch.options?.forEach {
ListItem(
headlineContent = { Text(it.title) },
supportingContent = { Text(it.description) },
overlineContent = {
Button(onClick = { unset(it.key) }) {
Text("reset")
}
},
trailingContent = {
val key = it.key
val value =
if (values == null || !values.contains(key)) it.defaultValue else values[key]
OptionField(option = it, value = value, setValue = { set(key, it) })
}
)
}
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.apply))
}
}
},
title = { Text(stringResource(R.string.options)) },
text = { Text("You really thought these would exist?") }
)
}
}

View File

@ -25,7 +25,7 @@ import app.revanced.manager.domain.manager.KeystoreManager.Companion.DEFAULT
import app.revanced.manager.domain.manager.KeystoreManager.Companion.FLUTTER_MANAGER_PASSWORD
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.FileSelector
import app.revanced.manager.ui.component.ContentSelector
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.sources.SourceSelector
import org.koin.androidx.compose.getViewModel
@ -181,7 +181,7 @@ fun ImportKeystoreDialog(
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
FileSelector(
ContentSelector(
mime = "*/*",
onSelect = {
onImport(it, cn, pass)

View File

@ -21,13 +21,14 @@ import app.revanced.manager.R
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.patcher.worker.Step
import app.revanced.manager.service.InstallService
import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.AppInfo
import app.revanced.manager.ui.destination.Destination
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -35,18 +36,16 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import java.nio.file.Files
import java.util.UUID
@Stable
class InstallerViewModel(
input: AppInfo,
selectedPatches: PatchesSelection
) : ViewModel(), KoinComponent {
class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinComponent {
private val keystoreManager: KeystoreManager by inject()
private val app: Application by inject()
private val pm: PM by inject()
private val workerRepository: WorkerRepository by inject()
val packageName: String = input.packageName
val packageName: String = input.app.packageName
private val outputFile = File(app.cacheDir, "output.apk")
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
private var hasSigned = false
@ -59,23 +58,31 @@ class InstallerViewModel(
private val workManager = WorkManager.getInstance(app)
private val _progress = MutableStateFlow(PatcherProgressManager.generateSteps(
app,
selectedPatches.flatMap { (_, selected) -> selected }
).toImmutableList())
val progress = _progress.asStateFlow()
private val _progress: MutableStateFlow<ImmutableList<Step>>
private val patcherWorkerId: UUID
private val patcherWorkerId =
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args(
input.path!!.absolutePath,
outputFile.path,
selectedPatches,
input.packageName,
input.packageInfo!!.versionName,
_progress
init {
val (appInfo, patches, options) = input
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
app,
patches.flatMap { (_, selected) -> selected }
).toImmutableList())
patcherWorkerId =
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args(
appInfo.path!!.absolutePath,
outputFile.path,
patches,
options,
packageName,
appInfo.packageInfo!!.versionName,
_progress
)
)
)
}
val progress = _progress.asStateFlow()
val patcherState =
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->

View File

@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.manager.PreferencesManager
@ -13,6 +14,7 @@ import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SourceRepository
import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.util.AppInfo
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.SnapshotStateSet
import app.revanced.manager.util.flatMapLatestAndCombine
@ -58,9 +60,13 @@ class PatchesSelectorViewModel(
}
private val selectedPatches = mutableStateMapOf<Int, SnapshotStateSet<String>>()
private val patchOptions =
mutableStateMapOf<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>()
var showOptionsDialog by mutableStateOf(false)
private set
/**
* Show the patch options dialog for this patch.
*/
var optionsDialog by mutableStateOf<Pair<Int, PatchInfo>?>(null)
val compatibleVersions = mutableStateListOf<String>()
@ -118,13 +124,20 @@ class PatchesSelectorViewModel(
}
}
fun dismissDialogs() {
showOptionsDialog = false
compatibleVersions.clear()
fun getOptions(): Options = patchOptions
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value
}
fun openOptionsDialog() {
showOptionsDialog = true
fun unsetOption(bundle: Int, patch: PatchInfo, key: String) {
patchOptions[bundle]?.get(patch.name)?.remove(key)
}
fun dismissDialogs() {
optionsDialog = null
compatibleVersions.clear()
}
fun openUnsupportedDialog(unsupportedVersions: List<PatchInfo>) {
@ -148,6 +161,9 @@ class PatchesSelectorViewModel(
const val SHOW_SUPPORTED = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1
const val SHOW_UNSUPPORTED = 4 // 2^2
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
getOrPut(key, ::mutableStateMapOf)
}
data class BundleInfo(

View File

@ -0,0 +1,18 @@
package app.revanced.manager.util
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
@RequiresApi(Build.VERSION_CODES.R)
class RequestManageStorageContract(private val forceLaunch: Boolean = false) : ActivityResultContract<String, Boolean>() {
override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
override fun getSynchronousResult(context: Context, input: String): SynchronousResult<Boolean>? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null
override fun parseResult(resultCode: Int, intent: Intent?) = Environment.isExternalStorageManager()
}

View File

@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable
import android.util.Log
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.runtime.saveable.Saver
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
@ -19,8 +20,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch
import java.nio.file.Path
import kotlin.io.path.Path
typealias PatchesSelection = Map<Int, List<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
fun Context.openUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
@ -92,4 +96,9 @@ inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
combine(iterable.map(transformer)) {
combiner(it)
}
}
}
val PathSaver = Saver<Path, String>(
save = { it.toString() },
restore = { Path(it) }
)