mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: patch options (#45)
This commit is contained in:
parent
7ac3bb74e0
commit
01fd4c8ffa
@ -10,6 +10,9 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<queries>
|
||||
|
@ -59,7 +59,8 @@ class MainActivity : ComponentActivity() {
|
||||
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
|
||||
dynamicColor = prefs.dynamicColor
|
||||
) {
|
||||
val navController = rememberNavController<Destination>(startDestination = Destination.Dashboard)
|
||||
val navController =
|
||||
rememberNavController<Destination>(startDestination = Destination.Dashboard)
|
||||
|
||||
NavBackHandler(navController)
|
||||
|
||||
@ -83,11 +84,12 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
is Destination.PatchesSelector -> PatchesSelectorScreen(
|
||||
onBackClick = { navController.pop() },
|
||||
onPatchClick = {
|
||||
onPatchClick = { patches, options ->
|
||||
navController.navigate(
|
||||
Destination.Installer(
|
||||
destination.input,
|
||||
it
|
||||
patches,
|
||||
options
|
||||
)
|
||||
)
|
||||
},
|
||||
@ -101,12 +103,7 @@ class MainActivity : ComponentActivity() {
|
||||
navigate(Destination.Dashboard)
|
||||
}
|
||||
},
|
||||
vm = getViewModel {
|
||||
parametersOf(
|
||||
destination.input,
|
||||
destination.selectedPatches
|
||||
)
|
||||
}
|
||||
vm = getViewModel { parametersOf(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -44,6 +44,7 @@ data class CompatiblePackage(
|
||||
constructor(pkg: Package) : this(pkg.name, pkg.versions.toList().toImmutableList())
|
||||
}
|
||||
|
||||
data class Option(val title: String, val key: String, val description: String, val required: Boolean) {
|
||||
constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required)
|
||||
@Immutable
|
||||
data class Option(val title: String, val key: String, val description: String, val required: Boolean, val type: Class<out PatchOption<*>>, val defaultValue: Any?) {
|
||||
constructor(option: PatchOption<*>) : this(option.title, option.key, option.description, option.required, option::class.java, option.value)
|
||||
}
|
@ -19,8 +19,10 @@ import app.revanced.manager.domain.worker.Worker
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.Session
|
||||
import app.revanced.manager.patcher.aapt.Aapt
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchesSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.patcher.extensions.PatchExtensions.options
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@ -41,6 +43,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
||||
val input: String,
|
||||
val output: String,
|
||||
val selectedPatches: PatchesSelection,
|
||||
val options: Options,
|
||||
val packageName: String,
|
||||
val packageVersion: String,
|
||||
val progress: MutableStateFlow<ImmutableList<Step>>
|
||||
@ -124,14 +127,31 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
||||
}
|
||||
|
||||
return try {
|
||||
val patchList = args.selectedPatches.flatMap { (bundleName, selected) ->
|
||||
bundles[bundleName]?.patchClasses(args.packageName)
|
||||
?.filter { selected.contains(it.patchName) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundleName does not exist")
|
||||
// TODO: consider passing all the classes directly now that the input no longer needs to be serializable.
|
||||
val selectedBundles = args.selectedPatches.keys
|
||||
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
|
||||
.mapValues { (_, bundle) -> bundle.patchClasses(args.packageName) }
|
||||
|
||||
// Set all patch options.
|
||||
args.options.forEach { (bundle, configuredPatchOptions) ->
|
||||
val patches = allPatches[bundle] ?: return@forEach
|
||||
configuredPatchOptions.forEach { (patchName, options) ->
|
||||
patches.single { it.patchName == patchName }.options?.let {
|
||||
options.forEach { (key, value) ->
|
||||
it[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val patches = args.selectedPatches.flatMap { (bundle, selected) ->
|
||||
allPatches[bundle]?.filter { selected.contains(it.patchName) }
|
||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||
}
|
||||
|
||||
|
||||
// Ensure they are in the correct order so we can track progress properly.
|
||||
progressManager.replacePatchesList(patchList.map { it.patchName })
|
||||
progressManager.replacePatchesList(patches.map { it.patchName })
|
||||
|
||||
updateProgress(Progress.Unpacking)
|
||||
|
||||
@ -143,7 +163,7 @@ class PatcherWorker(context: Context, parameters: WorkerParameters) :
|
||||
) {
|
||||
updateProgress(it)
|
||||
}.use { session ->
|
||||
session.run(File(args.output), patchList, integrations)
|
||||
session.run(File(args.output), patches, integrations)
|
||||
}
|
||||
|
||||
Log.i(tag, "Patching succeeded".logFmt())
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
) {
|
||||
|
@ -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
|
||||
}
|
@ -13,12 +13,15 @@ import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Build
|
||||
import androidx.compose.material.icons.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Search
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
@ -40,21 +43,25 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.patches.OptionField
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_SUPPORTED
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNIVERSAL
|
||||
import app.revanced.manager.ui.viewmodel.PatchesSelectorViewModel.Companion.SHOW_UNSUPPORTED
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchesSelection
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PatchesSelectorScreen(
|
||||
onPatchClick: (PatchesSelection) -> Unit,
|
||||
onPatchClick: (PatchesSelection, Options) -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
vm: PatchesSelectorViewModel
|
||||
) {
|
||||
@ -70,7 +77,15 @@ fun PatchesSelectorScreen(
|
||||
onDismissRequest = vm::dismissDialogs
|
||||
)
|
||||
|
||||
if (vm.showOptionsDialog) OptionsDialog(onDismissRequest = vm::dismissDialogs, onConfirm = {})
|
||||
vm.optionsDialog?.let { (bundle, patch) ->
|
||||
OptionsDialog(
|
||||
onDismissRequest = vm::dismissDialogs,
|
||||
patch = patch,
|
||||
values = vm.getOptions(bundle, patch),
|
||||
set = { key, value -> vm.setOption(bundle, patch, key, value) },
|
||||
unset = { vm.unsetOption(bundle, patch, it) }
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@ -93,7 +108,8 @@ fun PatchesSelectorScreen(
|
||||
icon = { Icon(Icons.Default.Build, null) },
|
||||
onClick = {
|
||||
composableScope.launch {
|
||||
onPatchClick(vm.getAndSaveSelection())
|
||||
// TODO: only allow this if all required options have been set.
|
||||
onPatchClick(vm.getAndSaveSelection(), vm.getOptions())
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -112,7 +128,13 @@ fun PatchesSelectorScreen(
|
||||
bundles.forEachIndexed { index, bundle ->
|
||||
Tab(
|
||||
selected = pagerState.currentPage == index,
|
||||
onClick = { composableScope.launch { pagerState.animateScrollToPage(index) } },
|
||||
onClick = {
|
||||
composableScope.launch {
|
||||
pagerState.animateScrollToPage(
|
||||
index
|
||||
)
|
||||
}
|
||||
},
|
||||
text = { Text(bundle.name) },
|
||||
selectedContentColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@ -177,8 +199,13 @@ fun PatchesSelectorScreen(
|
||||
) { patch ->
|
||||
PatchItem(
|
||||
patch = patch,
|
||||
onOptionsDialog = vm::openOptionsDialog,
|
||||
selected = supported && vm.isSelected(bundle.uid, patch),
|
||||
onOptionsDialog = {
|
||||
vm.optionsDialog = bundle.uid to patch
|
||||
},
|
||||
selected = supported && vm.isSelected(
|
||||
bundle.uid,
|
||||
patch
|
||||
),
|
||||
onToggle = { vm.togglePatch(bundle.uid, patch) },
|
||||
supported = supported
|
||||
)
|
||||
@ -299,24 +326,56 @@ fun UnsupportedDialog(
|
||||
}
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OptionsDialog(
|
||||
onDismissRequest: () -> Unit, onConfirm: () -> Unit
|
||||
) = AlertDialog(
|
||||
patch: PatchInfo,
|
||||
values: Map<String, Any?>?,
|
||||
unset: (String) -> Unit,
|
||||
set: (String, Any?) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) = Dialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
dismissOnBackPress = true
|
||||
)
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = patch.name,
|
||||
onBackClick = onDismissRequest
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(stringResource(R.string.apply))
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
patch.options?.forEach {
|
||||
ListItem(
|
||||
headlineContent = { Text(it.title) },
|
||||
supportingContent = { Text(it.description) },
|
||||
overlineContent = {
|
||||
Button(onClick = { unset(it.key) }) {
|
||||
Text("reset")
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
val key = it.key
|
||||
val value =
|
||||
if (values == null || !values.contains(key)) it.defaultValue else values[key]
|
||||
|
||||
OptionField(option = it, value = value, setValue = { set(key, it) })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.apply))
|
||||
}
|
||||
}
|
||||
},
|
||||
title = { Text(stringResource(R.string.options)) },
|
||||
text = { Text("You really thought these would exist?") }
|
||||
)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -21,13 +21,14 @@ import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.worker.WorkerRepository
|
||||
import app.revanced.manager.patcher.worker.PatcherProgressManager
|
||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||
import app.revanced.manager.patcher.worker.Step
|
||||
import app.revanced.manager.service.InstallService
|
||||
import app.revanced.manager.service.UninstallService
|
||||
import app.revanced.manager.util.AppInfo
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
import app.revanced.manager.util.PM
|
||||
import app.revanced.manager.util.PatchesSelection
|
||||
import app.revanced.manager.util.tag
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@ -35,18 +36,16 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.UUID
|
||||
|
||||
@Stable
|
||||
class InstallerViewModel(
|
||||
input: AppInfo,
|
||||
selectedPatches: PatchesSelection
|
||||
) : ViewModel(), KoinComponent {
|
||||
class InstallerViewModel(input: Destination.Installer) : ViewModel(), KoinComponent {
|
||||
private val keystoreManager: KeystoreManager by inject()
|
||||
private val app: Application by inject()
|
||||
private val pm: PM by inject()
|
||||
private val workerRepository: WorkerRepository by inject()
|
||||
|
||||
val packageName: String = input.packageName
|
||||
val packageName: String = input.app.packageName
|
||||
private val outputFile = File(app.cacheDir, "output.apk")
|
||||
private val signedFile = File(app.cacheDir, "signed.apk").also { if (it.exists()) it.delete() }
|
||||
private var hasSigned = false
|
||||
@ -59,23 +58,31 @@ class InstallerViewModel(
|
||||
|
||||
private val workManager = WorkManager.getInstance(app)
|
||||
|
||||
private val _progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
||||
app,
|
||||
selectedPatches.flatMap { (_, selected) -> selected }
|
||||
).toImmutableList())
|
||||
val progress = _progress.asStateFlow()
|
||||
private val _progress: MutableStateFlow<ImmutableList<Step>>
|
||||
private val patcherWorkerId: UUID
|
||||
|
||||
private val patcherWorkerId =
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
input.path!!.absolutePath,
|
||||
outputFile.path,
|
||||
selectedPatches,
|
||||
input.packageName,
|
||||
input.packageInfo!!.versionName,
|
||||
_progress
|
||||
init {
|
||||
val (appInfo, patches, options) = input
|
||||
|
||||
_progress = MutableStateFlow(PatcherProgressManager.generateSteps(
|
||||
app,
|
||||
patches.flatMap { (_, selected) -> selected }
|
||||
).toImmutableList())
|
||||
patcherWorkerId =
|
||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||
"patching", PatcherWorker.Args(
|
||||
appInfo.path!!.absolutePath,
|
||||
outputFile.path,
|
||||
patches,
|
||||
options,
|
||||
packageName,
|
||||
appInfo.packageInfo!!.versionName,
|
||||
_progress
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val progress = _progress.asStateFlow()
|
||||
|
||||
val patcherState =
|
||||
workManager.getWorkInfoByIdLiveData(patcherWorkerId).map { workInfo: WorkInfo ->
|
||||
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
@ -13,6 +14,7 @@ import app.revanced.manager.domain.repository.PatchSelectionRepository
|
||||
import app.revanced.manager.domain.repository.SourceRepository
|
||||
import app.revanced.manager.patcher.patch.PatchInfo
|
||||
import app.revanced.manager.util.AppInfo
|
||||
import app.revanced.manager.util.Options
|
||||
import app.revanced.manager.util.PatchesSelection
|
||||
import app.revanced.manager.util.SnapshotStateSet
|
||||
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||
@ -58,9 +60,13 @@ class PatchesSelectorViewModel(
|
||||
}
|
||||
|
||||
private val selectedPatches = mutableStateMapOf<Int, SnapshotStateSet<String>>()
|
||||
private val patchOptions =
|
||||
mutableStateMapOf<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>()
|
||||
|
||||
var showOptionsDialog by mutableStateOf(false)
|
||||
private set
|
||||
/**
|
||||
* Show the patch options dialog for this patch.
|
||||
*/
|
||||
var optionsDialog by mutableStateOf<Pair<Int, PatchInfo>?>(null)
|
||||
|
||||
val compatibleVersions = mutableStateListOf<String>()
|
||||
|
||||
@ -118,13 +124,20 @@ class PatchesSelectorViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissDialogs() {
|
||||
showOptionsDialog = false
|
||||
compatibleVersions.clear()
|
||||
fun getOptions(): Options = patchOptions
|
||||
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
|
||||
|
||||
fun setOption(bundle: Int, patch: PatchInfo, key: String, value: Any?) {
|
||||
patchOptions.getOrCreate(bundle).getOrCreate(patch.name)[key] = value
|
||||
}
|
||||
|
||||
fun openOptionsDialog() {
|
||||
showOptionsDialog = true
|
||||
fun unsetOption(bundle: Int, patch: PatchInfo, key: String) {
|
||||
patchOptions[bundle]?.get(patch.name)?.remove(key)
|
||||
}
|
||||
|
||||
fun dismissDialogs() {
|
||||
optionsDialog = null
|
||||
compatibleVersions.clear()
|
||||
}
|
||||
|
||||
fun openUnsupportedDialog(unsupportedVersions: List<PatchInfo>) {
|
||||
@ -148,6 +161,9 @@ class PatchesSelectorViewModel(
|
||||
const val SHOW_SUPPORTED = 1 // 2^0
|
||||
const val SHOW_UNIVERSAL = 2 // 2^1
|
||||
const val SHOW_UNSUPPORTED = 4 // 2^2
|
||||
|
||||
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
|
||||
getOrPut(key, ::mutableStateMapOf)
|
||||
}
|
||||
|
||||
data class BundleInfo(
|
||||
|
@ -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()
|
||||
}
|
@ -7,6 +7,7 @@ import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@ -19,8 +20,11 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
|
||||
typealias PatchesSelection = Map<Int, List<String>>
|
||||
typealias Options = Map<Int, Map<String, Map<String, Any?>>>
|
||||
|
||||
fun Context.openUrl(url: String) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, url.toUri()).apply {
|
||||
@ -92,4 +96,9 @@ inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
|
||||
combine(iterable.map(transformer)) {
|
||||
combiner(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val PathSaver = Saver<Path, String>(
|
||||
save = { it.toString() },
|
||||
restore = { Path(it) }
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user