Skip to content

Commit

Permalink
feat: patch options (ReVanced#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
Axelen123 authored Jul 3, 2023
1 parent 7ac3bb7 commit 01fd4c8
Show file tree
Hide file tree
Showing 17 changed files with 431 additions and 76 deletions.
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
15 changes: 6 additions & 9 deletions app/src/main/java/app/revanced/manager/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
)
)
},
Expand All @@ -101,12 +103,7 @@ class MainActivity : ComponentActivity() {
navigate(Destination.Dashboard)
}
},
vm = getViewModel {
parametersOf(
destination.input,
destination.selectedPatches
)
}
vm = getViewModel { parametersOf(destination) }
)
}
}
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/app/revanced/manager/data/platform/FileSystem.kt
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions app/src/main/java/app/revanced/manager/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,6 +13,7 @@ import org.koin.dsl.module
val repositoryModule = module {
singleOf(::ReVancedRepository)
singleOf(::ManagerAPI)
singleOf(::FileSystem)
singleOf(::SourcePersistenceRepository)
singleOf(::PatchSelectionRepository)
singleOf(::SourceRepository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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>>
Expand Down Expand Up @@ -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)

Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
}
Loading

0 comments on commit 01fd4c8

Please sign in to comment.