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.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <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" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<queries> <queries>

View File

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

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 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.PatchSelectionRepository
import app.revanced.manager.domain.repository.ReVancedRepository import app.revanced.manager.domain.repository.ReVancedRepository
import app.revanced.manager.network.api.ManagerAPI import app.revanced.manager.network.api.ManagerAPI
@ -12,6 +13,7 @@ import org.koin.dsl.module
val repositoryModule = module { val repositoryModule = module {
singleOf(::ReVancedRepository) singleOf(::ReVancedRepository)
singleOf(::ManagerAPI) singleOf(::ManagerAPI)
singleOf(::FileSystem)
singleOf(::SourcePersistenceRepository) singleOf(::SourcePersistenceRepository)
singleOf(::PatchSelectionRepository) singleOf(::PatchSelectionRepository)
singleOf(::SourceRepository) singleOf(::SourceRepository)

View File

@ -44,6 +44,7 @@ data class CompatiblePackage(
constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList()) 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) { @Immutable
constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required) 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.domain.worker.WorkerRepository
import app.revanced.manager.patcher.Session import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.aapt.Aapt import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -41,6 +43,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
val input: String, val input: String,
val output: String, val output: String,
val selectedPatches: PatchesSelection, val selectedPatches: PatchesSelection,
val options: Options,
val packageName: String, val packageName: String,
val packageVersion: String, val packageVersion: String,
val progress: MutableStateFlow<ImmutableList<Step>> val progress: MutableStateFlow<ImmutableList<Step>>
@ -124,14 +127,31 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
} }
return try { return try {
val patchList = args.selectedPatches.flatMap { (bundleName, selected) -> // TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
bundles[bundleName]?.patchClasses(args.packageName) val selectedBundles = args.selectedPatches.keys
?.filter { selected.contains(it.patchName) } val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
?: throw IllegalArgumentException("Patch bundle $bundleName does not exist") .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. // 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) updateProgress(Progress.Unpacking)
@ -143,7 +163,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
) { ) {
updateProgress(it) updateProgress(it)
}.use { session -> }.use { session ->
session.run(File(args.output), patchList, integrations) session.run(File(args.output), patches, integrations)
} }
Log.i(tag, "Patching succeeded".logFmt()) Log.i(tag, "Patching succeeded".logFmt())

View File

@ -7,7 +7,7 @@ import androidx.compose.material3.Button
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@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 -> val activityLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let(onSelect) 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp 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.APK_MIMETYPE
import app.revanced.manager.util.JAR_MIMETYPE import app.revanced.manager.util.JAR_MIMETYPE
@ -15,14 +15,14 @@ fun LocalBundleSelectors(onPatchesSelection: (Uri) -> Unit, onIntegrationsSelect
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
FileSelector( ContentSelector(
mime = JAR_MIMETYPE, mime = JAR_MIMETYPE,
onSelect = onPatchesSelection onSelect = onPatchesSelection
) { ) {
Text("Patches") Text("Patches")
} }
FileSelector( ContentSelector(
mime = APK_MIMETYPE, mime = APK_MIMETYPE,
onSelect = onIntegrationsSelection onSelect = onIntegrationsSelection
) { ) {

View File

@ -2,8 +2,10 @@ package app.revanced.manager.ui.destination
import android.os.Parcelable import android.os.Parcelable
import app.revanced.manager.util.AppInfo import app.revanced.manager.util.AppInfo
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
sealed interface Destination : Parcelable { sealed interface Destination : Parcelable {
@ -20,5 +22,5 @@ sealed interface Destination : Parcelable {
data class PatchesSelector(val input: AppInfo) : Destination data class PatchesSelector(val input: AppInfo) : Destination
@Parcelize @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.lazy.items
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState 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.Icons
import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.outlined.HelpOutline import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Search
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
@ -40,21 +43,25 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R import app.revanced.manager.R
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.ui.component.AppTopBar 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
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED 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_UNIVERSAL
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun PatchesSelectorScreen( fun PatchesSelectorScreen(
onPatchClick: (PatchesSelection) -> Unit, onPatchClick: (PatchesSelection, Options) -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
vm: PatchesSelectorViewModel vm: PatchesSelectorViewModel
) { ) {
@ -70,7 +77,15 @@ fun PatchesSelectorScreen(
onDismissRequest = vm::dismissDialogs 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( Scaffold(
topBar = { topBar = {
@ -93,7 +108,8 @@ fun PatchesSelectorScreen(
icon = { Icon(Icons.Default.Build, null) }, icon = { Icon(Icons.Default.Build, null) },
onClick = { onClick = {
composableScope.launch { 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 -> bundles.forEachIndexed { index, bundle ->
Tab( Tab(
selected = pagerState.currentPage == index, selected = pagerState.currentPage == index,
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } }, onClick = {
composableScope.launch {
pagerState.animateScrollToPage(
index
)
}
},
text = { Text(bundle.name) }, text = { Text(bundle.name) },
selectedContentColor = MaterialTheme.colorScheme.primary, selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
@ -177,8 +199,13 @@ fun PatchesSelectorScreen(
) { patch -> ) { patch ->
PatchItem( PatchItem(
patch = patch, patch = patch,
onOptionsDialog = vm::openOptionsDialog, onOptionsDialog = {
selected = supported && vm.isSelected(bundle.uid, patch), vm.optionsDialog = bundle.uid to patch
},
selected = supported && vm.isSelected(
bundle.uid,
patch
),
onToggle = { vm.togglePatch(bundle.uid, patch) }, onToggle = { vm.togglePatch(bundle.uid, patch) },
supported = supported supported = supported
) )
@ -299,24 +326,56 @@ fun UnsupportedDialog(
} }
) )
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun OptionsDialog( fun OptionsDialog(
onDismissRequest: () -> Unit, onConfirm: () -> Unit patch: PatchInfo,
) = AlertDialog( values: Map<String, Any?>?,
unset: (String) -> Unit,
set: (String, Any?) -> Unit,
onDismissRequest: () -> Unit,
) = Dialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
dismissButton = { properties = DialogProperties(
TextButton(onClick = onDismissRequest) { usePlatformDefaultWidth = false,
Text(stringResource(R.string.cancel)) dismissOnBackPress = true
)
) {
Scaffold(
topBar = {
AppTopBar(
title = patch.name,
onBackClick = onDismissRequest
)
} }
}, ) { paddingValues ->
confirmButton = { Column(
TextButton(onClick = { modifier = Modifier
onConfirm() .padding(paddingValues)
onDismissRequest() .verticalScroll(rememberScrollState())
}) { ) {
Text(stringResource(R.string.apply)) 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.KeystoreManager.Companion.FLUTTER_MANAGER_PASSWORD
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.component.AppTopBar 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.GroupHeader
import app.revanced.manager.ui.component.sources.SourceSelector import app.revanced.manager.ui.component.sources.SourceSelector
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@ -181,7 +181,7 @@ fun ImportKeystoreDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
confirmButton = { confirmButton = {
FileSelector( ContentSelector(
mime = "*/*", mime = "*/*",
onSelect = { onSelect = {
onImport(it, cn, pass) 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.domain.worker.WorkerRepository
import app.revanced.manager.patcher.worker.PatcherProgressManager import app.revanced.manager.patcher.worker.PatcherProgressManager
import app.revanced.manager.patcher.worker.PatcherWorker 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.InstallService
import app.revanced.manager.service.UninstallService 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.PM
import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.tag import app.revanced.manager.util.tag
import app.revanced.manager.util.toast import app.revanced.manager.util.toast
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -35,18 +36,16 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.util.UUID
@Stable @Stable
class InstallerViewModel( class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinComponent {
input: AppInfo,
selectedPatches: PatchesSelection
) : ViewModel(), KoinComponent {
private val keystoreManager: KeystoreManager by inject() private val keystoreManager: KeystoreManager by inject()
private val app: Application by inject() private val app: Application by inject()
private val pm: PM by inject() private val pm: PM by inject()
private val workerRepository: WorkerRepository 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 outputFile = File(app.cacheDir, "output.apk")
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() } private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
private var hasSigned = false private var hasSigned = false
@ -59,23 +58,31 @@ class InstallerViewModel(
private val workManager = WorkManager.getInstance(app) private val workManager = WorkManager.getInstance(app)
private val _progress = MutableStateFlow(PatcherProgressManager.generateSteps( private val _progress: MutableStateFlow<ImmutableList<Step>>
app, private val patcherWorkerId: UUID
selectedPatches.flatMap { (_, selected) -> selected }
).toImmutableList())
val progress = _progress.asStateFlow()
private val patcherWorkerId = init {
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>( val (appInfo, patches, options) = input
"patching", PatcherWorker.Args(
input.path!!.absolutePath, _progress = MutableStateFlow(PatcherProgressManager.generateSteps(
outputFile.path, app,
selectedPatches, patches.flatMap { (_, selected) -> selected }
input.packageName, ).toImmutableList())
input.packageInfo!!.versionName, patcherWorkerId =
_progress 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 = val patcherState =
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo -> 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.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.manager.PreferencesManager 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.domain.repository.SourceRepository
import app.revanced.manager.patcher.patch.PatchInfo import app.revanced.manager.patcher.patch.PatchInfo
import app.revanced.manager.util.AppInfo import app.revanced.manager.util.AppInfo
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.PatchesSelection
import app.revanced.manager.util.SnapshotStateSet import app.revanced.manager.util.SnapshotStateSet
import app.revanced.manager.util.flatMapLatestAndCombine import app.revanced.manager.util.flatMapLatestAndCombine
@ -58,9 +60,13 @@ class PatchesSelectorViewModel(
} }
private val selectedPatches = mutableStateMapOf<Int, SnapshotStateSet<String>>() 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>() val compatibleVersions = mutableStateListOf<String>()
@ -118,13 +124,20 @@ class PatchesSelectorViewModel(
} }
} }
fun dismissDialogs() { fun getOptions(): Options = patchOptions
showOptionsDialog = false fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
compatibleVersions.clear()
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value
} }
fun openOptionsDialog() { fun unsetOption(bundle: Int, patch: PatchInfo, key: String) {
showOptionsDialog = true patchOptions[bundle]?.get(patch.name)?.remove(key)
}
fun dismissDialogs() {
optionsDialog = null
compatibleVersions.clear()
} }
fun openUnsupportedDialog(unsupportedVersions: List<PatchInfo>) { fun openUnsupportedDialog(unsupportedVersions: List<PatchInfo>) {
@ -148,6 +161,9 @@ class PatchesSelectorViewModel(
const val SHOW_SUPPORTED = 1 // 2^0 const val SHOW_SUPPORTED = 1 // 2^0
const val SHOW_UNIVERSAL = 2 // 2^1 const val SHOW_UNIVERSAL = 2 // 2^1
const val SHOW_UNSUPPORTED = 4 // 2^2 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( 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.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.saveable.Saver
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@ -19,8 +20,11 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.nio.file.Path
import kotlin.io.path.Path
typealias PatchesSelection = Map<Int, List<String>> typealias PatchesSelection = Map<Int, List<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
fun Context.openUrl(url: String) { fun Context.openUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply { 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)) { combine(iterable.map(transformer)) {
combiner(it) combiner(it)
} }
} }
val PathSaver = Saver<Path, String>(
save = { it.toString() },
restore = { Path(it) }
)