mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-19 17:23:58 +00:00
feat: add external process runtime (#1799)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user