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
implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.work:work-runtime-ktx:2.7.1")
// Koin
val koinVersion = "3.3.0"
implementation("io.insert-koin:koin-android:$koinVersion")
implementation("io.insert-koin:koin-androidx-compose:3.3.0")
implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion")
// Compose
val composeVersion = "1.4.0-alpha01"
implementation("androidx.compose.ui:ui:$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}")
// Accompanist
@ -113,11 +111,11 @@ dependencies {
implementation("com.github.X1nto:Taxi:1.2.0")
// ReVanced
implementation("app.revanced:revanced-patcher:6.0.0")
implementation("app.revanced:revanced-patcher:6.0.2")
// Signing & aligning
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
implementation("com.mikepenz:aboutlibraries-compose:10.5.1")

View File

@ -2,10 +2,7 @@ package app.revanced.manager
import android.app.Application
import app.revanced.manager.di.*
import coil.ImageLoader
import coil.ImageLoaderFactory
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin
class ManagerApplication : Application() {
@ -14,13 +11,11 @@ class ManagerApplication : Application() {
startKoin {
androidContext(this@ManagerApplication)
workManagerFactory()
modules(
httpModule,
preferencesModule,
viewModelModule,
repositoryModule,
workerModule,
patcherModule,
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
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
@ -14,14 +12,14 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.sp
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.PatchLog
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.Status
import com.xinto.taxi.BackstackNavigator
import kotlinx.coroutines.launch
import org.koin.androidx.compose.getViewModel
@ -33,8 +31,6 @@ fun PatchingSubscreen(
vm: PatchingScreenViewModel = getViewModel()
) {
var patching by mutableStateOf(false)
val scrollState = rememberScrollState()
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(patching) {
if (!patching) {
patching = true
@ -70,7 +66,7 @@ fun PatchingSubscreen(
) {
when (vm.status) {
is PatchingScreenViewModel.Status.Failure -> {
is Status.Failure -> {
Icon(
Icons.Default.Close,
"failed",
@ -80,7 +76,7 @@ fun PatchingSubscreen(
)
Text(text = "Failed!", fontSize = 30.sp)
}
is PatchingScreenViewModel.Status.Patching -> {
is Status.Patching -> {
CircularProgressIndicator(
modifier = Modifier
.padding(vertical = 16.dp)
@ -88,7 +84,7 @@ fun PatchingSubscreen(
)
Text(text = "Patching...", fontSize = 30.sp)
}
is PatchingScreenViewModel.Status.Success -> {
is Status.Success -> {
Icon(
Icons.Default.Done,
"done",
@ -98,7 +94,7 @@ fun PatchingSubscreen(
)
Text(text = "Completed!", fontSize = 30.sp)
}
PatchingScreenViewModel.Status.Idle -> {}
Status.Idle -> {}
}
}
}
@ -108,24 +104,24 @@ fun PatchingSubscreen(
.padding(20.dp)
) {
ElevatedCard {
Text(
text = Logging.log,
modifier = Modifier
LazyColumn(
Modifier
.padding(horizontal = 20.dp, vertical = 10.dp)
.fillMaxSize()
.verticalScroll(scrollState),
fontSize = 20.sp,
lineHeight = 35.sp,
overflow = TextOverflow.Visible,
onTextLayout = {
coroutineScope.launch {
scrollState.animateScrollTo(
it.size.height,
tween(1000, easing = LinearEasing)
)
}
) {
items(vm.logs) { log ->
Text(
modifier = Modifier.height(36.dp),
text = log.message,
color = when (log) {
is PatchLog.Success -> Color.Green
is PatchLog.Info -> LocalContentColor.current
is PatchLog.Error -> Color.Red
},
fontSize = 20.sp,
)
}
)
}
}
}
}

View File

@ -1,36 +1,53 @@
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.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.work.*
import app.revanced.manager.patcher.worker.PatcherWorker
import androidx.lifecycle.viewModelScope
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 =
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setInputData(
Data.Builder()
.build()
).build()
sealed interface PatchLog {
val message: String
private val liveData =
WorkManager.getInstance(app).getWorkInfoByIdLiveData(patcherWorker.id) // get LiveData
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
}
data class Success(override val message: String) : PatchLog
data class Info(override val message: String) : PatchLog
data class Error(override val message: String) : PatchLog
}
val logs = mutableStateListOf<PatchLog>()
var status by mutableStateOf<Status>(Status.Idle)
sealed class Status {
@ -42,27 +59,149 @@ class PatchingScreenViewModel(val app: Application) : ViewModel() {
fun startPatcher() {
cancelPatching() // cancel patching if its still running
Logging.log = "" // clear logs
WorkManager.getInstance(app)
.enqueueUniqueWork(
"patching",
ExistingWorkPolicy.KEEP,
patcherWorker
) // enqueue patching process
liveData.observeForever(observer) // start observing patch status
logs.clear()
status = Status.Patching
viewModelScope.launch(Dispatchers.IO) {
try {
runPatcher(createWorkDir())
} catch (e: Exception) {
status = Status.Failure
Log.e(tag, "Error while patching: ${e.message ?: e::class.simpleName}")
Sentry.captureException(e)
}
}
}
private fun cancelPatching() {
WorkManager.getInstance(app).cancelWorkById(patcherWorker.id)
viewModelScope.coroutineContext.cancelChildren(CancellationException("Patching was cancelled by user."))
logs.clear()
}
override fun onCleared() {
super.onCleared()
liveData.removeObserver(observer) // remove observer when ViewModel is destroyed
}
}
private suspend fun runPatcher(
workdir: File
) {
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 {
var log by mutableStateOf("")
log(PatchLog.Info("Checking prerequisites..."))
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)
}
}