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 {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
@ -28,6 +30,8 @@ android {
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
resValue("string", "app_name", "ReVanced Manager Debug")
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||||
}
|
}
|
||||||
|
|
||||||
release {
|
release {
|
||||||
|
@ -42,6 +46,8 @@ android {
|
||||||
resValue("string", "app_name", "ReVanced Manager Debug")
|
resValue("string", "app_name", "ReVanced Manager Debug")
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "0L")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +89,12 @@ android {
|
||||||
buildFeatures.buildConfig=true
|
buildFeatures.buildConfig=true
|
||||||
|
|
||||||
composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
|
composeOptions.kotlinCompilerExtensionVersion = "1.5.10"
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
|
version = "3.22.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@ -137,6 +149,13 @@ dependencies {
|
||||||
implementation(libs.revanced.patcher)
|
implementation(libs.revanced.patcher)
|
||||||
implementation(libs.revanced.library)
|
implementation(libs.revanced.library)
|
||||||
|
|
||||||
|
// Native processes
|
||||||
|
implementation(libs.kotlin.process)
|
||||||
|
|
||||||
|
// HiddenAPI
|
||||||
|
compileOnly(libs.hidden.api.stub)
|
||||||
|
|
||||||
|
// LibSU
|
||||||
implementation(libs.libsu.core)
|
implementation(libs.libsu.core)
|
||||||
implementation(libs.libsu.service)
|
implementation(libs.libsu.service)
|
||||||
implementation(libs.libsu.nio)
|
implementation(libs.libsu.nio)
|
||||||
|
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
|
@ -26,6 +26,10 @@
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
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
|
# Required for the patcher to function correctly
|
||||||
-keep class app.revanced.patcher.** {
|
-keep class app.revanced.patcher.** {
|
||||||
*;
|
*;
|
||||||
|
@ -45,6 +49,7 @@
|
||||||
-keep class com.android.** {
|
-keep class com.android.** {
|
||||||
*;
|
*;
|
||||||
}
|
}
|
||||||
|
-dontwarn com.google.auto.value.**
|
||||||
-dontwarn java.awt.**
|
-dontwarn java.awt.**
|
||||||
-dontwarn javax.**
|
-dontwarn javax.**
|
||||||
-dontwarn org.slf4j.**
|
-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 api = stringPreference("api_url", "https://api.revanced.app")
|
||||||
|
|
||||||
val multithreadingDexFileWriter = booleanPreference("multithreading_dex_file_writer", true)
|
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 disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||||
|
|
||||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
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
|
package app.revanced.manager.patcher
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import app.revanced.library.ApkUtils
|
|
||||||
import app.revanced.library.ApkUtils.applyTo
|
import app.revanced.library.ApkUtils.applyTo
|
||||||
import app.revanced.manager.R
|
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.manager.ui.model.State
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
|
import app.revanced.patcher.PatcherConfig
|
||||||
import app.revanced.patcher.PatcherOptions
|
import app.revanced.patcher.PatcherOptions
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.PatchResult
|
import app.revanced.patcher.patch.PatchResult
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
import java.util.logging.Logger
|
|
||||||
|
|
||||||
internal typealias PatchList = List<Patch<*>>
|
internal typealias PatchList = List<Patch<*>>
|
||||||
|
|
||||||
|
@ -27,9 +25,9 @@ class Session(
|
||||||
aaptPath: String,
|
aaptPath: String,
|
||||||
multithreadingDexFileWriter: Boolean,
|
multithreadingDexFileWriter: Boolean,
|
||||||
private val androidContext: Context,
|
private val androidContext: Context,
|
||||||
private val logger: ManagerLogger,
|
private val logger: Logger,
|
||||||
private val input: File,
|
input: File,
|
||||||
private val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
private val onPatchCompleted: () -> Unit,
|
||||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
||||||
) : Closeable {
|
) : Closeable {
|
||||||
private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
|
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 tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||||
private val patcher = Patcher(
|
private val patcher = Patcher(
|
||||||
PatcherOptions(
|
PatcherConfig(
|
||||||
inputFile = input,
|
apkFile = input,
|
||||||
resourceCachePath = tempDir.resolve("aapt-resources"),
|
temporaryFilesPath = tempDir,
|
||||||
frameworkFileDirectory = frameworkDir,
|
frameworkFileDirectory = frameworkDir,
|
||||||
aaptBinaryPath = aaptPath,
|
aaptBinaryPath = aaptPath,
|
||||||
multithreadingDexFileWriter = multithreadingDexFileWriter,
|
multithreadingDexFileWriter = multithreadingDexFileWriter,
|
||||||
|
@ -71,9 +69,7 @@ class Session(
|
||||||
|
|
||||||
nextPatchIndex++
|
nextPatchIndex++
|
||||||
|
|
||||||
patchesProgress.value.let {
|
onPatchCompleted()
|
||||||
patchesProgress.emit(it.copy(it.first + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
||||||
updateProgress(
|
updateProgress(
|
||||||
|
@ -96,14 +92,16 @@ class Session(
|
||||||
|
|
||||||
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) {
|
suspend fun run(output: File, selectedPatches: PatchList, integrations: List<File>) {
|
||||||
updateProgress(state = State.COMPLETED) // Unpacking
|
updateProgress(state = State.COMPLETED) // Unpacking
|
||||||
Logger.getLogger("").apply {
|
|
||||||
|
java.util.logging.Logger.getLogger("").apply {
|
||||||
handlers.forEach {
|
handlers.forEach {
|
||||||
it.close()
|
it.close()
|
||||||
removeHandler(it)
|
removeHandler(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
addHandler(logger)
|
addHandler(logger.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
with(patcher) {
|
with(patcher) {
|
||||||
logger.info("Merging integrations")
|
logger.info("Merging integrations")
|
||||||
acceptIntegrations(integrations.toSet())
|
acceptIntegrations(integrations.toSet())
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
package app.revanced.manager.patcher.aapt
|
package app.revanced.manager.patcher.aapt
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import app.revanced.manager.patcher.LibraryResolver
|
||||||
import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS
|
import android.os.Build.SUPPORTED_ABIS as DEVICE_ABIS
|
||||||
import java.io.File
|
object Aapt : LibraryResolver() {
|
||||||
|
|
||||||
object Aapt {
|
|
||||||
private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64")
|
private val WORKING_ABIS = setOf("arm64-v8a", "x86", "x86_64")
|
||||||
|
|
||||||
fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty()
|
fun supportsDevice() = (DEVICE_ABIS intersect WORKING_ABIS).isNotEmpty()
|
||||||
|
|
||||||
fun binary(context: Context): File? {
|
fun binary(context: Context) = findLibrary(context, "aapt")
|
||||||
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 app.revanced.patcher.patch.Patch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class PatchBundle(private val loader: Iterable<Patch<*>>, val integrations: File?) {
|
class PatchBundle(val patchesJar: File, val integrations: File?) {
|
||||||
constructor(bundleJar: File, integrations: File?) : this(
|
private val loader = object : Iterable<Patch<*>> {
|
||||||
object : Iterable<Patch<*>> {
|
|
||||||
private fun load(): Iterable<Patch<*>> {
|
private fun load(): Iterable<Patch<*>> {
|
||||||
bundleJar.setReadOnly()
|
patchesJar.setReadOnly()
|
||||||
return PatchBundleLoader.Dex(bundleJar, optimizedDexDirectory = null)
|
return PatchBundleLoader.Dex(patchesJar, optimizedDexDirectory = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
override fun iterator(): Iterator<Patch<*>> = load().iterator()
|
||||||
},
|
}
|
||||||
integrations
|
|
||||||
) {
|
init {
|
||||||
Log.d(tag, "Loaded patch bundle: $bundleJar")
|
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.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
import app.revanced.manager.domain.repository.DownloadedAppRepository
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
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.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.aapt.Aapt
|
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||||
import app.revanced.manager.patcher.logger.ManagerLogger
|
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
import app.revanced.manager.ui.model.State
|
||||||
import app.revanced.manager.util.Options
|
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.PatchSelection
|
||||||
import app.revanced.manager.util.tag
|
import app.revanced.manager.util.tag
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
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.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
|
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
||||||
|
|
||||||
class PatcherWorker(
|
class PatcherWorker(
|
||||||
context: Context,
|
context: Context,
|
||||||
parameters: WorkerParameters
|
parameters: WorkerParameters
|
||||||
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
) : Worker<PatcherWorker.Args>(context, parameters), KoinComponent {
|
||||||
private val patchBundleRepository: PatchBundleRepository by inject()
|
|
||||||
private val workerRepository: WorkerRepository by inject()
|
private val workerRepository: WorkerRepository by inject()
|
||||||
private val prefs: PreferencesManager by inject()
|
private val prefs: PreferencesManager by inject()
|
||||||
private val keystoreManager: KeystoreManager by inject()
|
private val keystoreManager: KeystoreManager by inject()
|
||||||
|
@ -61,11 +60,11 @@ class PatcherWorker(
|
||||||
val output: String,
|
val output: String,
|
||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
val logger: ManagerLogger,
|
val logger: Logger,
|
||||||
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
|
val downloadProgress: MutableStateFlow<Pair<Float, Float>?>,
|
||||||
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
val patchesProgress: MutableStateFlow<Pair<Int, Int>>,
|
||||||
val setInputFile: (File) -> Unit,
|
val setInputFile: (File) -> Unit,
|
||||||
val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
val onProgress: ProgressEventHandler
|
||||||
) {
|
) {
|
||||||
val packageName get() = input.packageName
|
val packageName get() = input.packageName
|
||||||
}
|
}
|
||||||
|
@ -111,7 +110,8 @@ class PatcherWorker(
|
||||||
|
|
||||||
val wakeLock: PowerManager.WakeLock =
|
val wakeLock: PowerManager.WakeLock =
|
||||||
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
(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)
|
acquire(10 * 60 * 1000L)
|
||||||
Log.d(tag, "Acquired wakelock.")
|
Log.d(tag, "Acquired wakelock.")
|
||||||
}
|
}
|
||||||
|
@ -133,26 +133,6 @@ class PatcherWorker(
|
||||||
val patchedApk = fs.tempDir.resolve("patched.apk")
|
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||||
|
|
||||||
return try {
|
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) {
|
if (args.input is SelectedApp.Installed) {
|
||||||
installedAppRepository.get(args.packageName)?.let {
|
installedAppRepository.get(args.packageName)?.let {
|
||||||
if (it.installType == InstallType.ROOT) {
|
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) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
is SelectedApp.Download -> {
|
||||||
downloadedAppRepository.download(
|
downloadedAppRepository.download(
|
||||||
|
@ -190,31 +157,38 @@ class PatcherWorker(
|
||||||
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
|
is SelectedApp.Installed -> File(pm.getPackageInfo(selectedApp.packageName)!!.applicationInfo.sourceDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
Session(
|
val runtime = if (prefs.useProcessRuntime.get()) {
|
||||||
fs.tempDir.absolutePath,
|
ProcessRuntime(applicationContext)
|
||||||
frameworkPath,
|
} else {
|
||||||
aaptPath,
|
CoroutineRuntime(applicationContext)
|
||||||
prefs.multithreadingDexFileWriter.get(),
|
|
||||||
applicationContext,
|
|
||||||
args.logger,
|
|
||||||
inputFile,
|
|
||||||
args.patchesProgress,
|
|
||||||
args.onProgress
|
|
||||||
).use { session ->
|
|
||||||
session.run(
|
|
||||||
patchedApk,
|
|
||||||
selectedPatches,
|
|
||||||
integrations
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
keystoreManager.sign(patchedApk, File(args.output))
|
||||||
updateProgress(state = State.COMPLETED) // Signing
|
updateProgress(state = State.COMPLETED) // Signing
|
||||||
|
|
||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
Result.success()
|
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) {
|
} 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())
|
updateProgress(state = State.FAILED, message = e.stackTraceToString())
|
||||||
Result.failure()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -223,7 +197,7 @@ class PatcherWorker(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val logPrefix = "[Worker]:"
|
private const val LOG_PREFIX = "[Worker]"
|
||||||
private fun String.logFmt() = "$logPrefix $this"
|
private fun String.logFmt() = "$LOG_PREFIX $this"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -22,7 +22,6 @@ import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
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
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.asRemoteOrNull
|
||||||
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault
|
import app.revanced.manager.domain.bundles.PatchBundleSource.Companion.isDefault
|
||||||
|
|
|
@ -85,7 +85,7 @@ private fun StringOptionDialog(
|
||||||
value = fieldValue,
|
value = fieldValue,
|
||||||
onValueChange = { fieldValue = it },
|
onValueChange = { fieldValue = it },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(stringResource(R.string.string_option_placeholder))
|
Text(stringResource(R.string.dialog_input_placeholder))
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
|
||||||
|
@ -184,7 +184,7 @@ private val optionImplementations = mapOf<String, OptionImpl>(
|
||||||
IconButton(onClick = ::showInputDialog) {
|
IconButton(onClick = ::showInputDialog) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Outlined.Edit,
|
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 {
|
val progress by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
|
val (patchesCompleted, patchesTotal) = patchesProgress
|
||||||
|
|
||||||
val current = vm.steps.count {
|
val current = vm.steps.count {
|
||||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
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()
|
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.ColumnWithScrollbar
|
||||||
import app.revanced.manager.ui.component.GroupHeader
|
import app.revanced.manager.ui.component.GroupHeader
|
||||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
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.component.settings.SettingsListItem
|
||||||
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
import app.revanced.manager.ui.viewmodel.AdvancedSettingsViewModel
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
@ -86,6 +87,18 @@ fun AdvancedSettingsScreen(
|
||||||
)
|
)
|
||||||
|
|
||||||
GroupHeader(stringResource(R.string.patcher))
|
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(
|
BooleanItem(
|
||||||
preference = vm.prefs.disablePatchVersionCompatCheck,
|
preference = vm.prefs.disablePatchVersionCompatCheck,
|
||||||
coroutineScope = vm.viewModelScope,
|
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.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
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.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.service.InstallService
|
import app.revanced.manager.service.InstallService
|
||||||
import app.revanced.manager.ui.destination.Destination
|
import app.revanced.manager.ui.destination.Destination
|
||||||
|
@ -74,8 +75,17 @@ class PatcherViewModel(
|
||||||
private var inputFile: File? = null
|
private var inputFile: File? = null
|
||||||
private val outputFile = tempDir.resolve("output.apk")
|
private val outputFile = tempDir.resolve("output.apk")
|
||||||
|
|
||||||
private val workManager = WorkManager.getInstance(app)
|
private val logs = mutableListOf<Pair<LogLevel, String>>()
|
||||||
private val logger = ManagerLogger()
|
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 }))
|
val patchesProgress = MutableStateFlow(Pair(0, input.selectedPatches.values.sumOf { it.size }))
|
||||||
private val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
|
private val downloadProgress = MutableStateFlow<Pair<Float, Float>?>(null)
|
||||||
|
@ -86,6 +96,8 @@ class PatcherViewModel(
|
||||||
).toMutableStateList()
|
).toMutableStateList()
|
||||||
private var currentStepIndex = 0
|
private var currentStepIndex = 0
|
||||||
|
|
||||||
|
private val workManager = WorkManager.getInstance(app)
|
||||||
|
|
||||||
private val patcherWorkerId: UUID =
|
private val patcherWorkerId: UUID =
|
||||||
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
"patching", PatcherWorker.Args(
|
"patching", PatcherWorker.Args(
|
||||||
|
@ -98,6 +110,7 @@ class PatcherViewModel(
|
||||||
patchesProgress,
|
patchesProgress,
|
||||||
setInputFile = { inputFile = it },
|
setInputFile = { inputFile = it },
|
||||||
onProgress = { name, state, message ->
|
onProgress = { name, state, message ->
|
||||||
|
viewModelScope.launch {
|
||||||
steps[currentStepIndex] = steps[currentStepIndex].run {
|
steps[currentStepIndex] = steps[currentStepIndex].run {
|
||||||
copy(
|
copy(
|
||||||
name = name ?: this.name,
|
name = name ?: this.name,
|
||||||
|
@ -109,7 +122,9 @@ class PatcherViewModel(
|
||||||
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
|
||||||
currentStepIndex++
|
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) {
|
fun exportLogs(context: Context) {
|
||||||
val sendIntent: Intent = Intent().apply {
|
val sendIntent: Intent = Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
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"
|
type = "text/plain"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +273,8 @@ class PatcherViewModel(
|
||||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
||||||
try {
|
try {
|
||||||
rootInstaller.uninstall(packageName)
|
rootInstaller.uninstall(packageName)
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,22 +284,34 @@ class PatcherViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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(
|
fun generateSteps(
|
||||||
context: Context,
|
context: Context,
|
||||||
selectedApp: SelectedApp,
|
selectedApp: SelectedApp,
|
||||||
downloadProgress: StateFlow<Pair<Float, Float>?>? = null
|
downloadProgress: StateFlow<Pair<Float, Float>?>? = null
|
||||||
): List<Step> {
|
): List<Step> {
|
||||||
|
val needsDownload = selectedApp is SelectedApp.Download
|
||||||
|
|
||||||
return listOfNotNull(
|
return listOfNotNull(
|
||||||
Step(
|
|
||||||
context.getString(R.string.patcher_step_load_patches),
|
|
||||||
StepCategory.PREPARING,
|
|
||||||
state = State.RUNNING
|
|
||||||
),
|
|
||||||
Step(
|
Step(
|
||||||
context.getString(R.string.download_apk),
|
context.getString(R.string.download_apk),
|
||||||
StepCategory.PREPARING,
|
StepCategory.PREPARING,
|
||||||
downloadProgress = downloadProgress
|
state = State.RUNNING,
|
||||||
).takeIf { selectedApp is SelectedApp.Download },
|
downloadProgress = downloadProgress,
|
||||||
|
).takeIf { needsDownload },
|
||||||
|
Step(
|
||||||
|
context.getString(R.string.patcher_step_load_patches),
|
||||||
|
StepCategory.PREPARING,
|
||||||
|
state = if (needsDownload) State.WAITING else State.RUNNING,
|
||||||
|
),
|
||||||
Step(
|
Step(
|
||||||
context.getString(R.string.patcher_step_unpack),
|
context.getString(R.string.patcher_step_unpack),
|
||||||
StepCategory.PREPARING
|
StepCategory.PREPARING
|
||||||
|
|
|
@ -111,6 +111,8 @@
|
||||||
|
|
||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="ok">OK</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="reset">Reset</string>
|
||||||
<string name="patch">Patch</string>
|
<string name="patch">Patch</string>
|
||||||
<string name="select_from_storage">Select from storage</string>
|
<string name="select_from_storage">Select from storage</string>
|
||||||
|
@ -129,6 +131,10 @@
|
||||||
<string name="dark">Dark</string>
|
<string name="dark">Dark</string>
|
||||||
<string name="appearance">Appearance</string>
|
<string name="appearance">Appearance</string>
|
||||||
<string name="downloaded_apps">Downloaded apps</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">API URL</string>
|
||||||
<string name="api_url_dialog_title">Set custom 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>
|
<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_title">Filter</string>
|
||||||
<string name="patch_selector_sheet_filter_compat_title">Compatibility</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_menu_description">More options</string>
|
||||||
<string name="string_option_placeholder">Value</string>
|
|
||||||
|
|
||||||
<string name="path_selector">Select from storage</string>
|
<string name="path_selector">Select from storage</string>
|
||||||
<string name="path_selector_parent_dir">Previous directory</string>
|
<string name="path_selector_parent_dir">Previous directory</string>
|
||||||
|
|
|
@ -31,6 +31,8 @@ skrapeit = "1.2.2"
|
||||||
libsu = "5.2.1"
|
libsu = "5.2.1"
|
||||||
scrollbars = "1.0.4"
|
scrollbars = "1.0.4"
|
||||||
compose-icons = "1.2.4"
|
compose-icons = "1.2.4"
|
||||||
|
kotlin-process = "1.4.1"
|
||||||
|
hidden-api-stub = "4.3.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX Core
|
# AndroidX Core
|
||||||
|
@ -102,6 +104,12 @@ markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-ren
|
||||||
# Fading Edges
|
# Fading Edges
|
||||||
fading-edges = { group = "com.github.GIGAMOLE", name = "ComposeFadingEdges", version.ref = "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
|
||||||
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "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" }
|
libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }
|
||||||
|
|
Loading…
Reference in a new issue