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