diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2330564..994f98ca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import kotlin.random.Random + plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -28,6 +30,8 @@ android { debug { applicationIdSuffix = ".debug" resValue("string", "app_name", "ReVanced Manager Debug") + + buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") } release { @@ -42,6 +46,8 @@ android { resValue("string", "app_name", "ReVanced Manager Debug") signingConfig = signingConfigs.getByName("debug") } + + buildConfigField("long", "BUILD_ID", "0L") } } @@ -83,6 +89,12 @@ android { buildFeatures.buildConfig=true composeOptions.kotlinCompilerExtensionVersion = "1.5.10" + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } } kotlin { @@ -137,6 +149,13 @@ dependencies { implementation(libs.revanced.patcher) implementation(libs.revanced.library) + // Native processes + implementation(libs.kotlin.process) + + // HiddenAPI + compileOnly(libs.hidden.api.stub) + + // LibSU implementation(libs.libsu.core) implementation(libs.libsu.service) implementation(libs.libsu.nio) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f3984a94..f284b52a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -26,6 +26,10 @@ kotlinx.serialization.KSerializer serializer(...); } +# This required for the process runtime. +-keep class app.revanced.manager.patcher.runtime.process.* { + *; +} # Required for the patcher to function correctly -keep class app.revanced.patcher.** { *; @@ -45,6 +49,7 @@ -keep class com.android.** { *; } +-dontwarn com.google.auto.value.** -dontwarn java.awt.** -dontwarn javax.** -dontwarn org.slf4j.** diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl new file mode 100644 index 00000000..27a4f61b --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl @@ -0,0 +1,11 @@ +// IPatcherEvents.aidl +package app.revanced.manager.patcher.runtime.process; + +// Interface for sending events back to the main app process. +oneway interface IPatcherEvents { + void log(String level, String msg); + void patchSucceeded(); + void progress(String name, String state, String msg); + // The patching process has ended. The exceptionStackTrace is null if it finished successfully. + void finished(String exceptionStackTrace); +} \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl new file mode 100644 index 00000000..f938ca62 --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherProcess.aidl @@ -0,0 +1,14 @@ +// IPatcherProcess.aidl +package app.revanced.manager.patcher.runtime.process; + +import app.revanced.manager.patcher.runtime.process.Parameters; +import app.revanced.manager.patcher.runtime.process.IPatcherEvents; + +interface IPatcherProcess { + // Returns BuildConfig.BUILD_ID, which is used to ensure the main app and runner process are running the same code. + long buildId(); + // Makes the patcher process exit with code 0 + oneway void exit(); + // Starts patching. + oneway void start(in Parameters parameters, IPatcherEvents events); +} \ No newline at end of file diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl new file mode 100644 index 00000000..a1e8bee7 --- /dev/null +++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/Parameters.aidl @@ -0,0 +1,4 @@ +// Parameters.aidl +package app.revanced.manager.patcher.runtime.process; + +parcelable Parameters; \ No newline at end of file diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..64793f8f --- /dev/null +++ b/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,38 @@ + +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html. +# For more examples on how to use CMake, see https://github.com/android/ndk-samples. + +# Sets the minimum CMake version required for this project. +cmake_minimum_required(VERSION 3.22.1) + +# Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, +# Since this is the top level CMakeLists.txt, the project name is also accessible +# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level +# build script scope). +project("prop_override") + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. +# +# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define +# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} +# is preferred for the same purpose. +# +# In order to load a library into your app from Java/Kotlin, you must call +# System.loadLibrary() and pass the name of the library defined here; +# for GameActivity/NativeActivity derived applications, the same library name must be +# used in the AndroidManifest.xml file. +add_library(${CMAKE_PROJECT_NAME} SHARED + # List C/C++ source files with relative paths to this CMakeLists.txt. + prop_override.cpp) + +# Specifies libraries CMake should link to your target library. You +# can link libraries from various origins, such as libraries defined in this +# build script, prebuilt third-party libraries, or Android system libraries. +target_link_libraries(${CMAKE_PROJECT_NAME} + # List libraries link to the target library + android + log) diff --git a/app/src/main/cpp/prop_override.cpp b/app/src/main/cpp/prop_override.cpp new file mode 100644 index 00000000..b314ccd1 --- /dev/null +++ b/app/src/main/cpp/prop_override.cpp @@ -0,0 +1,62 @@ +// Library for overriding Android system properties via environment variables. +// +// Usage: LD_PRELOAD=prop_override.so PROP_dalvik.vm.heapsize=123M getprop dalvik.vm.heapsize +// Output: 123M +#include +#include +#include +#include + +// Source: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/include/cutils/properties.h +#define PROP_VALUE_MAX 92 +// This is the mangled name of "android::base::GetProperty". +#define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_" + +extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *); +typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &); + +char *GetPropOverride(const std::string &key) { + auto envKey = "PROP_" + key; + + return getenv(envKey.c_str()); +} + +// See: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/properties.cpp +extern "C" int property_get(const char *key, char *value, const char *default_value) { + auto replacement = GetPropOverride(std::string(key)); + if (replacement) { + int len = strnlen(replacement, PROP_VALUE_MAX); + + strncpy(value, replacement, len); + return len; + } + + static property_get_ptr original = NULL; + if (!original) { + // Get the address of the original function. + original = reinterpret_cast(dlsym(RTLD_NEXT, "property_get")); + } + + return original(key, value, default_value); +} + +// Defining android::base::GetProperty ourselves won't work because std::string has a slightly different "path" in the NDK version of the C++ standard library. +// We can get around this by forcing the function to adopt a specific name using the asm keyword. +std::string GetProperty(const std::string &, const std::string &) asm(GET_PROPERTY_MANGLED_NAME); + + +// See: https://android.googlesource.com/platform/system/libbase/+/1a34bb67c4f3ba0a1ea6f4f20ac9fe117ba4fe64/properties.cpp +// This isn't used for the properties we want to override, but property_get is deprecated so that could change in the future. +std::string GetProperty(const std::string &key, const std::string &default_value) { + auto replacement = GetPropOverride(key); + if (replacement) { + return std::string(replacement); + } + + static GetProperty_ptr original = NULL; + if (!original) { + original = reinterpret_cast(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME)); + } + + return original(key, default_value); +} diff --git a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt index cebcdcd7..dc6d713c 100644 --- a/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt +++ b/app/src/main/java/app/revanced/manager/domain/manager/PreferencesManager.kt @@ -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) diff --git a/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt b/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt new file mode 100644 index 00000000..e0fe293f --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/LibraryResolver.kt @@ -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) } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt index 0ee323d2..1eff6ade 100644 --- a/app/src/main/java/app/revanced/manager/patcher/Session.kt +++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt @@ -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> @@ -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>, + 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) { 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()) diff --git a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt index 959768e6..f81ba2f4 100644 --- a/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt +++ b/app/src/main/java/app/revanced/manager/patcher/aapt/Aapt.kt @@ -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) } diff --git a/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt b/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt new file mode 100644 index 00000000..88f2a133 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/logger/Logger.kt @@ -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, +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/logger/ManagerLogger.kt b/app/src/main/java/app/revanced/manager/patcher/logger/ManagerLogger.kt deleted file mode 100644 index 03a741a4..00000000 --- a/app/src/main/java/app/revanced/manager/patcher/logger/ManagerLogger.kt +++ /dev/null @@ -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>() - 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" - } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt index 6da4fab4..a463998f 100644 --- a/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt +++ b/app/src/main/java/app/revanced/manager/patcher/patch/PatchBundle.kt @@ -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>, val integrations: File?) { - constructor(bundleJar: File, integrations: File?) : this( - object : Iterable> { - private fun load(): Iterable> { - bundleJar.setReadOnly() - return PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null) - } +class PatchBundle(val patchesJar: File, val integrations: File?) { + private val loader = object : Iterable> { + private fun load(): Iterable> { + patchesJar.setReadOnly() + return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null) + } - override fun iterator(): Iterator> = load().iterator() - }, - integrations - ) { - Log.d(tag, "Loaded patch bundle: $bundleJar") + override fun iterator(): Iterator> = load().iterator() + } + + init { + Log.d(tag, "Loaded patch bundle: $patchesJar") } /** diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt new file mode 100644 index 00000000..e2aed2ee --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt @@ -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 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt new file mode 100644 index 00000000..389d5201 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt @@ -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() + 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() + + 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(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() +} + diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt new file mode 100644 index 00000000..fd39c3f3 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt @@ -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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt new file mode 100644 index 00000000..c669c875 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/Parameters.kt @@ -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, +) : Parcelable + +@Parcelize +data class PatchConfiguration( + val bundlePath: String, + val integrationsPath: String?, + val patches: Set, + val options: @RawValue Map> +) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt new file mode 100644 index 00000000..4467f3ae --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt @@ -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) { + 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 + } + } +} \ 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 d59e5a5c..d825a18e 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 @@ -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(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?>, val patchesProgress: MutableStateFlow>, 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" } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt index d3b2744c..59b119f3 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/bundle/BundleInformationDialog.kt @@ -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 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 index 8f871afa..7647eccb 100644 --- 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 @@ -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( IconButton(onClick = ::showInputDialog) { Icon( Icons.Outlined.Edit, - contentDescription = stringResource(R.string.string_option_icon_description) + contentDescription = stringResource(R.string.edit) ) } } diff --git a/app/src/main/java/app/revanced/manager/ui/component/settings/IntegerItem.kt b/app/src/main/java/app/revanced/manager/ui/component/settings/IntegerItem.kt new file mode 100644 index 00000000..24874489 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/component/settings/IntegerItem.kt @@ -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, + 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)) + } + }, + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt index 882d8cfe..163dfbc6 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt @@ -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() } diff --git a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt index 5063b9b7..cf9aac71 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/settings/AdvancedSettingsScreen.kt @@ -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, diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt index d1b3d360..8b504dfb 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt @@ -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>() + 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?>(null) @@ -86,6 +96,8 @@ class PatcherViewModel( ).toMutableStateList() private var currentStepIndex = 0 + private val workManager = WorkManager.getInstance(app) + private val patcherWorkerId: UUID = workerRepository.launchExpedited( "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?>? = null ): List { + 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 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dab22c3d..04a072ce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,8 @@ Options OK + Edit + Value Reset Patch Select from storage @@ -129,6 +131,10 @@ Dark Appearance Downloaded apps + Run Patcher in another process (experimental) + This is faster and allows Patcher to use more memory. + Patcher process memory limit + The max amount of memory that the Patcher process can use (in megabytes) API URL Set custom API URL You may have issues with features when using a custom API URL. @@ -218,9 +224,7 @@ Filter Compatibility - Edit More options - Value Select from storage Previous directory diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5ec3f484..d66d9bb1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,8 @@ skrapeit = "1.2.2" libsu = "5.2.1" scrollbars = "1.0.4" compose-icons = "1.2.4" +kotlin-process = "1.4.1" +hidden-api-stub = "4.3.3" [libraries] # AndroidX Core @@ -102,6 +104,12 @@ markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-ren # Fading Edges fading-edges = { group = "com.github.GIGAMOLE", name = "ComposeFadingEdges", version.ref = "fading-edges"} +# Native processes +kotlin-process = { group = "com.github.pgreze", name = "kotlin-process", version.ref = "kotlin-process" } + +# HiddenAPI +hidden-api-stub = { group = "dev.rikka.hidden", name = "stub", version.ref = "hidden-api-stub" } + # LibSU libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }