diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 34b687b49a..687c71d38a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,9 @@
+
+
diff --git a/app/src/main/java/app/revanced/manager/MainActivity.kt b/app/src/main/java/app/revanced/manager/MainActivity.kt
index 16f234794f..8b27d50771 100644
--- a/app/src/main/java/app/revanced/manager/MainActivity.kt
+++ b/app/src/main/java/app/revanced/manager/MainActivity.kt
@@ -59,7 +59,8 @@ class MainActivity : ComponentActivity() {
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
dynamicColor = prefs.dynamicColor
) {
- val navController = rememberNavController(startDestination = Destination.Dashboard)
+ val navController =
+ rememberNavController(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) }
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/data/platform/FileSystem.kt b/app/src/main/java/app/revanced/manager/data/platform/FileSystem.kt
new file mode 100644
index 0000000000..f037e8a6e6
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/data/platform/FileSystem.kt
@@ -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, 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
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
index 4c929bb3de..7aa7699ae4 100644
--- a/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
+++ b/app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
@@ -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)
diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
index 100d53990d..bcfb06d21e 100644
--- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchInfo.kt
@@ -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>, val defaultValue: Any?) {
+ constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required, option::class.java, option.value)
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
index 8a31a48276..b47e4afb75 100644
--- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
@@ -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>
@@ -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())
diff --git a/app/src/main/java/app/revanced/manager/ui/component/FileSelector.kt b/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt
similarity index 85%
rename from app/src/main/java/app/revanced/manager/ui/component/FileSelector.kt
rename to app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt
index 4cbe7e7a0e..3e7117b1f8 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/FileSelector.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/ContentSelector.kt
@@ -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)
}
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
new file mode 100644
index 0000000000..e3d465734a
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/patches/OptionFields.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt
new file mode 100644
index 0000000000..793b3d0bd5
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt
@@ -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)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/sources/LocalBundleSelectors.kt b/app/src/main/java/app/revanced/manager/ui/component/sources/LocalBundleSelectors.kt
index b4a9bb5640..de90fc7fce 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/sources/LocalBundleSelectors.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/sources/LocalBundleSelectors.kt
@@ -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
) {
diff --git a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt
index 316fc2fc61..638f1566d4 100644
--- a/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt
+++ b/app/src/main/java/app/revanced/manager/ui/destination/Destination.kt
@@ -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
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
index 44d37c504c..963694d5d8 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatchesSelectorScreen.kt
@@ -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?,
+ 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?") }
-)
\ No newline at end of file
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt
index 4cd2bddfd0..e9656a8a34 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/ImportExportSettingsScreen.kt
@@ -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)
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt
index a5739bdbc9..0818956351 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstallerViewModel.kt
@@ -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>
+ private val patcherWorkerId: UUID
- private val patcherWorkerId =
- workerRepository.launchExpedited(
- "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(
+ "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 ->
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt
index 27269b2a32..f1d5d1318d 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt
@@ -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>()
+ private val patchOptions =
+ mutableStateMapOf>>()
- var showOptionsDialog by mutableStateOf(false)
- private set
+ /**
+ * Show the patch options dialog for this patch.
+ */
+ var optionsDialog by mutableStateOf?>(null)
val compatibleVersions = mutableStateListOf()
@@ -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) {
@@ -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 SnapshotStateMap>.getOrCreate(key: K) =
+ getOrPut(key, ::mutableStateMapOf)
}
data class BundleInfo(
diff --git a/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt
new file mode 100644
index 0000000000..8d7b7ec31c
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/util/RequestManageStorageContract.kt
@@ -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() {
+ override fun createIntent(context: Context, input: String) = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
+
+ override fun getSynchronousResult(context: Context, input: String): SynchronousResult? = if (!forceLaunch && Environment.isExternalStorageManager()) SynchronousResult(true) else null
+
+ override fun parseResult(resultCode: Int, intent: Intent?) = Environment.isExternalStorageManager()
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt
index 2aa5daba9e..0f422d44cf 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -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>
+typealias Options = Map>>
fun Context.openUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
@@ -92,4 +96,9 @@ inline fun Flow>.flatMapLatestAndCombine(
combine(iterable.map(transformer)) {
combiner(it)
}
-}
\ No newline at end of file
+}
+
+val PathSaver = Saver(
+ save = { it.toString() },
+ restore = { Path(it) }
+)
\ No newline at end of file