diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4af3628031..2e3b018fdd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + @@ -45,6 +46,8 @@ android:name=".MainActivity" android:exported="false" android:theme="@style/Theme.ReVancedManager" /> + + \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screens/mainsubscreens/PatcherForegroundService.kt b/app/src/main/java/app/revanced/manager/ui/screens/mainsubscreens/PatcherForegroundService.kt new file mode 100644 index 0000000000..7290dc8570 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/ui/screens/mainsubscreens/PatcherForegroundService.kt @@ -0,0 +1,192 @@ +package app.revanced.manager.ui.screens.mainsubscreens + +import android.app.* +import android.content.Intent +import android.os.IBinder +import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.core.content.ContextCompat +import androidx.lifecycle.viewModelScope +import app.revanced.manager.R +import app.revanced.manager.backend.api.ManagerAPI +import app.revanced.manager.backend.utils.Aapt +import app.revanced.manager.backend.utils.aligning.ZipAligner +import app.revanced.manager.backend.utils.filesystem.ZipFileSystemUtils +import app.revanced.manager.backend.utils.signing.Signer +import app.revanced.manager.ui.Resource +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherOptions +import app.revanced.patcher.data.Data +import app.revanced.patcher.extensions.PatchExtensions.patchName +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.util.patch.implementation.DexPatchBundle +import dalvik.system.DexClassLoader +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.io.File +import java.lang.IllegalArgumentException + +class PatcherForegroundService : Service() { + + val patches = mutableStateOf>>>>(Resource.Loading) + val tag = "Patcher" + + override fun onCreate() { + super.onCreate() + val notificationIntent = Intent(this, PatcherForegroundService::class.java) + val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) + val channel = + NotificationChannel("revanced-patcher-patching", + "Patching", + NotificationManager.IMPORTANCE_LOW) + val notificationManager = + ContextCompat.getSystemService(this, NotificationManager::class.java) + notificationManager!!.createNotificationChannel(channel) + val notification: Notification = Notification.Builder(this, channel.id) + .setContentTitle(getText(R.string.patcher_notification_title)) + .setContentText(getText(R.string.patcher_notification_message)) + //.setSmallIcon(R.drawable.icon) + .setContentIntent(pendingIntent) + .build() + + startForeground(1, notification) + } + + override fun onBind(intent: Intent?): IBinder? { + return null + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + try { + val selectedPatches = intent.getStringArrayExtra("selectedPatches") + ?: throw IllegalArgumentException("selectedPatches is missing") + val patchBundleFile = intent.getStringExtra("patchBundleFile") + ?: throw IllegalArgumentException("patchBundleFile is missing") + runPatcher(selectedPatches.toList(), patchBundleFile) + }finally { + stopSelf() + } + return START_NOT_STICKY + } + + private fun runPatcher(selectedPatches: List, patchBundleFile: String): Boolean { + + val aaptPath = Aapt.binary(this).absolutePath + val frameworkPath = filesDir.resolve("framework").also { it.mkdirs() }.absolutePath + val integrationsCacheDir = filesDir.resolve("integrations-cache").also { it.mkdirs() } + + runBlocking { + loadPatches(patchBundleFile) + Log.d(tag, "Checking prerequisites") + val patches = findPatchesByIds(selectedPatches) + if (patches.isEmpty()) return@runBlocking true + val integrations = downloadIntegrations(integrationsCacheDir) + + Log.d(tag, "Creating directories") + val workdir = createWorkDir() + val inputFile = File(workdir.parentFile!!, "base.apk") + val patchedFile = File(workdir, "patched.apk") + val alignedFile = File(workdir, "aligned.apk") + val outputFile = File(workdir, "out.apk") + val cacheDirectory = workdir.resolve("cache") + + try { + // Log.d(tag, "Copying base.apk from ${info.packageName}") + // withContext(Dispatchers.IO) { + // Files.copy( + // File(info.publicSourceDir).toPath(), + // inputFile.toPath(), + // StandardCopyOption.REPLACE_EXISTING + // ) + // } + + Log.d(tag, "Creating patcher") + val patcher = Patcher( + PatcherOptions( + inputFile, + cacheDirectory.absolutePath, + patchResources = true, + aaptPath = aaptPath, + frameworkFolderLocation = frameworkPath + ) + ) + + Log.d(tag, "Merging integrations") + patcher.addFiles(listOf(integrations)) {} + + Log.d(tag, "Adding ${patches.size} patch(es)") + patcher.addPatches(patches) + + Log.d(tag, "Applying patches") + patcher.applyPatches().forEach { (patch, result) -> + if (result.isSuccess) { + Log.i(tag, "[success] $patch") + return@forEach + } + Log.e(tag, "[error] $patch:", result.exceptionOrNull()!!) + } + + Log.d(tag, "Saving file") + val result = patcher.save() + ZipFileSystemUtils(result.resourceFile!!, patchedFile).use { fs -> + result.dexFiles.forEach { fs.write(it.name, it.dexFileInputStream.readBytes()) } + fs.writeInput() + fs.uncompress(*result.doNotCompress!!.toTypedArray()) + } + + Log.d(tag, "Aligning apk") + ZipAligner.align(patchedFile, alignedFile) + Log.d(tag, "Signing apk") + Signer("ReVanced", "s3cur3p@ssw0rd").signApk(alignedFile, outputFile) + + // TODO: install apk! + Log.d(tag, "Installing apk") + } catch (e: Exception) { + Log.e(tag, "Error while patching", e) + } + + Log.d(tag, "Deleting workdir") + //workdir.deleteRecursively() + } + return false + } + + private fun createWorkDir(): File { + return filesDir.resolve("tmp-${System.currentTimeMillis()}").also { it.mkdirs() } + } + + private fun findPatchesByIds(ids: Iterable): List>> { + val (patches) = patches.value as? Resource.Success ?: return listOf() + return patches.filter { patch -> ids.any { it == patch.patchName } } + } + + private suspend fun downloadIntegrations(workdir: File): File { + return try { + val (_, out) = ManagerAPI.downloadIntegrations(workdir) + out + } catch (e: Exception) { + throw Exception("Failed to download integrations", e) + } + } + + private fun loadPatches(patchBundleFile: String) { + try { + loadPatches0(patchBundleFile) + } catch (e: Exception) { + Log.e(tag, "An error occurred while loading patches", e) + } + } + + private fun loadPatches0(path: String) { + val patchClasses = DexPatchBundle( + path, DexClassLoader( + path, + codeCacheDir.absolutePath, + null, + javaClass.classLoader + ) + ).loadPatches() + patches.value = Resource.Success(patchClasses) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/ui/screens/mainsubscreens/PatcherSubscreen.kt b/app/src/main/java/app/revanced/manager/ui/screens/mainsubscreens/PatcherSubscreen.kt index fd750cf4af..92349f104c 100644 --- a/app/src/main/java/app/revanced/manager/ui/screens/mainsubscreens/PatcherSubscreen.kt +++ b/app/src/main/java/app/revanced/manager/ui/screens/mainsubscreens/PatcherSubscreen.kt @@ -1,6 +1,7 @@ package app.revanced.manager.ui.screens.mainsubscreens import android.app.Application +import android.content.Intent import android.content.pm.PackageManager import android.util.Log import androidx.activity.ComponentActivity @@ -24,19 +25,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import app.revanced.manager.R import app.revanced.manager.backend.api.ManagerAPI -import app.revanced.manager.backend.utils.Aapt -import app.revanced.manager.backend.utils.aligning.ZipAligner -import app.revanced.manager.backend.utils.filesystem.ZipFileSystemUtils -import app.revanced.manager.backend.utils.signing.Signer import app.revanced.manager.ui.Resource import app.revanced.manager.ui.components.FloatingActionButton import app.revanced.manager.ui.screens.destinations.AppSelectorScreenDestination import app.revanced.manager.ui.screens.destinations.PatchesSelectorScreenDestination -import app.revanced.patcher.Patcher -import app.revanced.patcher.PatcherOptions import app.revanced.patcher.data.Data import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.patch.Patch import app.revanced.patcher.util.patch.implementation.DexPatchBundle import com.ramcosta.composedestinations.annotation.Destination @@ -46,6 +40,7 @@ import kotlinx.coroutines.launch import java.io.File import java.util.* + private const val tag = "PatcherScreen" @OptIn(ExperimentalMaterial3Api::class) @@ -54,7 +49,7 @@ private const val tag = "PatcherScreen" @Composable fun PatcherSubscreen( navigator: NavController, - vm: PatcherViewModel = viewModel(LocalContext.current as ComponentActivity) + vm: PatcherViewModel = viewModel(LocalContext.current as ComponentActivity), ) { val selectedAppPackage by vm.selectedAppPackage val hasAppSelected = selectedAppPackage.isPresent @@ -127,18 +122,16 @@ fun PatcherSubscreen( data class PatchClass( val patch: Class>, - val unsupported: Boolean + val unsupported: Boolean, ) class PatcherViewModel(val app: Application) : AndroidViewModel(app) { - private val aaptPath = Aapt.binary(app).absolutePath private val bundleCacheDir = app.filesDir.resolve("bundle-cache").also { it.mkdirs() } - private val frameworkPath = app.filesDir.resolve("framework").also { it.mkdirs() }.absolutePath - private val integrationsCacheDir = app.filesDir.resolve("integrations-cache").also { it.mkdirs() } val selectedAppPackage = mutableStateOf(Optional.empty()) private val selectedPatches = mutableStateListOf() val patches = mutableStateOf>>>>(Resource.Loading) + lateinit var patchBundleFile: String init { loadPatches() @@ -168,10 +161,7 @@ class PatcherViewModel(val app: Application) : AndroidViewModel(app) { return !selectedPatches.isEmpty() } - private fun findPatchesByIds(ids: Iterable): List>> { - val (patches) = patches.value as? Resource.Success ?: return listOf() - return patches.filter { patch -> ids.any { it == patch.patchName } } - } + private fun getSelectedPackageInfo() = if (selectedAppPackage.value.isPresent) @@ -207,18 +197,10 @@ class PatcherViewModel(val app: Application) : AndroidViewModel(app) { } } - private suspend fun downloadIntegrations(workdir: File): File { - return try { - val (_, out) = ManagerAPI.downloadIntegrations(workdir) - out - } catch (e: Exception) { - throw Exception("Failed to download integrations", e) - } - } - private fun loadPatches() = viewModelScope.launch { try { val file = downloadDefaultPatchBundle(bundleCacheDir) + patchBundleFile=file.absolutePath loadPatches0(file.absolutePath) } catch (e: Exception) { Log.e(tag, "An error occurred while loading patches", e) @@ -237,86 +219,10 @@ class PatcherViewModel(val app: Application) : AndroidViewModel(app) { patches.value = Resource.Success(patchClasses) } - private fun createWorkDir(): File { - return app.filesDir.resolve("tmp-${System.currentTimeMillis()}").also { it.mkdirs() } - } - fun startPatcher() { - val tag = "Patcher" - - viewModelScope.launch { - Log.d(tag, "Checking prerequisites") - val info = getSelectedPackageInfo()?.applicationInfo ?: return@launch - val patches = findPatchesByIds(selectedPatches) - if (patches.isEmpty()) return@launch - val integrations = downloadIntegrations(integrationsCacheDir) - - Log.d(tag, "Creating directories") - val workdir = createWorkDir() - val inputFile = File(workdir.parentFile!!, "base.apk") - val patchedFile = File(workdir, "patched.apk") - val alignedFile = File(workdir, "aligned.apk") - val outputFile = File(workdir, "out.apk") - val cacheDirectory = workdir.resolve("cache") - val buildDirectory = cacheDirectory.resolve("build") - - try { -// Log.d(tag, "Copying base.apk from ${info.packageName}") -// withContext(Dispatchers.IO) { -// Files.copy( -// File(info.publicSourceDir).toPath(), -// inputFile.toPath(), -// StandardCopyOption.REPLACE_EXISTING -// ) -// } - - Log.d(tag, "Creating patcher") - val patcher = Patcher( - PatcherOptions( - inputFile, - cacheDirectory.absolutePath, - patchResources = true, - aaptPath = aaptPath, - frameworkFolderLocation = frameworkPath - ) - ) - - Log.d(tag, "Merging integrations") - patcher.addFiles(listOf(integrations)) {} - - Log.d(tag, "Adding ${patches.size} patch(es)") - patcher.addPatches(patches) - - Log.d(tag, "Applying patches") - patcher.applyPatches().forEach { (patch, result) -> - if (result.isSuccess) { - Log.i(tag, "[success] $patch") - return@forEach - } - Log.e(tag, "[error] $patch:", result.exceptionOrNull()!!) - } - - Log.d(tag, "Saving file") - val result = patcher.save() - ZipFileSystemUtils(result.resourceFile!!, patchedFile).use { fs -> - result.dexFiles.forEach { fs.write(it.name, it.dexFileInputStream.readBytes()) } - fs.writeInput() - fs.uncompress(*result.doNotCompress!!.toTypedArray()) - } - - Log.d(tag, "Aligning apk") - ZipAligner.align(patchedFile, alignedFile) - Log.d(tag, "Signing apk") - Signer("ReVanced", "s3cur3p@ssw0rd").signApk(alignedFile, outputFile) - - // TODO: install apk! - Log.d(tag, "Installing apk") - } catch (e: Exception) { - Log.e(tag, "Error while patching", e) - } - - Log.d(tag, "Deleting workdir") - //workdir.deleteRecursively() - } + val serviceIntent = Intent(app, PatcherForegroundService::class.java) + serviceIntent.putExtra("selectedPatches",selectedPatches.toTypedArray()) + serviceIntent.putExtra("patchBundleFile",patchBundleFile) + app.startForegroundService(serviceIntent) } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e3a289ef1..3e703f06f4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,4 +42,6 @@ Team Translators Patcher + Patching + ReVanced Manager is patching \ No newline at end of file