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 // AndroidX activity
implementation("androidx.activity:activity-compose:1.6.1") implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.work:work-runtime-ktx:2.7.1")
// Koin // Koin
val koinVersion = "3.3.0" val koinVersion = "3.3.0"
implementation("io.insert-koin:koin-android:$koinVersion") implementation("io.insert-koin:koin-android:$koinVersion")
implementation("io.insert-koin:koin-androidx-compose:3.3.0") implementation("io.insert-koin:koin-androidx-compose:3.3.0")
implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion")
// Compose // Compose
val composeVersion = "1.4.0-alpha01" val composeVersion = "1.4.0-alpha01"
@ -116,7 +118,7 @@ dependencies {
// Signing & aligning // Signing & aligning
implementation("org.bouncycastle:bcpkix-jdk15on:1.70") 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 // Licenses
implementation("com.mikepenz:aboutlibraries-compose:10.5.1") 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.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_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.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
@ -51,6 +52,17 @@
</intent-filter> </intent-filter>
</activity> </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.InstallService" />
<service android:name=".installer.service.UninstallService" /> <service android:name=".installer.service.UninstallService" />
</application> </application>

View File

@ -3,6 +3,7 @@ package app.revanced.manager
import android.app.Application import android.app.Application
import app.revanced.manager.di.* import app.revanced.manager.di.*
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.workmanager.koin.workManagerFactory
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
class ManagerApplication : Application() { class ManagerApplication : Application() {
@ -11,11 +12,13 @@ class ManagerApplication : Application() {
startKoin { startKoin {
androidContext(this@ManagerApplication) androidContext(this@ManagerApplication)
workManagerFactory()
modules( modules(
httpModule, httpModule,
preferencesModule, preferencesModule,
viewModelModule, viewModelModule,
repositoryModule, repositoryModule,
workerModule,
patcherModule, patcherModule,
serviceModule 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.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageInstaller 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.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.work.*
import app.revanced.manager.installer.service.InstallService import app.revanced.manager.installer.service.InstallService
import app.revanced.manager.installer.service.UninstallService import app.revanced.manager.installer.service.UninstallService
import app.revanced.manager.installer.utils.PM import app.revanced.manager.installer.utils.PM
import app.revanced.manager.network.api.ManagerAPI import app.revanced.manager.patcher.worker.PatcherWorker
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 java.io.File 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( class PatchingScreenViewModel(
private val app: Application, private val app: Application,
private val managerAPI: ManagerAPI,
private val patcherUtils: PatcherUtils
) : ViewModel() { ) : ViewModel() {
var installFailure by mutableStateOf(false)
var pmStatus by mutableStateOf(-999)
var extra by mutableStateOf("")
sealed interface PatchLog { sealed interface PatchLog {
val message: String val message: String
@ -66,7 +38,30 @@ class PatchingScreenViewModel(
object Failure : Status() 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>() val logs = mutableStateListOf<PatchLog>()
var status by mutableStateOf<Status>(Status.Idle) var status by mutableStateOf<Status>(Status.Idle)
@ -81,18 +76,29 @@ class PatchingScreenViewModel(
} }
UninstallService.APP_UNINSTALL_ACTION -> { 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 { init {
app.registerReceiver( workManager.enqueueUniqueWork("patching", ExistingWorkPolicy.KEEP, patcherWorker)
installBroadcastReceiver, liveData.observeForever(observer)
IntentFilter().apply { app.registerReceiver(installBroadcastReceiver, IntentFilter().apply {
addAction(InstallService.APP_INSTALL_ACTION) addAction(InstallService.APP_INSTALL_ACTION)
addAction(UninstallService.APP_UNINSTALL_ACTION) addAction(UninstallService.APP_UNINSTALL_ACTION)
} addAction(PatcherWorker.PATCH_LOG)
) })
} }
fun installApk(apk: File) { 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() { override fun onCleared() {
super.onCleared() super.onCleared()
liveData.removeObserver(observer)
app.unregisterReceiver(installBroadcastReceiver) app.unregisterReceiver(installBroadcastReceiver)
patcher.cancel(CancellationException("ViewModel cleared")) workManager.cancelWorkById(patcherWorker.id)
logs.clear() logs.clear()
} }
private fun createWorkDir(): File {
return app.cacheDir.resolve("tmp-${System.currentTimeMillis()}").also { it.mkdirs() }
}
private fun log(data: PatchLog) { private fun log(data: PatchLog) {
logs.add(data) logs.add(data)
} }

View File

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