From 928ba0ff21fcca5bbf61f59dbcb462cd87d7218e Mon Sep 17 00:00:00 2001 From: Canny Date: Mon, 21 Nov 2022 20:26:41 +0300 Subject: [PATCH] feat: use coroutines for patching --- app/build.gradle.kts | 8 +- .../revanced/manager/ManagerApplication.kt | 5 - .../app/revanced/manager/di/WorkerModule.kt | 10 - .../manager/patcher/worker/PatcherWorker.kt | 224 ------------------ .../ui/screen/subscreens/PatchingSubscreen.kt | 52 ++-- .../ui/viewmodel/PatchingScreenViewModel.kt | 217 ++++++++++++++--- 6 files changed, 205 insertions(+), 311 deletions(-) delete mode 100644 app/src/main/java/app/revanced/manager/di/WorkerModule.kt delete mode 100644 app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index af4db7c..9ff19f8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index e2f5f2f..06063bb 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -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 ) diff --git a/app/src/main/java/app/revanced/manager/di/WorkerModule.kt b/app/src/main/java/app/revanced/manager/di/WorkerModule.kt deleted file mode 100644 index 4f8178e..0000000 --- a/app/src/main/java/app/revanced/manager/di/WorkerModule.kt +++ /dev/null @@ -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()) } -} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt deleted file mode 100644 index a7b99cf..0000000 --- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt +++ /dev/null @@ -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() } - } -} diff --git a/app/src/main/java/app/revanced/manager/ui/screen/subscreens/PatchingSubscreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/subscreens/PatchingSubscreen.kt index e95da46..cec80cc 100644 --- a/app/src/main/java/app/revanced/manager/ui/screen/subscreens/PatchingSubscreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screen/subscreens/PatchingSubscreen.kt @@ -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, + ) } - ) + } } } } diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchingScreenViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchingScreenViewModel.kt index 1586e41..def5384 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchingScreenViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchingScreenViewModel.kt @@ -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() var status by mutableStateOf(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) + } } \ No newline at end of file