diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 34b687b..687c71d 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 16f2347..8b27d50 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 0000000..f037e8a --- /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 4c929bb..7aa7699 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 100d539..bcfb06d 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 8a31a48..b47e4af 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 4cbe7e7..3e7117b 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 0000000..e3d4657 --- /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 0000000..793b3d0 --- /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 b4a9bb5..de90fc7 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 316fc2f..638f156 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 44d37c5..963694d 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 4cd2bdd..e9656a8 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 a5739bd..0818956 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 27269b2..f1d5d13 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 0000000..8d7b7ec --- /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 2aa5dab..0f422d4 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