feat: use coroutines for patching

This commit is contained in:
Canny 2022-11-21 20:26:41 +03:00
parent 8fff5241c2
commit 928ba0ff21
No known key found for this signature in database
GPG Key ID: 395CCB0AA979F27B
6 changed files with 205 additions and 311 deletions

View File

@ -81,19 +81,17 @@ dependencies {
// AndroidX activity // AndroidX activity
implementation("androidx.activity:activity-compose:1.6.1") implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.work:work-runtime-ktx:2.7.1")
// Koin // Koin
val koinVersion = "3.3.0" val koinVersion = "3.3.0"
implementation("io.insert-koin:koin-android:$koinVersion") implementation("io.insert-koin:koin-android:$koinVersion")
implementation("io.insert-koin:koin-androidx-compose:3.3.0") implementation("io.insert-koin:koin-androidx-compose:3.3.0")
implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion")
// Compose // Compose
val composeVersion = "1.4.0-alpha01" val composeVersion = "1.4.0-alpha01"
implementation("androidx.compose.ui:ui:$composeVersion") implementation("androidx.compose.ui:ui:$composeVersion")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion") debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
implementation("androidx.compose.material3:material3:1.1.0-alpha01") implementation("androidx.compose.material3:material3:1.1.0-alpha02")
implementation("androidx.compose.material:material-icons-extended:${composeVersion}") implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
// Accompanist // Accompanist
@ -113,11 +111,11 @@ dependencies {
implementation("com.github.X1nto:Taxi:1.2.0") implementation("com.github.X1nto:Taxi:1.2.0")
// ReVanced // ReVanced
implementation("app.revanced:revanced-patcher:6.0.0") implementation("app.revanced:revanced-patcher:6.0.2")
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("com.android.tools.build:apksig:8.0.0-alpha07") implementation("com.android.tools.build:apksig:8.0.0-alpha08")
// Licenses // Licenses
implementation("com.mikepenz:aboutlibraries-compose:10.5.1") implementation("com.mikepenz:aboutlibraries-compose:10.5.1")

View File

@ -2,10 +2,7 @@ package app.revanced.manager
import android.app.Application import android.app.Application
import app.revanced.manager.di.* import app.revanced.manager.di.*
import coil.ImageLoader
import coil.ImageLoaderFactory
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
class ManagerApplication : Application() { class ManagerApplication : Application() {
@ -14,13 +11,11 @@ class ManagerApplication : Application() {
startKoin { startKoin {
androidContext(this@ManagerApplication) androidContext(this@ManagerApplication)
workManagerFactory()
modules( modules(
httpModule, httpModule,
preferencesModule, preferencesModule,
viewModelModule, viewModelModule,
repositoryModule, repositoryModule,
workerModule,
patcherModule, patcherModule,
serviceModule serviceModule
) )

View File

@ -1,10 +0,0 @@
package app.revanced.manager.di
import app.revanced.manager.patcher.worker.PatcherWorker
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.dsl.worker
import org.koin.dsl.module
val workerModule = module {
worker { PatcherWorker(androidContext(), get(), get(), get()) }
}

View File

@ -1,224 +0,0 @@
package app.revanced.manager.patcher.worker
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Icon
import android.os.Environment
import android.os.PowerManager
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import app.revanced.manager.R
import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.patcher.aligning.ZipAligner
import app.revanced.manager.patcher.aligning.zip.ZipFile
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
import app.revanced.manager.patcher.signing.Signer
import app.revanced.manager.ui.viewmodel.Logging
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.logging.Logger
import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import java.io.File
import java.io.FileNotFoundException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
class PatcherWorker(
context: Context,
parameters: WorkerParameters,
private val managerAPI: ManagerAPI,
private val patcherUtils: PatcherUtils
) : CoroutineWorker(context, parameters), KoinComponent {
val tag = "ReVanced Manager"
private val workdir = createWorkDir()
override suspend fun getForegroundInfo(): ForegroundInfo {
return ForegroundInfo(1, createNotification())
}
private fun createNotification(): Notification {
val notificationIntent = Intent(applicationContext, PatcherWorker::class.java)
val pendingIntent: PendingIntent = PendingIntent.getActivity(
applicationContext, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE
)
val channel = NotificationChannel(
"revanced-patcher-patching", "Patching", NotificationManager.IMPORTANCE_LOW
)
val notificationManager =
ContextCompat.getSystemService(applicationContext, NotificationManager::class.java)
notificationManager!!.createNotificationChannel(channel)
return Notification.Builder(applicationContext, channel.id)
.setContentTitle(applicationContext.getText(R.string.patcher_notification_title))
.setContentText(applicationContext.getText(R.string.patcher_notification_message))
.setLargeIcon(Icon.createWithResource(applicationContext, R.drawable.manager))
.setSmallIcon(Icon.createWithResource(applicationContext, R.drawable.manager))
.setContentIntent(pendingIntent).build()
}
override suspend fun doWork(): Result {
if (runAttemptCount > 0) {
Log.d(tag, "Android requested retrying but retrying is disabled.")
return Result.failure() // don't retry
}
try {
setForeground(ForegroundInfo(1, createNotification()))
} catch (e: Exception) {
Log.d(tag, "Failed to set foreground info:", e)
Sentry.captureException(e)
}
return try {
runPatcher(workdir)
Result.success()
} catch (e: Exception) {
Log.e(tag, "Error while patching: ${e.message ?: e::class.simpleName}")
Sentry.captureException(e)
Result.failure()
}
}
private suspend fun runPatcher(
workdir: File
): Boolean {
val wakeLock: PowerManager.WakeLock =
(applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.FULL_WAKE_LOCK, "$tag::Patcher").apply {
acquire(10 * 60 * 1000L)
}
}
Log.d(tag, "Acquired wakelock.")
val aaptPath = Aapt.binary(applicationContext)?.absolutePath
if (aaptPath == null) {
Logging.log += "AAPT2 not found.\n"
throw FileNotFoundException()
}
val frameworkPath =
applicationContext.filesDir.resolve("framework").also { it.mkdirs() }.absolutePath
val integrationsCacheDir =
applicationContext.filesDir.resolve("integrations-cache").also { it.mkdirs() }
val reVancedFolder =
Environment.getExternalStorageDirectory().resolve("ReVanced").also { it.mkdirs() }
val appInfo = patcherUtils.selectedAppPackage.value.get()
Logging.log += "Checking prerequisites\n"
val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches)
if (patches.isEmpty()) return true
Logging.log += "Creating directories\n"
val inputFile = File(applicationContext.filesDir, "input.apk")
val patchedFile = File(workdir, "patched.apk")
val outputFile = File(applicationContext.filesDir, "output.apk")
val cacheDirectory = workdir.resolve("cache")
val integrations = managerAPI.downloadIntegrations(integrationsCacheDir)
Logging.log += "Copying base.apk from device\n"
withContext(Dispatchers.IO) {
Files.copy(
File(appInfo.publicSourceDir).toPath(),
inputFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
try {
Logging.log += "Decoding resources\n"
val patcher = Patcher( // start patcher
PatcherOptions(
inputFile,
cacheDirectory.absolutePath,
aaptPath = aaptPath,
frameworkFolderLocation = frameworkPath,
logger = object : Logger {
override fun error(msg: String) {
Log.e(tag, msg)
}
override fun warn(msg: String) {
Log.w(tag, msg)
}
override fun info(msg: String) {
Log.i(tag, msg)
}
override fun trace(msg: String) {
Log.v(tag, msg)
}
}
)
)
Log.d(tag, "Adding ${patches.size} patch(es)")
patcher.addPatches(patches)
Logging.log += "Merging integrations\n"
patcher.addFiles(listOf(integrations)) {}
Logging.log += "Applying ${patches.size} patch(es)\n"
patcher.executePatches().forEach { (patch, result) ->
if (result.isFailure) {
Logging.log += "Failed to apply $patch" + result.exceptionOrNull()!!.cause + "\n"
return@forEach
}
}
Logging.log += "Saving file\n"
val result = patcher.save() // compile apk
ZipFile(patchedFile).use { fs ->
result.dexFiles.forEach {
Logging.log += "Writing dex file ${it.name}\n"
fs.addEntryCompressData(ZipEntry.createWithName(it.name), it.stream.readBytes())
}
Logging.log += "Aligning apk!\n"
result.resourceFile?.let {
fs.copyEntriesFromFileAligned(ZipFile(it), ZipAligner::getEntryAlignment)
}
fs.copyEntriesFromFileAligned(ZipFile(inputFile), ZipAligner::getEntryAlignment)
}
Logging.log += "Signing apk\n"
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outputFile)
Logging.log += "Successfully patched!\n"
withContext(Dispatchers.IO) {
Files.copy(
outputFile.inputStream(),
reVancedFolder.resolve(appInfo.packageName + ".apk")
.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
Logging.log += "Copied file to storage!\n"
patcherUtils.cleanup()
} finally {
Log.d(tag, "Deleting workdir")
workdir.deleteRecursively()
wakeLock.release()
Log.d(tag, "Released wakelock.")
}
return false
}
private fun createWorkDir(): File {
return applicationContext.cacheDir.resolve("tmp-${System.currentTimeMillis()}")
.also { it.mkdirs() }
}
}

View File

@ -1,11 +1,9 @@
package app.revanced.manager.ui.screen.subscreens package app.revanced.manager.ui.screen.subscreens
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
@ -14,14 +12,14 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.revanced.manager.ui.navigation.AppDestination import app.revanced.manager.ui.navigation.AppDestination
import app.revanced.manager.ui.viewmodel.Logging
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.PatchLog
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.Status
import com.xinto.taxi.BackstackNavigator import com.xinto.taxi.BackstackNavigator
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@ -33,8 +31,6 @@ fun PatchingSubscreen(
vm: PatchingScreenViewModel = getViewModel() vm: PatchingScreenViewModel = getViewModel()
) { ) {
var patching by mutableStateOf(false) var patching by mutableStateOf(false)
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(patching) { LaunchedEffect(patching) {
if (!patching) { if (!patching) {
patching = true patching = true
@ -70,7 +66,7 @@ fun PatchingSubscreen(
) { ) {
when (vm.status) { when (vm.status) {
is PatchingScreenViewModel.Status.Failure -> { is Status.Failure -> {
Icon( Icon(
Icons.Default.Close, Icons.Default.Close,
"failed", "failed",
@ -80,7 +76,7 @@ fun PatchingSubscreen(
) )
Text(text = "Failed!", fontSize = 30.sp) Text(text = "Failed!", fontSize = 30.sp)
} }
is PatchingScreenViewModel.Status.Patching -> { is Status.Patching -> {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier modifier = Modifier
.padding(vertical = 16.dp) .padding(vertical = 16.dp)
@ -88,7 +84,7 @@ fun PatchingSubscreen(
) )
Text(text = "Patching...", fontSize = 30.sp) Text(text = "Patching...", fontSize = 30.sp)
} }
is PatchingScreenViewModel.Status.Success -> { is Status.Success -> {
Icon( Icon(
Icons.Default.Done, Icons.Default.Done,
"done", "done",
@ -98,7 +94,7 @@ fun PatchingSubscreen(
) )
Text(text = "Completed!", fontSize = 30.sp) Text(text = "Completed!", fontSize = 30.sp)
} }
PatchingScreenViewModel.Status.Idle -> {} Status.Idle -> {}
} }
} }
} }
@ -108,24 +104,24 @@ fun PatchingSubscreen(
.padding(20.dp) .padding(20.dp)
) { ) {
ElevatedCard { ElevatedCard {
Text( LazyColumn(
text = Logging.log, Modifier
modifier = Modifier
.padding(horizontal = 20.dp, vertical = 10.dp) .padding(horizontal = 20.dp, vertical = 10.dp)
.fillMaxSize() .fillMaxSize()
.verticalScroll(scrollState), ) {
fontSize = 20.sp, items(vm.logs) { log ->
lineHeight = 35.sp, Text(
overflow = TextOverflow.Visible, modifier = Modifier.height(36.dp),
onTextLayout = { text = log.message,
coroutineScope.launch { color = when (log) {
scrollState.animateScrollTo( is PatchLog.Success -> Color.Green
it.size.height, is PatchLog.Info -> LocalContentColor.current
tween(1000, easing = LinearEasing) is PatchLog.Error -> Color.Red
) },
} fontSize = 20.sp,
)
} }
) }
} }
} }
} }

View File

@ -1,36 +1,53 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.Context
import android.os.Environment
import android.os.PowerManager
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.work.* import androidx.lifecycle.viewModelScope
import app.revanced.manager.patcher.worker.PatcherWorker import app.revanced.manager.network.api.ManagerAPI
import app.revanced.manager.patcher.PatcherUtils
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.patcher.aligning.ZipAligner
import app.revanced.manager.patcher.aligning.zip.ZipFile
import app.revanced.manager.patcher.aligning.zip.structures.ZipEntry
import app.revanced.manager.patcher.signing.Signer
import app.revanced.manager.util.tag
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.logging.Logger
import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileNotFoundException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.concurrent.CancellationException
class PatchingScreenViewModel(val app: Application) : ViewModel() { class PatchingScreenViewModel(
private val app: Application,
private val managerAPI: ManagerAPI,
private val patcherUtils: PatcherUtils
) : ViewModel() {
private val patcherWorker = sealed interface PatchLog {
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker val message: String
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(
Data.Builder()
.build()
).build()
private val liveData = data class Success(override val message: String) : PatchLog
WorkManager.getInstance(app).getWorkInfoByIdLiveData(patcherWorker.id) // get LiveData data class Info(override val message: String) : PatchLog
data class Error(override val message: String) : PatchLog
private val observer = Observer { workInfo: WorkInfo -> // observer for observing patch status
status = when (workInfo.state) {
WorkInfo.State.RUNNING -> Status.Patching
WorkInfo.State.SUCCEEDED -> Status.Success
WorkInfo.State.FAILED -> Status.Failure
else -> Status.Idle
}
} }
val logs = mutableStateListOf<PatchLog>()
var status by mutableStateOf<Status>(Status.Idle) var status by mutableStateOf<Status>(Status.Idle)
sealed class Status { sealed class Status {
@ -42,27 +59,149 @@ class PatchingScreenViewModel(val app: Application) : ViewModel() {
fun startPatcher() { fun startPatcher() {
cancelPatching() // cancel patching if its still running cancelPatching() // cancel patching if its still running
Logging.log = "" // clear logs logs.clear()
status = Status.Patching
WorkManager.getInstance(app) viewModelScope.launch(Dispatchers.IO) {
.enqueueUniqueWork( try {
"patching", runPatcher(createWorkDir())
ExistingWorkPolicy.KEEP, } catch (e: Exception) {
patcherWorker status = Status.Failure
) // enqueue patching process Log.e(tag, "Error while patching: ${e.message ?: e::class.simpleName}")
liveData.observeForever(observer) // start observing patch status Sentry.captureException(e)
}
}
} }
private fun cancelPatching() { private fun cancelPatching() {
WorkManager.getInstance(app).cancelWorkById(patcherWorker.id) viewModelScope.coroutineContext.cancelChildren(CancellationException("Patching was cancelled by user."))
logs.clear()
} }
override fun onCleared() { private suspend fun runPatcher(
super.onCleared() workdir: File
liveData.removeObserver(observer) // remove observer when ViewModel is destroyed ) {
} val wakeLock: PowerManager.WakeLock =
} (app.getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.FULL_WAKE_LOCK, "$tag::Patcher").apply {
acquire(10 * 60 * 1000L)
}
}
Log.d(tag, "Acquired wakelock.")
val aaptPath = Aapt.binary(app)?.absolutePath
if (aaptPath == null) {
log(PatchLog.Error("AAPT2 not found."))
throw FileNotFoundException()
}
val frameworkPath = app.filesDir.resolve("framework").also { it.mkdirs() }.absolutePath
val integrationsCacheDir = app.filesDir.resolve("integrations-cache").also { it.mkdirs() }
val reVancedFolder =
Environment.getExternalStorageDirectory().resolve("ReVanced").also { it.mkdirs() }
val appInfo = patcherUtils.selectedAppPackage.value.get()
object Logging { log(PatchLog.Info("Checking prerequisites..."))
var log by mutableStateOf("") val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches)
if (patches.isEmpty()) return
log(PatchLog.Info("Creating directories..."))
val inputFile = File(app.filesDir, "input.apk")
val patchedFile = File(workdir, "patched.apk")
val outputFile = File(app.filesDir, "output.apk")
val cacheDirectory = workdir.resolve("cache")
val integrations = managerAPI.downloadIntegrations(integrationsCacheDir)
log(PatchLog.Info("Copying APK from device..."))
withContext(Dispatchers.IO) {
Files.copy(
File(appInfo.publicSourceDir).toPath(),
inputFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
try {
log(PatchLog.Info("Decoding resources"))
val patcher = Patcher( // start patcher
PatcherOptions(inputFile,
cacheDirectory.absolutePath,
aaptPath = aaptPath,
frameworkFolderLocation = frameworkPath,
logger = object : Logger {
override fun error(msg: String) {
Log.e(tag, msg)
}
override fun warn(msg: String) {
Log.w(tag, msg)
}
override fun info(msg: String) {
Log.i(tag, msg)
}
override fun trace(msg: String) {
Log.v(tag, msg)
}
})
)
Log.d(tag, "Adding ${patches.size} patch(es)")
patcher.addPatches(patches)
log(PatchLog.Info("Merging integrations"))
patcher.addFiles(listOf(integrations)) {}
val patchesString = if (patches.size > 1) "patches" else "patch"
log(PatchLog.Info("Applying ${patches.size} $patchesString"))
patcher.executePatches().forEach { (patch, result) ->
if (result.isFailure) {
log(PatchLog.Info("Failed to apply $patch" + result.exceptionOrNull()!!.cause))
return@forEach
}
}
log(PatchLog.Info("Saving file"))
val result = patcher.save() // compile apk
ZipFile(patchedFile).use { fs ->
result.dexFiles.forEach {
log(PatchLog.Info("Writing dex file ${it.name}"))
fs.addEntryCompressData(ZipEntry.createWithName(it.name), it.stream.readBytes())
}
log(PatchLog.Info("Aligning apk..."))
result.resourceFile?.let {
fs.copyEntriesFromFileAligned(ZipFile(it), ZipAligner::getEntryAlignment)
}
fs.copyEntriesFromFileAligned(ZipFile(inputFile), ZipAligner::getEntryAlignment)
}
log(PatchLog.Info("Signing apk..."))
Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outputFile)
withContext(Dispatchers.IO) {
Files.copy(
outputFile.inputStream(),
reVancedFolder.resolve(appInfo.packageName + ".apk").toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
log(PatchLog.Success("Successfully patched!"))
patcherUtils.cleanup()
status = Status.Success
} finally {
Log.d(tag, "Deleting workdir")
workdir.deleteRecursively()
wakeLock.release()
Log.d(tag, "Released wakelock.")
}
}
private fun createWorkDir(): File {
return app.cacheDir.resolve("tmp-${System.currentTimeMillis()}").also { it.mkdirs() }
}
private fun log(data: PatchLog) {
logs.add(data)
}
} }