feat: add external process runtime (#1799)

This commit is contained in:
Ax333l
2024-03-29 16:00:52 +01:00
committed by oSumAtrIX
parent 9d961f6a52
commit 0d73e0cd32
28 changed files with 922 additions and 186 deletions

View File

@@ -13,6 +13,8 @@ class PreferencesManager(
val api = stringPreference("api_url", "https://api.revanced.app")
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
val useProcessRuntime = booleanPreference("use_process_runtime", false)
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)

View File

@@ -0,0 +1,10 @@
package app.revanced.manager.patcher
import android.content.Context
import java.io.File
abstract class LibraryResolver {
protected fun findLibrary(context: Context, searchTerm: String): File? = File(context.applicationInfo.nativeLibraryDir).run {
list { _, f -> !File(f).isDirectory && f.contains(searchTerm) }?.firstOrNull()?.let { resolve(it) }
}
}

View File

@@ -1,23 +1,21 @@
package app.revanced.manager.patcher
import android.content.Context
import app.revanced.library.ApkUtils
import app.revanced.library.ApkUtils.applyTo
import app.revanced.manager.R
import app.revanced.manager.patcher.logger.ManagerLogger
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.ui.model.State
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import java.io.Closeable
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.logging.Logger
internal typealias PatchList = List<Patch<*>>
@@ -27,9 +25,9 @@ class Session(
aaptPath: String,
multithreadingDexFileWriter: Boolean,
private val androidContext: Context,
private val logger: ManagerLogger,
private val input: File,
private val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
private val logger: Logger,
input: File,
private val onPatchCompleted: () -> Unit,
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
) : Closeable {
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
@@ -37,9 +35,9 @@ class Session(
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
private val patcher = Patcher(
PatcherOptions(
inputFile = input,
resourceCachePath = tempDir.resolve("aapt-resources"),
PatcherConfig(
apkFile = input,
temporaryFilesPath = tempDir,
frameworkFileDirectory = frameworkDir,
aaptBinaryPath = aaptPath,
multithreadingDexFileWriter = multithreadingDexFileWriter,
@@ -71,9 +69,7 @@ class Session(
nextPatchIndex++
patchesProgress.value.let {
patchesProgress.emit(it.copy(it.first + 1))
}
onPatchCompleted()
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
updateProgress(
@@ -96,14 +92,16 @@ class Session(
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) {
updateProgress(state = State.COMPLETED) // Unpacking
Logger.getLogger("").apply {
java.util.logging.Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
addHandler(logger)
addHandler(logger.handler)
}
with(patcher) {
logger.info("Merging integrations")
acceptIntegrations(integrations.toSet())

View File

@@ -1,18 +1,12 @@
package app.revanced.manager.patcher.aapt
import android.content.Context
import app.revanced.manager.patcher.LibraryResolver
import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS
import java.io.File
object Aapt {
object Aapt : LibraryResolver() {
private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64")
fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty()
fun binary(context: Context): File? {
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
}
fun binary(context: Context) = findLibrary(context, "aapt")
}
private fun File.resolveAapt() =
list { _, f -> !File(f).isDirectory && f.contains("aapt") }?.firstOrNull()?.let { resolve(it) }

View File

@@ -0,0 +1,37 @@
package app.revanced.manager.patcher.logger
import java.util.logging.Handler
import java.util.logging.Level
import java.util.logging.LogRecord
abstract class Logger {
abstract fun log(level: LogLevel, message: String)
fun trace(msg: String) = log(LogLevel.TRACE, msg)
fun info(msg: String) = log(LogLevel.INFO, msg)
fun warn(msg: String) = log(LogLevel.WARN, msg)
fun error(msg: String) = log(LogLevel.ERROR, msg)
val handler = object : Handler() {
override fun publish(record: LogRecord) {
val msg = record.message
when (record.level) {
Level.INFO -> info(msg)
Level.SEVERE -> error(msg)
Level.WARNING -> warn(msg)
else -> trace(msg)
}
}
override fun flush() = Unit
override fun close() = Unit
}
}
enum class LogLevel {
TRACE,
INFO,
WARN,
ERROR,
}

View File

@@ -1,59 +0,0 @@
package app.revanced.manager.patcher.logger
import android.util.Log
import java.util.logging.Handler
import java.util.logging.Level
import java.util.logging.LogRecord
class ManagerLogger : Handler() {
private val logs = mutableListOf<Pair<LogLevel, String>>()
private fun log(level: LogLevel, msg: String) {
level.androidLog(msg)
if (level == LogLevel.TRACE) return
logs.add(level to msg)
}
fun export() =
logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n")
fun trace(msg: String) = log(LogLevel.TRACE, msg)
fun info(msg: String) = log(LogLevel.INFO, msg)
fun warn(msg: String) = log(LogLevel.WARN, msg)
fun error(msg: String) = log(LogLevel.ERROR, msg)
override fun publish(record: LogRecord) {
val msg = record.message
val fn = when (record.level) {
Level.INFO -> ::info
Level.SEVERE -> ::error
Level.WARNING -> ::warn
else -> ::trace
}
fn(msg)
}
override fun flush() = Unit
override fun close() = Unit
}
enum class LogLevel {
TRACE {
override fun androidLog(msg: String) = Log.v(androidTag, msg)
},
INFO {
override fun androidLog(msg: String) = Log.i(androidTag, msg)
},
WARN {
override fun androidLog(msg: String) = Log.w(androidTag, msg)
},
ERROR {
override fun androidLog(msg: String) = Log.e(androidTag, msg)
};
abstract fun androidLog(msg: String): Int
private companion object {
const val androidTag = "ReVanced Patcher"
}
}

View File

@@ -6,19 +6,18 @@ import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch
import java.io.File
class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File?) {
constructor(bundleJar: File, integrations: File?) : this(
object : Iterable<Patch<*>> {
private fun load(): Iterable<Patch<*>> {
bundleJar.setReadOnly()
return PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
}
class PatchBundle(val patchesJar: File, val integrations: File?) {
private val loader = object : Iterable<Patch<*>> {
private fun load(): Iterable<Patch<*>> {
patchesJar.setReadOnly()
return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null)
}
override fun iterator(): Iterator<Patch<*>> = load().iterator()
},
integrations
) {
Log.d(tag, "Loaded patch bundle: $bundleJar")
override fun iterator(): Iterator<Patch<*>> = load().iterator()
}
init {
Log.d(tag, "Loaded patch bundle: $patchesJar")
}
/**

View File

@@ -0,0 +1,70 @@
package app.revanced.manager.patcher.runtime
import android.content.Context
import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.ProgressEventHandler
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import java.io.File
/**
* Simple [Runtime] implementation that runs the patcher using coroutines.
*/
class CoroutineRuntime(private val context: Context) : Runtime(context) {
override suspend fun execute(
inputFile: String,
outputFile: String,
packageName: String,
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
onPatchCompleted: () -> Unit,
onProgress: ProgressEventHandler,
) {
val bundles = bundles()
val selectedBundles = selectedPatches.keys
val allPatches = bundles.filterKeys { selectedBundles.contains(it) }
.mapValues { (_, bundle) -> bundle.patchClasses(packageName) }
val patchList = selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { selected.contains(it.name) }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
}
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
// Set all patch options.
options.forEach { (bundle, bundlePatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
val patchOptions = patches.single { it.name == patchName }.options
configuredPatchOptions.forEach { (key, value) ->
patchOptions[key] = value
}
}
}
onProgress(null, State.COMPLETED, null) // Loading patches
Session(
cacheDir,
frameworkPath,
aaptPath,
enableMultithreadedDexWriter(),
context,
logger,
File(inputFile),
onPatchCompleted = onPatchCompleted,
onProgress
).use { session ->
session.run(
File(outputFile),
patchList,
integrations
)
}
}
}

View File

@@ -0,0 +1,188 @@
package app.revanced.manager.patcher.runtime
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.core.content.ContextCompat
import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
import app.revanced.manager.patcher.LibraryResolver
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.runtime.process.Parameters
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
import app.revanced.manager.patcher.runtime.process.PatcherProcess
import app.revanced.manager.patcher.worker.ProgressEventHandler
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.tag
import com.github.pgreze.process.Redirect
import com.github.pgreze.process.process
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import org.koin.core.component.inject
/**
* Runs the patcher in another process by using the app_process binary and IPC.
*/
class ProcessRuntime(private val context: Context) : Runtime(context) {
private val pm: PM by inject()
private suspend fun awaitBinderConnection(): IPatcherProcess {
val binderFuture = CompletableDeferred<IPatcherProcess>()
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val binder =
intent.getBundleExtra(INTENT_BUNDLE_KEY)?.getBinder(BUNDLE_BINDER_KEY)!!
binderFuture.complete(IPatcherProcess.Stub.asInterface(binder))
}
}
ContextCompat.registerReceiver(context, receiver, IntentFilter().apply {
addAction(CONNECT_TO_APP_ACTION)
}, ContextCompat.RECEIVER_NOT_EXPORTED)
return try {
withTimeout(10000L) {
binderFuture.await()
}
} finally {
context.unregisterReceiver(receiver)
}
}
override suspend fun execute(
inputFile: String,
outputFile: String,
packageName: String,
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
onPatchCompleted: () -> Unit,
onProgress: ProgressEventHandler,
) = coroutineScope {
// Get the location of our own Apk.
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo.sourceDir
val limit = "${prefs.patcherProcessMemoryLimit.get()}M"
val propOverride = resolvePropOverride(context)?.absolutePath
?: throw Exception("Couldn't find prop override library")
val env =
System.getenv().toMutableMap().apply {
putAll(
mapOf(
"CLASSPATH" to managerBaseApk,
// Override the props used by ART to set the memory limit.
"LD_PRELOAD" to propOverride,
"PROP_dalvik.vm.heapgrowthlimit" to limit,
"PROP_dalvik.vm.heapsize" to limit,
)
)
}
launch(Dispatchers.IO) {
val result = process(
APP_PROCESS_BIN_PATH,
"-Djava.io.tmpdir=$cacheDir", // The process will use /tmp if this isn't set, which is a problem because that folder is not accessible on Android.
"/", // The unused cmd-dir parameter
"--nice-name=${context.packageName}:Patcher",
PatcherProcess::class.java.name, // The class with the main function.
context.packageName,
env = env,
stdout = Redirect.CAPTURE,
stderr = Redirect.CAPTURE,
) { line ->
// The process shouldn't generally be writing to stdio. Log any lines we get as warnings.
logger.warn("[STDIO]: $line")
}
Log.d(tag, "Process finished with exit code ${result.resultCode}")
if (result.resultCode != 0) throw Exception("Process exited with nonzero exit code ${result.resultCode}")
}
val patching = CompletableDeferred<Unit>()
launch(Dispatchers.IO) {
val binder = awaitBinderConnection()
// Android Studio's fast deployment feature causes an issue where the other process will be running older code compared to the main process.
// The patcher process is running outdated code if the randomly generated BUILD_ID numbers don't match.
// To fix it, clear the cache in the Android settings or disable fast deployment (Run configurations -> Edit Configurations -> app -> Enable "always deploy with package manager").
if (binder.buildId() != BuildConfig.BUILD_ID) throw Exception("app_process is running outdated code. Clear the app cache or disable disable Android 11 deployment optimizations in your IDE")
val eventHandler = object : IPatcherEvents.Stub() {
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
override fun patchSucceeded() = onPatchCompleted()
override fun progress(name: String?, state: String?, msg: String?) =
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
override fun finished(exceptionStackTrace: String?) {
binder.exit()
exceptionStackTrace?.let {
patching.completeExceptionally(RemoteFailureException(it))
return
}
patching.complete(Unit)
}
}
val bundles = bundles()
val parameters = Parameters(
aaptPath = aaptPath,
frameworkDir = frameworkPath,
cacheDir = cacheDir,
packageName = packageName,
inputFile = inputFile,
outputFile = outputFile,
enableMultithrededDexWriter = enableMultithreadedDexWriter(),
configurations = selectedPatches.map { (id, patches) ->
val bundle = bundles[id]!!
PatchConfiguration(
bundle.patchesJar.absolutePath,
bundle.integrations?.absolutePath,
patches,
options[id].orEmpty()
)
}
)
binder.start(parameters, eventHandler)
}
// Wait until patching finishes.
patching.await()
}
companion object : LibraryResolver() {
private const val APP_PROCESS_BIN_PATH = "/system/bin/app_process"
const val CONNECT_TO_APP_ACTION = "CONNECT_TO_APP_ACTION"
const val INTENT_BUNDLE_KEY = "BUNDLE"
const val BUNDLE_BINDER_KEY = "BINDER"
private fun resolvePropOverride(context: Context) = findLibrary(context, "prop_override")
}
/**
* An [Exception] occured in the remote process while patching.
*
* @param originalStackTrace The stack trace of the original [Exception].
*/
class RemoteFailureException(val originalStackTrace: String) : Exception()
}

View File

@@ -0,0 +1,41 @@
package app.revanced.manager.patcher.runtime
import android.content.Context
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.ProgressEventHandler
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.flow.first
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.FileNotFoundException
sealed class Runtime(context: Context) : KoinComponent {
private val fs: Filesystem by inject()
private val patchBundlesRepo: PatchBundleRepository by inject()
protected val prefs: PreferencesManager by inject()
protected val cacheDir: String = fs.tempDir.absolutePath
protected val aaptPath = Aapt.binary(context)?.absolutePath
?: throw FileNotFoundException("Could not resolve aapt.")
protected val frameworkPath: String =
context.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
protected suspend fun bundles() = patchBundlesRepo.bundles.first()
protected suspend fun enableMultithreadedDexWriter() = prefs.multithreadingDexFileWriter.get()
abstract suspend fun execute(
inputFile: String,
outputFile: String,
packageName: String,
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
onPatchCompleted: () -> Unit,
onProgress: ProgressEventHandler,
)
}

View File

@@ -0,0 +1,25 @@
package app.revanced.manager.patcher.runtime.process
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
@Parcelize
data class Parameters(
val cacheDir: String,
val aaptPath: String,
val frameworkDir: String,
val packageName: String,
val inputFile: String,
val outputFile: String,
val enableMultithrededDexWriter: Boolean,
val configurations: List<PatchConfiguration>,
) : Parcelable
@Parcelize
data class PatchConfiguration(
val bundlePath: String,
val integrationsPath: String?,
val patches: Set<String>,
val options: @RawValue Map<String, Map<String, Any?>>
) : Parcelable

View File

@@ -0,0 +1,126 @@
package app.revanced.manager.patcher.runtime.process
import android.app.ActivityThread
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Looper
import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.Session
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.patch.PatchBundle
import app.revanced.manager.patcher.runtime.ProcessRuntime
import app.revanced.manager.ui.model.State
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import kotlin.system.exitProcess
/**
* The main class that runs inside the runner process launched by [ProcessRuntime].
*/
class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
private var eventBinder: IPatcherEvents? = null
private val scope =
CoroutineScope(Dispatchers.Default + CoroutineExceptionHandler { _, throwable ->
// Try to send the exception information to the main app.
eventBinder?.let {
try {
it.finished(throwable.stackTraceToString())
return@CoroutineExceptionHandler
} catch (_: Exception) {
}
}
throwable.printStackTrace()
exitProcess(1)
})
override fun buildId() = BuildConfig.BUILD_ID
override fun exit() = exitProcess(0)
override fun start(parameters: Parameters, events: IPatcherEvents) {
eventBinder = events
scope.launch {
val logger = object : Logger() {
override fun log(level: LogLevel, message: String) =
events.log(level.name, message)
}
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
val integrations =
parameters.configurations.mapNotNull { it.integrationsPath?.let(::File) }
val patchList = parameters.configurations.flatMap { config ->
val bundle = PatchBundle(File(config.bundlePath), null)
val patches =
bundle.patchClasses(parameters.packageName).filter { it.name in config.patches }
.associateBy { it.name }
config.options.forEach { (patchName, opts) ->
val patchOptions = patches[patchName]?.options
?: throw Exception("Patch with name $patchName does not exist.")
opts.forEach { (key, value) ->
patchOptions[key] = value
}
}
patches.values
}
events.progress(null, State.COMPLETED.name, null) // Loading patches
Session(
cacheDir = parameters.cacheDir,
aaptPath = parameters.aaptPath,
frameworkDir = parameters.frameworkDir,
multithreadingDexFileWriter = parameters.enableMultithrededDexWriter,
androidContext = context,
logger = logger,
input = File(parameters.inputFile),
onPatchCompleted = { events.patchSucceeded() },
onProgress = { name, state, message ->
events.progress(name, state?.name, message)
}
).use {
it.run(File(parameters.outputFile), patchList, integrations)
}
events.finished(null)
}
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
Looper.prepare()
val managerPackageName = args[0]
// Abuse hidden APIs to get a context.
val systemContext = ActivityThread.systemMain().systemContext as Context
val appContext = systemContext.createPackageContext(managerPackageName, 0)
val ipcInterface = PatcherProcess(appContext)
appContext.sendBroadcast(Intent().apply {
action = ProcessRuntime.CONNECT_TO_APP_ACTION
`package` = managerPackageName
putExtra(ProcessRuntime.INTENT_BUNDLE_KEY, Bundle().apply {
putBinder(ProcessRuntime.BUNDLE_BINDER_KEY, ipcInterface.asBinder())
})
})
Looper.loop()
exitProcess(1) // Shouldn't happen
}
}
}

View File

@@ -23,12 +23,11 @@ import app.revanced.manager.domain.manager.KeystoreManager
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.DownloadedAppRepository
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.repository.PatchBundleRepository
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.patcher.logger.ManagerLogger
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.runtime.CoroutineRuntime
import app.revanced.manager.patcher.runtime.ProcessRuntime
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options
@@ -36,17 +35,17 @@ import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.tag
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import java.io.FileNotFoundException
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
class PatcherWorker(
context: Context,
parameters: WorkerParameters
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
private val patchBundleRepository: PatchBundleRepository by inject()
private val workerRepository: WorkerRepository by inject()
private val prefs: PreferencesManager by inject()
private val keystoreManager: KeystoreManager by inject()
@@ -61,11 +60,11 @@ class PatcherWorker(
val output: String,
val selectedPatches: PatchSelection,
val options: Options,
val logger: ManagerLogger,
val logger: Logger,
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
val setInputFile: (File) -> Unit,
val onProgress: (name: String?, state: State?, message: String?) -> Unit
val onProgress: ProgressEventHandler
) {
val packageName get() = input.packageName
}
@@ -111,7 +110,8 @@ class PatcherWorker(
val wakeLock: PowerManager.WakeLock =
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply {
.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher")
.apply {
acquire(10 * 60 * 1000L)
Log.d(tag, "Acquired wakelock.")
}
@@ -133,26 +133,6 @@ class PatcherWorker(
val patchedApk = fs.tempDir.resolve("patched.apk")
return try {
val bundles = patchBundleRepository.bundles.first()
// 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) }
val selectedPatches = args.selectedPatches.flatMap { (bundle, selected) ->
allPatches[bundle]?.filter { selected.contains(it.name) }
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
}
val aaptPath = Aapt.binary(applicationContext)?.absolutePath
?: throw FileNotFoundException("Could not resolve aapt.")
val frameworkPath =
applicationContext.cacheDir.resolve("framework").also { it.mkdirs() }.absolutePath
val integrations = bundles.mapNotNull { (_, bundle) -> bundle.integrations }
if (args.input is SelectedApp.Installed) {
installedAppRepository.get(args.packageName)?.let {
if (it.installType == InstallType.ROOT) {
@@ -161,19 +141,6 @@ class PatcherWorker(
}
}
// Set all patch options.
args.options.forEach { (bundle, bundlePatchOptions) ->
val patches = allPatches[bundle] ?: return@forEach
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
val patchOptions = patches.single { it.name == patchName }.options
configuredPatchOptions.forEach { (key, value) ->
patchOptions[key] = value
}
}
}
updateProgress(state = State.COMPLETED) // Loading patches
val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> {
downloadedAppRepository.download(
@@ -190,31 +157,38 @@ class PatcherWorker(
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
}
Session(
fs.tempDir.absolutePath,
frameworkPath,
aaptPath,
prefs.multithreadingDexFileWriter.get(),
applicationContext,
args.logger,
inputFile,
args.patchesProgress,
args.onProgress
).use { session ->
session.run(
patchedApk,
selectedPatches,
integrations
)
val runtime = if (prefs.useProcessRuntime.get()) {
ProcessRuntime(applicationContext)
} else {
CoroutineRuntime(applicationContext)
}
runtime.execute(
inputFile.absolutePath,
patchedApk.absolutePath,
args.packageName,
args.selectedPatches,
args.options,
args.logger,
onPatchCompleted = {
args.patchesProgress.update { (completed, total) ->
completed + 1 to total
}
},
args.onProgress
)
keystoreManager.sign(patchedApk, File(args.output))
updateProgress(state = State.COMPLETED) // Signing
Log.i(tag, "Patching succeeded".logFmt())
Result.success()
} catch (e: ProcessRuntime.RemoteFailureException) {
Log.e(tag, "An exception occured in the remote process while patching. ${e.originalStackTrace}".logFmt())
updateProgress(state = State.FAILED, message = e.originalStackTrace)
Result.failure()
} catch (e: Exception) {
Log.e(tag, "Exception while patching".logFmt(), e)
Log.e(tag, "An exception occured while patching".logFmt(), e)
updateProgress(state = State.FAILED, message = e.stackTraceToString())
Result.failure()
} finally {
@@ -223,7 +197,7 @@ class PatcherWorker(
}
companion object {
private const val logPrefix = "[Worker]:"
private fun String.logFmt() = "$logPrefix $this"
private const val LOG_PREFIX = "[Worker]"
private fun String.logFmt() = "$LOG_PREFIX $this"
}
}

View File

@@ -22,7 +22,6 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import app.revanced.manager.R
import app.revanced.manager.domain.bundles.LocalPatchBundle
import app.revanced.manager.domain.bundles.RemotePatchBundle
import app.revanced.manager.domain.bundles.PatchBundleSource
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault

View File

@@ -85,7 +85,7 @@ private fun StringOptionDialog(
value = fieldValue,
onValueChange = { fieldValue = it },
placeholder = {
Text(stringResource(R.string.string_option_placeholder))
Text(stringResource(R.string.dialog_input_placeholder))
},
trailingIcon = {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
@@ -184,7 +184,7 @@ private val optionImplementations = mapOf<String, OptionImpl>(
IconButton(onClick = ::showInputDialog) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.string_option_icon_description)
contentDescription = stringResource(R.string.edit)
)
}
}

View File

@@ -0,0 +1,121 @@
package app.revanced.manager.ui.component.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.revanced.manager.R
import app.revanced.manager.domain.manager.base.Preference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun IntegerItem(
modifier: Modifier = Modifier,
preference: Preference<Int>,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
@StringRes headline: Int,
@StringRes description: Int
) {
val value by preference.getAsState()
IntegerItem(
modifier = modifier,
value = value,
onValueChange = { coroutineScope.launch { preference.update(it) } },
headline = headline,
description = description
)
}
@Composable
fun IntegerItem(
modifier: Modifier = Modifier,
value: Int,
onValueChange: (Int) -> Unit,
@StringRes headline: Int,
@StringRes description: Int
) {
var dialogOpen by rememberSaveable {
mutableStateOf(false)
}
if (dialogOpen) {
IntegerItemDialog(current = value, name = headline) { new ->
dialogOpen = false
new?.let(onValueChange)
}
}
SettingsListItem(
modifier = Modifier
.clickable { dialogOpen = true }
.then(modifier),
headlineContent = stringResource(headline),
supportingContent = stringResource(description),
trailingContent = {
IconButton(onClick = { dialogOpen = true }) {
Icon(
Icons.Outlined.Edit,
contentDescription = stringResource(R.string.edit)
)
}
}
)
}
@Composable
private fun IntegerItemDialog(current: Int, @StringRes name: Int, onSubmit: (Int?) -> Unit) {
var fieldValue by rememberSaveable {
mutableStateOf(current.toString())
}
val integerFieldValue by remember {
derivedStateOf {
fieldValue.toIntOrNull()
}
}
AlertDialog(
onDismissRequest = { onSubmit(null) },
title = { Text(stringResource(name)) },
text = {
OutlinedTextField(
value = fieldValue,
onValueChange = { fieldValue = it },
placeholder = {
Text(stringResource(R.string.dialog_input_placeholder))
},
)
},
confirmButton = {
TextButton(
onClick = { integerFieldValue?.let(onSubmit) },
enabled = integerFieldValue != null,
) {
Text(stringResource(R.string.save))
}
},
dismissButton = {
TextButton(onClick = { onSubmit(null) }) {
Text(stringResource(R.string.cancel))
}
},
)
}

View File

@@ -73,11 +73,13 @@ fun PatcherScreen(
val progress by remember {
derivedStateOf {
val (patchesCompleted, patchesTotal) = patchesProgress
val current = vm.steps.count {
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
} + patchesProgress.first
} + patchesCompleted
val total = vm.steps.size - 1 + patchesProgress.second
val total = vm.steps.size - 1 + patchesTotal
current.toFloat() / total.toFloat()
}

View File

@@ -35,6 +35,7 @@ import app.revanced.manager.ui.component.AppTopBar
import app.revanced.manager.ui.component.ColumnWithScrollbar
import app.revanced.manager.ui.component.GroupHeader
import app.revanced.manager.ui.component.settings.BooleanItem
import app.revanced.manager.ui.component.settings.IntegerItem
import app.revanced.manager.ui.component.settings.SettingsListItem
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
import org.koin.androidx.compose.getViewModel
@@ -86,6 +87,18 @@ fun AdvancedSettingsScreen(
)
GroupHeader(stringResource(R.string.patcher))
BooleanItem(
preference = vm.prefs.useProcessRuntime,
coroutineScope = vm.viewModelScope,
headline = R.string.process_runtime,
description = R.string.process_runtime_description,
)
IntegerItem(
preference = vm.prefs.patcherProcessMemoryLimit,
coroutineScope = vm.viewModelScope,
headline = R.string.process_runtime_memory_limit,
description = R.string.process_runtime_memory_limit_description,
)
BooleanItem(
preference = vm.prefs.disablePatchVersionCompatCheck,
coroutineScope = vm.viewModelScope,

View File

@@ -26,7 +26,8 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.patcher.logger.ManagerLogger
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.service.InstallService
import app.revanced.manager.ui.destination.Destination
@@ -74,8 +75,17 @@ class PatcherViewModel(
private var inputFile: File? = null
private val outputFile = tempDir.resolve("output.apk")
private val workManager = WorkManager.getInstance(app)
private val logger = ManagerLogger()
private val logs = mutableListOf<Pair<LogLevel, String>>()
private val logger = object : Logger() {
override fun log(level: LogLevel, message: String) {
level.androidLog(message)
if (level == LogLevel.TRACE) return
viewModelScope.launch {
logs.add(level to message)
}
}
}
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
private val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
@@ -86,6 +96,8 @@ class PatcherViewModel(
).toMutableStateList()
private var currentStepIndex = 0
private val workManager = WorkManager.getInstance(app)
private val patcherWorkerId: UUID =
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
"patching", PatcherWorker.Args(
@@ -98,18 +110,21 @@ class PatcherViewModel(
patchesProgress,
setInputFile = { inputFile = it },
onProgress = { name, state, message ->
steps[currentStepIndex] = steps[currentStepIndex].run {
copy(
name = name ?: this.name,
state = state ?: this.state,
message = message ?: this.message
)
}
viewModelScope.launch {
steps[currentStepIndex] = steps[currentStepIndex].run {
copy(
name = name ?: this.name,
state = state ?: this.state,
message = message ?: this.message
)
}
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
currentStepIndex++
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
currentStepIndex++
steps[currentStepIndex] = steps[currentStepIndex].copy(state = State.RUNNING)
steps[currentStepIndex] =
steps[currentStepIndex].copy(state = State.RUNNING)
}
}
}
)
@@ -204,7 +219,10 @@ class PatcherViewModel(
fun exportLogs(context: Context) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, logger.export())
putExtra(
Intent.EXTRA_TEXT,
logs.asSequence().map { (level, msg) -> "[${level.name}]: $msg" }.joinToString("\n")
)
type = "text/plain"
}
@@ -255,7 +273,8 @@ class PatcherViewModel(
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
try {
rootInstaller.uninstall(packageName)
} catch (_: Exception) { }
} catch (_: Exception) {
}
}
}
}
@@ -265,22 +284,34 @@ class PatcherViewModel(
}
companion object {
private const val TAG = "ReVanced Patcher"
fun LogLevel.androidLog(msg: String) = when (this) {
LogLevel.TRACE -> Log.v(TAG, msg)
LogLevel.INFO -> Log.i(TAG, msg)
LogLevel.WARN -> Log.w(TAG, msg)
LogLevel.ERROR -> Log.e(TAG, msg)
}
fun generateSteps(
context: Context,
selectedApp: SelectedApp,
downloadProgress: StateFlow<Pair<Float, Float>?>? = null
): List<Step> {
val needsDownload = selectedApp is SelectedApp.Download
return listOfNotNull(
Step(
context.getString(R.string.patcher_step_load_patches),
StepCategory.PREPARING,
state = State.RUNNING
),
Step(
context.getString(R.string.download_apk),
StepCategory.PREPARING,
downloadProgress = downloadProgress
).takeIf { selectedApp is SelectedApp.Download },
state = State.RUNNING,
downloadProgress = downloadProgress,
).takeIf { needsDownload },
Step(
context.getString(R.string.patcher_step_load_patches),
StepCategory.PREPARING,
state = if (needsDownload) State.WAITING else State.RUNNING,
),
Step(
context.getString(R.string.patcher_step_unpack),
StepCategory.PREPARING