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