mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2024-11-10 01:01:56 +01:00
feat: add external process runtime (#1799)
This commit is contained in:
parent
5d7f9d1387
commit
ca49d3a465
28 changed files with 922 additions and 186 deletions
|
@ -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)
|
||||
|
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
|
@ -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.**
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// Parameters.aidl
|
||||
package app.revanced.manager.patcher.runtime.process;
|
||||
|
||||
parcelable Parameters;
|
38
app/src/main/cpp/CMakeLists.txt
Normal file
38
app/src/main/cpp/CMakeLists.txt
Normal file
|
@ -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)
|
62
app/src/main/cpp/prop_override.cpp
Normal file
62
app/src/main/cpp/prop_override.cpp
Normal file
|
@ -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 <string>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <dlfcn.h>
|
||||
|
||||
// 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<property_get_ptr>(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<GetProperty_ptr>(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME));
|
||||
}
|
||||
|
||||
return original(key, default_value);
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -111,6 +111,8 @@
|
|||
|
||||
<string name="options">Options</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="edit">Edit</string>
|
||||
<string name="dialog_input_placeholder">Value</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="patch">Patch</string>
|
||||
<string name="select_from_storage">Select from storage</string>
|
||||
|
@ -129,6 +131,10 @@
|
|||
<string name="dark">Dark</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="downloaded_apps">Downloaded apps</string>
|
||||
<string name="process_runtime">Run Patcher in another process (experimental)</string>
|
||||
<string name="process_runtime_description">This is faster and allows Patcher to use more memory.</string>
|
||||
<string name="process_runtime_memory_limit">Patcher process memory limit</string>
|
||||
<string name="process_runtime_memory_limit_description">The max amount of memory that the Patcher process can use (in megabytes)</string>
|
||||
<string name="api_url">API URL</string>
|
||||
<string name="api_url_dialog_title">Set custom API URL</string>
|
||||
<string name="api_url_dialog_description">You may have issues with features when using a custom API URL.</string>
|
||||
|
@ -218,9 +224,7 @@
|
|||
<string name="patch_selector_sheet_filter_title">Filter</string>
|
||||
<string name="patch_selector_sheet_filter_compat_title">Compatibility</string>
|
||||
|
||||
<string name="string_option_icon_description">Edit</string>
|
||||
<string name="string_option_menu_description">More options</string>
|
||||
<string name="string_option_placeholder">Value</string>
|
||||
|
||||
<string name="path_selector">Select from storage</string>
|
||||
<string name="path_selector_parent_dir">Previous directory</string>
|
||||
|
|
|
@ -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" }
|
||||
|
|
Loading…
Reference in a new issue