mirror of
https://github.com/revanced/revanced-manager-compose-old.git
synced 2025-05-02 15:34:26 +02:00
feat: use coroutines for patching
This commit is contained in:
parent
8fff5241c2
commit
928ba0ff21
@ -81,19 +81,17 @@ 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"
|
||||||
implementation("androidx.compose.ui:ui:$composeVersion")
|
implementation("androidx.compose.ui:ui:$composeVersion")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling:$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}")
|
implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
|
||||||
|
|
||||||
// Accompanist
|
// Accompanist
|
||||||
@ -113,11 +111,11 @@ dependencies {
|
|||||||
implementation("com.github.X1nto:Taxi:1.2.0")
|
implementation("com.github.X1nto:Taxi:1.2.0")
|
||||||
|
|
||||||
// ReVanced
|
// ReVanced
|
||||||
implementation("app.revanced:revanced-patcher:6.0.0")
|
implementation("app.revanced:revanced-patcher:6.0.2")
|
||||||
|
|
||||||
// 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-alpha07")
|
implementation("com.android.tools.build:apksig:8.0.0-alpha08")
|
||||||
|
|
||||||
// Licenses
|
// Licenses
|
||||||
implementation("com.mikepenz:aboutlibraries-compose:10.5.1")
|
implementation("com.mikepenz:aboutlibraries-compose:10.5.1")
|
||||||
|
@ -2,10 +2,7 @@ package app.revanced.manager
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import app.revanced.manager.di.*
|
import app.revanced.manager.di.*
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.ImageLoaderFactory
|
|
||||||
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() {
|
||||||
@ -14,13 +11,11 @@ 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
|
||||||
)
|
)
|
||||||
|
@ -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()) }
|
|
||||||
}
|
|
@ -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() }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,9 @@
|
|||||||
package app.revanced.manager.ui.screen.subscreens
|
package app.revanced.manager.ui.screen.subscreens
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
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.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
@ -14,14 +12,14 @@ import androidx.compose.material3.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import app.revanced.manager.ui.navigation.AppDestination
|
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
|
||||||
|
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.PatchLog
|
||||||
|
import app.revanced.manager.ui.viewmodel.PatchingScreenViewModel.Status
|
||||||
import com.xinto.taxi.BackstackNavigator
|
import com.xinto.taxi.BackstackNavigator
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koin.androidx.compose.getViewModel
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
|
|
||||||
@ -33,8 +31,6 @@ fun PatchingSubscreen(
|
|||||||
vm: PatchingScreenViewModel = getViewModel()
|
vm: PatchingScreenViewModel = getViewModel()
|
||||||
) {
|
) {
|
||||||
var patching by mutableStateOf(false)
|
var patching by mutableStateOf(false)
|
||||||
val scrollState = rememberScrollState()
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
LaunchedEffect(patching) {
|
LaunchedEffect(patching) {
|
||||||
if (!patching) {
|
if (!patching) {
|
||||||
patching = true
|
patching = true
|
||||||
@ -70,7 +66,7 @@ fun PatchingSubscreen(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
when (vm.status) {
|
when (vm.status) {
|
||||||
is PatchingScreenViewModel.Status.Failure -> {
|
is Status.Failure -> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Close,
|
Icons.Default.Close,
|
||||||
"failed",
|
"failed",
|
||||||
@ -80,7 +76,7 @@ fun PatchingSubscreen(
|
|||||||
)
|
)
|
||||||
Text(text = "Failed!", fontSize = 30.sp)
|
Text(text = "Failed!", fontSize = 30.sp)
|
||||||
}
|
}
|
||||||
is PatchingScreenViewModel.Status.Patching -> {
|
is Status.Patching -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
@ -88,7 +84,7 @@ fun PatchingSubscreen(
|
|||||||
)
|
)
|
||||||
Text(text = "Patching...", fontSize = 30.sp)
|
Text(text = "Patching...", fontSize = 30.sp)
|
||||||
}
|
}
|
||||||
is PatchingScreenViewModel.Status.Success -> {
|
is Status.Success -> {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Done,
|
Icons.Default.Done,
|
||||||
"done",
|
"done",
|
||||||
@ -98,7 +94,7 @@ fun PatchingSubscreen(
|
|||||||
)
|
)
|
||||||
Text(text = "Completed!", fontSize = 30.sp)
|
Text(text = "Completed!", fontSize = 30.sp)
|
||||||
}
|
}
|
||||||
PatchingScreenViewModel.Status.Idle -> {}
|
Status.Idle -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,24 +104,24 @@ fun PatchingSubscreen(
|
|||||||
.padding(20.dp)
|
.padding(20.dp)
|
||||||
) {
|
) {
|
||||||
ElevatedCard {
|
ElevatedCard {
|
||||||
Text(
|
LazyColumn(
|
||||||
text = Logging.log,
|
Modifier
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 20.dp, vertical = 10.dp)
|
.padding(horizontal = 20.dp, vertical = 10.dp)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(scrollState),
|
) {
|
||||||
fontSize = 20.sp,
|
items(vm.logs) { log ->
|
||||||
lineHeight = 35.sp,
|
Text(
|
||||||
overflow = TextOverflow.Visible,
|
modifier = Modifier.height(36.dp),
|
||||||
onTextLayout = {
|
text = log.message,
|
||||||
coroutineScope.launch {
|
color = when (log) {
|
||||||
scrollState.animateScrollTo(
|
is PatchLog.Success -> Color.Green
|
||||||
it.size.height,
|
is PatchLog.Info -> LocalContentColor.current
|
||||||
tween(1000, easing = LinearEasing)
|
is PatchLog.Error -> Color.Red
|
||||||
)
|
},
|
||||||
}
|
fontSize = 20.sp,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,53 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
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.getValue
|
||||||
|
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.work.*
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
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 =
|
sealed interface PatchLog {
|
||||||
OneTimeWorkRequest.Builder(PatcherWorker::class.java) // create Worker
|
val message: String
|
||||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
|
||||||
.setInputData(
|
|
||||||
Data.Builder()
|
|
||||||
.build()
|
|
||||||
).build()
|
|
||||||
|
|
||||||
private val liveData =
|
data class Success(override val message: String) : PatchLog
|
||||||
WorkManager.getInstance(app).getWorkInfoByIdLiveData(patcherWorker.id) // get LiveData
|
data class Info(override val message: String) : PatchLog
|
||||||
|
data class Error(override val message: String) : PatchLog
|
||||||
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)
|
var status by mutableStateOf<Status>(Status.Idle)
|
||||||
|
|
||||||
sealed class Status {
|
sealed class Status {
|
||||||
@ -42,27 +59,149 @@ class PatchingScreenViewModel(val app: Application) : ViewModel() {
|
|||||||
|
|
||||||
fun startPatcher() {
|
fun startPatcher() {
|
||||||
cancelPatching() // cancel patching if its still running
|
cancelPatching() // cancel patching if its still running
|
||||||
Logging.log = "" // clear logs
|
logs.clear()
|
||||||
|
status = Status.Patching
|
||||||
WorkManager.getInstance(app)
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
.enqueueUniqueWork(
|
try {
|
||||||
"patching",
|
runPatcher(createWorkDir())
|
||||||
ExistingWorkPolicy.KEEP,
|
} catch (e: Exception) {
|
||||||
patcherWorker
|
status = Status.Failure
|
||||||
) // enqueue patching process
|
Log.e(tag, "Error while patching: ${e.message ?: e::class.simpleName}")
|
||||||
liveData.observeForever(observer) // start observing patch status
|
Sentry.captureException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelPatching() {
|
private fun cancelPatching() {
|
||||||
WorkManager.getInstance(app).cancelWorkById(patcherWorker.id)
|
viewModelScope.coroutineContext.cancelChildren(CancellationException("Patching was cancelled by user."))
|
||||||
|
logs.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
private suspend fun runPatcher(
|
||||||
super.onCleared()
|
workdir: File
|
||||||
liveData.removeObserver(observer) // remove observer when ViewModel is destroyed
|
) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Logging {
|
|
||||||
var log by mutableStateOf("")
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user