From a3e41552f8b81055da2973333d5164ef9657b707 Mon Sep 17 00:00:00 2001 From: Canny Date: Tue, 6 Dec 2022 21:29:01 +0300 Subject: [PATCH] feat: switch back to WorkManager for patching --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 12 + .../revanced/manager/ManagerApplication.kt | 3 + .../app/revanced/manager/di/WorkerModule.kt | 10 + .../manager/patcher/worker/PatcherWorker.kt | 244 ++++++++++++++++++ .../ui/viewmodel/PatchingScreenViewModel.kt | 219 ++++------------ app/src/main/res/values/strings.xml | 3 +- 7 files changed, 320 insertions(+), 175 deletions(-) create mode 100644 app/src/main/java/app/revanced/manager/di/WorkerModule.kt create 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 07e7090..e515519 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -81,11 +81,13 @@ 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" @@ -116,7 +118,7 @@ dependencies { // Signing & aligning implementation("org.bouncycastle:bcpkix-jdk15on:1.70") - implementation("com.android.tools.build:apksig:8.0.0-alpha08") + implementation("com.android.tools.build:apksig:8.0.0-alpha09") // Licenses implementation("com.mikepenz:aboutlibraries-compose:10.5.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e7f3698..2120842 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + @@ -51,6 +52,17 @@ + + + + diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt index 06063bb..bd4f0af 100644 --- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt +++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt @@ -3,6 +3,7 @@ package app.revanced.manager import android.app.Application import app.revanced.manager.di.* import org.koin.android.ext.koin.androidContext +import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.context.startKoin class ManagerApplication : Application() { @@ -11,11 +12,13 @@ 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 new file mode 100644 index 0000000..4f8178e --- /dev/null +++ b/app/src/main/java/app/revanced/manager/di/WorkerModule.kt @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..b9bc8fb --- /dev/null +++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt @@ -0,0 +1,244 @@ +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 android.view.WindowManager +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.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.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 { + 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_HIGH + ) + val notificationManager = + ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) + notificationManager!!.createNotificationChannel(channel) + return Notification.Builder(applicationContext, channel.id) + .setContentTitle(applicationContext.getText(R.string.app_name)) + .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("Error while patching: ${e::class.simpleName}: ${e.message}", ERROR) + Log.e(tag, e.stackTraceToString()) + 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(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply { + acquire(10 * 60 * 1000L) + } + } + Log.d(tag, "Acquired wakelock.") + + try { + val aaptPath = Aapt.binary(applicationContext)?.absolutePath + if (aaptPath == null) { + log("AAPT2 not found.", ERROR) + 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() + val appPath = patcherUtils.selectedAppPackagePath.value + + log("Checking prerequisites...", INFO) + val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches) + if (patches.isEmpty()) throw IllegalStateException("No patches selected.") + + log("Creating directories...", INFO) + val inputFile = File(workdir, "input.apk") + val patchedFile = File(workdir, "patched.apk") + val outputFile = File(inputData.getString("output")!!) + val cacheDirectory = workdir.resolve("cache") + + val integrations = managerAPI.downloadIntegrations(integrationsCacheDir) + + log("Copying APK from device...", INFO) + withContext(Dispatchers.IO) { + Files.copy( + File(appPath ?: appInfo.publicSourceDir).toPath(), + inputFile.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } + log("Decoding resources", INFO) + 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("Merging integrations", INFO) + patcher.addFiles(listOf(integrations)) {} + + val patchesString = if (patches.size > 1) "patches" else "patch" + log("Applying ${patches.size} $patchesString", INFO) + patcher.executePatches().forEach { (patch, result) -> + if (result.isFailure) { + log( + "Failed to apply $patch: " + "${result.exceptionOrNull()!!.message ?: result.exceptionOrNull()!!::class.simpleName}", + ERROR + ) + Log.e(tag, result.exceptionOrNull()!!.stackTraceToString()) + return@forEach + } + } + + log("Saving file", INFO) + val result = patcher.save() // compile apk + + ZipFile(patchedFile).use { fs -> + result.dexFiles.forEach { + log("Writing dex file ${it.name}", INFO) + fs.addEntryCompressData(ZipEntry.createWithName(it.name), it.stream.readBytes()) + } + + log("Aligning apk...", INFO) + result.resourceFile?.let { + fs.copyEntriesFromFileAligned(ZipFile(it), ZipAligner::getEntryAlignment) + } + fs.copyEntriesFromFileAligned(ZipFile(inputFile), ZipAligner::getEntryAlignment) + } + + log("Signing apk...", INFO) + Signer("ReVanced", "s3cur3p@ssw0rd").signApk(patchedFile, outputFile) + withContext(Dispatchers.IO) { + Files.copy( + outputFile.inputStream(), + reVancedFolder.resolve(appInfo.packageName + ".apk").toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } + log("Successfully patched!", SUCCESS) + 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() } + } + + private fun log(message: String, status: Int) { + applicationContext.sendBroadcast(Intent().apply { + action = PATCH_LOG + putExtra(PATCH_MESSAGE, message) + putExtra(PATCH_STATUS, status) + }) + } + + companion object { + const val PATCH_MESSAGE = "PATCH_MESSAGE" + const val PATCH_STATUS = "PATCH_STATUS" + const val PATCH_LOG = "PATCH_LOG" + + const val INFO = 0 + const val ERROR = 1 + const val SUCCESS = 2 + } +} \ No newline at end of file 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 7e21fb8..e18a266 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 @@ -6,51 +6,23 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInstaller -import android.os.Environment -import android.os.PowerManager -import android.util.Log -import android.view.WindowManager 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.lifecycle.viewModelScope +import androidx.work.* import app.revanced.manager.installer.service.InstallService import app.revanced.manager.installer.service.UninstallService import app.revanced.manager.installer.utils.PM -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.launch -import kotlinx.coroutines.withContext +import app.revanced.manager.patcher.worker.PatcherWorker 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( private val app: Application, - private val managerAPI: ManagerAPI, - private val patcherUtils: PatcherUtils ) : ViewModel() { - var installFailure by mutableStateOf(false) - - var pmStatus by mutableStateOf(-999) - var extra by mutableStateOf("") - sealed interface PatchLog { val message: String @@ -66,7 +38,30 @@ class PatchingScreenViewModel( object Failure : Status() } - val outputFile = File(app.filesDir, "output.apk") + val workManager = WorkManager.getInstance(app) + var installFailure by mutableStateOf(false) + var pmStatus by mutableStateOf(-999) + var extra by mutableStateOf("") + + val outputFile = File(app.cacheDir, "output.apk") + + private val patcherWorker = + OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).setInputData( + Data.Builder().putString("output", outputFile.path).build() + ).build() + + private val liveData = workManager.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 + } + } + val logs = mutableStateListOf() var status by mutableStateOf(Status.Idle) @@ -81,18 +76,29 @@ class PatchingScreenViewModel( } UninstallService.APP_UNINSTALL_ACTION -> { } + PatcherWorker.PATCH_LOG -> { + val message = intent.getStringExtra(PatcherWorker.PATCH_MESSAGE) + val patchLog = + when (intent.getIntExtra(PatcherWorker.PATCH_STATUS, PatcherWorker.INFO)) { + PatcherWorker.INFO -> PatchLog.Info(message!!) + PatcherWorker.SUCCESS -> PatchLog.Success(message!!) + PatcherWorker.ERROR -> PatchLog.Error(message!!) + else -> null + } + patchLog?.let { log(it) } + } } } } init { - app.registerReceiver( - installBroadcastReceiver, - IntentFilter().apply { - addAction(InstallService.APP_INSTALL_ACTION) - addAction(UninstallService.APP_UNINSTALL_ACTION) - } - ) + workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker) + liveData.observeForever(observer) + app.registerReceiver(installBroadcastReceiver, IntentFilter().apply { + addAction(InstallService.APP_INSTALL_ACTION) + addAction(UninstallService.APP_UNINSTALL_ACTION) + addAction(PatcherWorker.PATCH_LOG) + }) } fun installApk(apk: File) { @@ -109,145 +115,14 @@ class PatchingScreenViewModel( } } - private val patcher = viewModelScope.launch(Dispatchers.IO) { - withContext(Dispatchers.Main) { - status = Status.Patching - } - val workdir = createWorkDir() - val wakeLock: PowerManager.WakeLock = - (app.getSystemService(Context.POWER_SERVICE) as PowerManager).run { - newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, "$tag::Patcher").apply { - acquire(10 * 60 * 1000L) - } - } - Log.d(tag, "Acquired wakelock.") - try { - 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() - val appPath = patcherUtils.selectedAppPackagePath.value - - log(PatchLog.Info("Checking prerequisites...")) - val patches = patcherUtils.findPatchesByIds(patcherUtils.selectedPatches) - if (patches.isEmpty()) throw IllegalStateException("No patches selected.") - - log(PatchLog.Info("Creating directories...")) - val inputFile = File(app.filesDir, "input.apk") - val patchedFile = File(workdir, "patched.apk") - val cacheDirectory = workdir.resolve("cache") - - val integrations = managerAPI.downloadIntegrations(integrationsCacheDir) - - log(PatchLog.Info("Copying APK from device...")) - withContext(Dispatchers.IO) { - Files.copy( - File(appPath ?: appInfo.publicSourceDir).toPath(), - inputFile.toPath(), - StandardCopyOption.REPLACE_EXISTING - ) - } - 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()!!.message ?: result.exceptionOrNull()!!::class.simpleName}")) - Log.e(tag, result.exceptionOrNull()!!.stackTraceToString()) - 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 - } catch (e: Exception) { - status = Status.Failure - log(PatchLog.Error("Error while patching: ${e::class.simpleName}: ${e.message}")) - Log.e(tag, e.stackTraceToString()) - Sentry.captureException(e) - } - Log.d(tag, "Deleting workdir") - workdir.deleteRecursively() - wakeLock.release() - Log.d(tag, "Released wakelock.") - } - override fun onCleared() { super.onCleared() + liveData.removeObserver(observer) app.unregisterReceiver(installBroadcastReceiver) - patcher.cancel(CancellationException("ViewModel cleared")) + workManager.cancelWorkById(patcherWorker.id) logs.clear() } - private fun createWorkDir(): File { - return app.cacheDir.resolve("tmp-${System.currentTimeMillis()}").also { it.mkdirs() } - } - private fun log(data: PatchLog) { logs.add(data) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e26913..1e9eb6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,8 +34,7 @@ Manager Integrations Unsupported version - Patching - ReVanced Manager is patching + Patching in progress… Theme Apply Warning