feat: switch back to WorkManager for patching

This commit is contained in:
Canny 2022-12-06 21:29:01 +03:00
parent f68a9b31a9
commit a3e41552f8
No known key found for this signature in database
GPG Key ID: 395CCB0AA979F27B
7 changed files with 320 additions and 175 deletions

View File

@ -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")

View File

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -51,6 +52,17 @@
</intent-filter>
</activity>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
<service android:name=".installer.service.InstallService" />
<service android:name=".installer.service.UninstallService" />
</application>

View File

@ -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
)

View File

@ -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()) }
}

View File

@ -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
}
}

View File

@ -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<PatchLog>()
var status by mutableStateOf<Status>(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)
}

View File

@ -34,8 +34,7 @@
<string name="manager_contributors">Manager</string>
<string name="integrations_contributors">Integrations</string>
<string name="unsupported_version">Unsupported version</string>
<string name="patcher_notification_title">Patching</string>
<string name="patcher_notification_message">ReVanced Manager is patching</string>
<string name="patcher_notification_message">Patching in progress…</string>
<string name="theme">Theme</string>
<string name="apply">Apply</string>
<string name="warning">Warning</string>