diff --git a/app/CHANGELOG.md b/app/CHANGELOG.md
index 357662fc..fa441e51 100644
--- a/app/CHANGELOG.md
+++ b/app/CHANGELOG.md
@@ -1,3 +1,17 @@
+# app [1.26.0-dev.16](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.15...v1.26.0-dev.16) (2025-12-30)
+
+
+### Features
+
+* Show patches as individual steps in patcher screen ([#2889](https://github.com/ReVanced/revanced-manager/issues/2889)) ([11dd6e4](https://github.com/ReVanced/revanced-manager/commit/11dd6e4064099427a8c9bc6f225a19412e5c70e2))
+
+# app [1.26.0-dev.15](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.14...v1.26.0-dev.15) (2025-12-29)
+
+
+### Bug Fixes
+
+* install dialog getting stuck ([#2900](https://github.com/ReVanced/revanced-manager/issues/2900)) ([18a4df9](https://github.com/ReVanced/revanced-manager/commit/18a4df9af9cac120fdb8e4ff7aadd2e2a8d5c1a6))
+
# app [1.26.0-dev.14](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.13...v1.26.0-dev.14) (2025-12-28)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index da4e9fe9..de9dc96b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -108,6 +108,10 @@ dependencies {
// Compose Icons
implementation(libs.compose.icons.fontawesome)
+
+ // Ackpine
+ implementation(libs.ackpine.core)
+ implementation(libs.ackpine.ktx)
}
buildscript {
diff --git a/app/gradle.properties b/app/gradle.properties
index 40507de2..f9f77dcd 100644
--- a/app/gradle.properties
+++ b/app/gradle.properties
@@ -1 +1 @@
-version = 1.26.0-dev.14
+version = 1.26.0-dev.16
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e418d68b..f6364fbc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -51,9 +51,6 @@
-
-
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/aidl/app/revanced/manager/patcher/ProgressEventParcel.aidl b/app/src/main/aidl/app/revanced/manager/patcher/ProgressEventParcel.aidl
new file mode 100644
index 00000000..e9f6cf2c
--- /dev/null
+++ b/app/src/main/aidl/app/revanced/manager/patcher/ProgressEventParcel.aidl
@@ -0,0 +1,4 @@
+// ProgressEventParcel.aidl
+package app.revanced.manager.patcher;
+
+parcelable ProgressEventParcel;
\ No newline at end of file
diff --git a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl
index 27a4f61b..fa11709a 100644
--- a/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl
+++ b/app/src/main/aidl/app/revanced/manager/patcher/runtime/process/IPatcherEvents.aidl
@@ -1,11 +1,12 @@
// IPatcherEvents.aidl
package app.revanced.manager.patcher.runtime.process;
+import app.revanced.manager.patcher.ProgressEventParcel;
+
// Interface for sending events back to the main app process.
oneway interface IPatcherEvents {
void log(String level, String msg);
- void patchSucceeded();
- void progress(String name, String state, String msg);
+ void event(in ProgressEventParcel event);
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
void finished(String exceptionStackTrace);
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ManagerApplication.kt b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
index 1d17e5ef..b5dec19b 100644
--- a/app/src/main/java/app/revanced/manager/ManagerApplication.kt
+++ b/app/src/main/java/app/revanced/manager/ManagerApplication.kt
@@ -48,7 +48,8 @@ class ManagerApplication : Application() {
workerModule,
viewModelModule,
databaseModule,
- rootModule
+ rootModule,
+ ackpineModule
)
}
diff --git a/app/src/main/java/app/revanced/manager/di/AckpineModule.kt b/app/src/main/java/app/revanced/manager/di/AckpineModule.kt
new file mode 100644
index 00000000..76fd0ec8
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/di/AckpineModule.kt
@@ -0,0 +1,19 @@
+package app.revanced.manager.di
+
+import android.content.Context
+import org.koin.android.ext.koin.androidContext
+import org.koin.dsl.module
+import ru.solrudev.ackpine.installer.PackageInstaller
+import ru.solrudev.ackpine.uninstaller.PackageUninstaller
+
+val ackpineModule = module {
+ fun provideInstaller(context: Context) = PackageInstaller.getInstance(context)
+ fun provideUninstaller(context: Context) = PackageUninstaller.getInstance(context)
+
+ single {
+ provideInstaller(androidContext())
+ }
+ single {
+ provideUninstaller(androidContext())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt b/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt
new file mode 100644
index 00000000..ce010a01
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/patcher/PatcherProgress.kt
@@ -0,0 +1,78 @@
+package app.revanced.manager.patcher
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+
+@Parcelize
+sealed class ProgressEvent : Parcelable {
+ abstract val stepId: StepId?
+
+ data class Started(override val stepId: StepId) : ProgressEvent()
+
+ data class Progress(
+ override val stepId: StepId,
+ val current: Long? = null,
+ val total: Long? = null,
+ val message: String? = null,
+ ) : ProgressEvent()
+
+ data class Completed(
+ override val stepId: StepId,
+ ) : ProgressEvent()
+
+ data class Failed(
+ override val stepId: StepId?,
+ val error: RemoteError,
+ ) : ProgressEvent()
+}
+
+/**
+ * Parcelable wrapper for [ProgressEvent].
+ *
+ * Required because AIDL does not support sealed classes.
+ */
+@Parcelize
+data class ProgressEventParcel(val event: ProgressEvent) : Parcelable
+
+fun ProgressEventParcel.toEvent(): ProgressEvent = event
+fun ProgressEvent.toParcel(): ProgressEventParcel = ProgressEventParcel(this)
+
+@Parcelize
+sealed class StepId : Parcelable {
+ data object DownloadAPK : StepId()
+ data object LoadPatches : StepId()
+ data object ReadAPK : StepId()
+ data object ExecutePatches : StepId()
+ data class ExecutePatch(val index: Int) : StepId()
+ data object WriteAPK : StepId()
+ data object SignAPK : StepId()
+}
+
+@Parcelize
+data class RemoteError(
+ val type: String,
+ val message: String?,
+ val stackTrace: String,
+) : Parcelable
+
+fun Exception.toRemoteError() = RemoteError(
+ type = this::class.java.name,
+ message = this.message,
+ stackTrace = this.stackTraceToString(),
+)
+
+
+inline fun runStep(
+ stepId: StepId,
+ onEvent: (ProgressEvent) -> Unit,
+ block: () -> T,
+): T = try {
+ onEvent(ProgressEvent.Started(stepId))
+ val value = block()
+ onEvent(ProgressEvent.Completed(stepId))
+ value
+} catch (error: Exception) {
+ onEvent(ProgressEvent.Failed(stepId, error.toRemoteError()))
+ throw error
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/Session.kt b/app/src/main/java/app/revanced/manager/patcher/Session.kt
index dd5e7dc4..42f0e955 100644
--- a/app/src/main/java/app/revanced/manager/patcher/Session.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/Session.kt
@@ -1,10 +1,9 @@
package app.revanced.manager.patcher
-import android.content.Context
import app.revanced.library.ApkUtils.applyTo
-import app.revanced.manager.R
+import app.revanced.manager.patcher.Session.Companion.component1
+import app.revanced.manager.patcher.Session.Companion.component2
import app.revanced.manager.patcher.logger.Logger
-import app.revanced.manager.ui.model.State
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.patch.Patch
@@ -22,15 +21,10 @@ class Session(
cacheDir: String,
frameworkDir: String,
aaptPath: String,
- private val androidContext: Context,
private val logger: Logger,
private val input: File,
- private val onPatchCompleted: suspend () -> Unit,
- private val onProgress: (name: String?, state: State?, message: String?) -> Unit
+ private val onEvent: (ProgressEvent) -> Unit,
) : Closeable {
- private fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
- onProgress(name, state, message)
-
private val tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
private val patcher = Patcher(
PatcherConfig(
@@ -42,86 +36,68 @@ class Session(
)
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
- var nextPatchIndex = 0
-
- updateProgress(
- name = androidContext.getString(R.string.executing_patch, selectedPatches[nextPatchIndex]),
- state = State.RUNNING
- )
-
this().collect { (patch, exception) ->
- if (patch !in selectedPatches) return@collect
+ val index = selectedPatches.indexOf(patch)
+ if (index == -1) return@collect
if (exception != null) {
- updateProgress(
- name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
- state = State.FAILED,
- message = exception.stackTraceToString()
+ onEvent(
+ ProgressEvent.Failed(
+ StepId.ExecutePatch(index),
+ exception.toRemoteError(),
+ )
)
-
logger.error("${patch.name} failed:")
logger.error(exception.stackTraceToString())
throw exception
}
- nextPatchIndex++
-
- onPatchCompleted()
-
- selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
- updateProgress(
- name = androidContext.getString(R.string.executing_patch, nextPatch.name)
+ onEvent(
+ ProgressEvent.Completed(
+ StepId.ExecutePatch(index),
)
- }
+ )
logger.info("${patch.name} succeeded")
}
-
- updateProgress(
- state = State.COMPLETED,
- name = androidContext.resources.getQuantityString(
- R.plurals.patches_executed,
- selectedPatches.size,
- selectedPatches.size
- )
- )
}
suspend fun run(output: File, selectedPatches: PatchList) {
- updateProgress(state = State.COMPLETED) // Unpacking
+ runStep(StepId.ExecutePatches, onEvent) {
+ java.util.logging.Logger.getLogger("").apply {
+ handlers.forEach {
+ it.close()
+ removeHandler(it)
+ }
- java.util.logging.Logger.getLogger("").apply {
- handlers.forEach {
- it.close()
- removeHandler(it)
+ addHandler(logger.handler)
}
- addHandler(logger.handler)
+ with(patcher) {
+ logger.info("Merging integrations")
+ this += selectedPatches.toSet()
+
+ logger.info("Applying patches...")
+ applyPatchesVerbose(selectedPatches.sortedBy { it.name })
+ }
}
- with(patcher) {
- logger.info("Merging integrations")
- this += selectedPatches.toSet()
+ runStep(StepId.WriteAPK, onEvent) {
+ logger.info("Writing patched files...")
+ val result = patcher.get()
- logger.info("Applying patches...")
- applyPatchesVerbose(selectedPatches.sortedBy { it.name })
+ val patched = tempDir.resolve("result.apk")
+ withContext(Dispatchers.IO) {
+ Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ }
+ result.applyTo(patched)
+
+ logger.info("Patched apk saved to $patched")
+
+ withContext(Dispatchers.IO) {
+ Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ }
}
-
- logger.info("Writing patched files...")
- val result = patcher.get()
-
- val patched = tempDir.resolve("result.apk")
- withContext(Dispatchers.IO) {
- Files.copy(input.toPath(), patched.toPath(), StandardCopyOption.REPLACE_EXISTING)
- }
- result.applyTo(patched)
-
- logger.info("Patched apk saved to $patched")
-
- withContext(Dispatchers.IO) {
- Files.move(patched.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING)
- }
- updateProgress(state = State.COMPLETED) // Saving
}
override fun close() {
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt
index 50a96a1f..8b52f5d5 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/CoroutineRuntime.kt
@@ -1,11 +1,12 @@
package app.revanced.manager.patcher.runtime
import android.content.Context
+import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.Session
+import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.patch.PatchBundle
-import app.revanced.manager.patcher.worker.ProgressEventHandler
-import app.revanced.manager.ui.model.State
+import app.revanced.manager.patcher.runStep
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import java.io.File
@@ -13,7 +14,7 @@ import java.io.File
/**
* Simple [Runtime] implementation that runs the patcher using coroutines.
*/
-class CoroutineRuntime(private val context: Context) : Runtime(context) {
+class CoroutineRuntime(context: Context) : Runtime(context) {
override suspend fun execute(
inputFile: String,
outputFile: String,
@@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
- onPatchCompleted: suspend () -> Unit,
- onProgress: ProgressEventHandler,
+ onEvent: (ProgressEvent) -> Unit,
) {
- val selectedBundles = selectedPatches.keys
- val bundles = bundles()
- val uids = bundles.entries.associate { (key, value) -> value to key }
+ val patchList = runStep(StepId.LoadPatches, onEvent) {
+ val selectedBundles = selectedPatches.keys
+ val bundles = bundles()
+ val uids = bundles.entries.associate { (key, value) -> value to key }
- val allPatches =
- PatchBundle.Loader.patches(bundles.values, packageName)
- .mapKeys { (b, _) -> uids[b]!! }
- .filterKeys { it in selectedBundles }
+ val allPatches =
+ PatchBundle.Loader.patches(bundles.values, packageName)
+ .mapKeys { (b, _) -> uids[b]!! }
+ .filterKeys { it in selectedBundles }
- val patchList = selectedPatches.flatMap { (bundle, selected) ->
- allPatches[bundle]?.filter { it.name in selected }
- ?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
- }
+ val patchList = selectedPatches.flatMap { (bundle, selected) ->
+ allPatches[bundle]?.filter { it.name in selected }
+ ?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
+ }
- // Set all patch options.
- options.forEach { (bundle, bundlePatchOptions) ->
- val patches = allPatches[bundle] ?: return@forEach
- bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
- val patchOptions = patches.single { it.name == patchName }.options
- configuredPatchOptions.forEach { (key, value) ->
- patchOptions[key] = value
+ // Set all patch options.
+ options.forEach { (bundle, bundlePatchOptions) ->
+ val patches = allPatches[bundle] ?: return@forEach
+ bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
+ val patchOptions = patches.single { it.name == patchName }.options
+ configuredPatchOptions.forEach { (key, value) ->
+ patchOptions[key] = value
+ }
}
}
+
+ patchList
}
- onProgress(null, State.COMPLETED, null) // Loading patches
+ val session = runStep(StepId.ReadAPK, onEvent) {
+ Session(
+ cacheDir,
+ frameworkPath,
+ aaptPath,
+ logger,
+ File(inputFile),
+ onEvent,
+ )
+ }
- Session(
- cacheDir,
- frameworkPath,
- aaptPath,
- context,
- logger,
- File(inputFile),
- onPatchCompleted = onPatchCompleted,
- onProgress
- ).use { session ->
- session.run(
+ session.use { s ->
+ s.run(
File(outputFile),
patchList
)
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt
index 2e026298..c8597eca 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/ProcessRuntime.kt
@@ -10,12 +10,13 @@ import app.revanced.manager.BuildConfig
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
import app.revanced.manager.patcher.LibraryResolver
+import app.revanced.manager.patcher.ProgressEvent
+import app.revanced.manager.patcher.ProgressEventParcel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.runtime.process.Parameters
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
import app.revanced.manager.patcher.runtime.process.PatcherProcess
-import app.revanced.manager.patcher.worker.ProgressEventHandler
-import app.revanced.manager.ui.model.State
+import app.revanced.manager.patcher.toEvent
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
@@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
- onPatchCompleted: suspend () -> Unit,
- onProgress: ProgressEventHandler,
+ onEvent: (ProgressEvent) -> Unit,
) = coroutineScope {
// Get the location of our own Apk.
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
}
val patching = CompletableDeferred()
- val scope = this
launch(Dispatchers.IO) {
val binder = awaitBinderConnection()
@@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
val eventHandler = object : IPatcherEvents.Stub() {
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
- override fun patchSucceeded() {
- scope.launch { onPatchCompleted() }
+ override fun event(event: ProgressEventParcel?) {
+ event?.let { onEvent(it.toEvent()) }
}
- override fun progress(name: String?, state: String?, msg: String?) =
- onProgress(name, state?.let { enumValueOf(it) }, msg)
-
override fun finished(exceptionStackTrace: String?) {
binder.exit()
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt
index 7f4616bc..67da1ee0 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/Runtime.kt
@@ -4,9 +4,9 @@ import android.content.Context
import app.revanced.manager.data.platform.Filesystem
import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchBundleRepository
+import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.aapt.Aapt
import app.revanced.manager.patcher.logger.Logger
-import app.revanced.manager.patcher.worker.ProgressEventHandler
import app.revanced.manager.util.Options
import app.revanced.manager.util.PatchSelection
import kotlinx.coroutines.flow.first
@@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent {
selectedPatches: PatchSelection,
options: Options,
logger: Logger,
- onPatchCompleted: suspend () -> Unit,
- onProgress: ProgressEventHandler,
+ onEvent: (ProgressEvent) -> Unit,
)
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt
index f117f201..ab9f5229 100644
--- a/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/runtime/process/PatcherProcess.kt
@@ -8,12 +8,15 @@ import android.os.Build
import android.os.Bundle
import android.os.Looper
import app.revanced.manager.BuildConfig
+import app.revanced.manager.patcher.ProgressEvent
import app.revanced.manager.patcher.Session
+import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.patch.PatchBundle
+import app.revanced.manager.patcher.runStep
import app.revanced.manager.patcher.runtime.ProcessRuntime
-import app.revanced.manager.ui.model.State
+import app.revanced.manager.patcher.toParcel
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -24,7 +27,7 @@ import kotlin.system.exitProcess
/**
* The main class that runs inside the runner process launched by [ProcessRuntime].
*/
-class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
+class PatcherProcess() : IPatcherProcess.Stub() {
private var eventBinder: IPatcherEvents? = null
private val scope =
@@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
override fun exit() = exitProcess(0)
override fun start(parameters: Parameters, events: IPatcherEvents) {
+ fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
+
eventBinder = events
scope.launch {
@@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
- val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
- val patchList = parameters.configurations.flatMap { config ->
- val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
+ val patchList = runStep(StepId.LoadPatches, ::onEvent) {
+ val allPatches = PatchBundle.Loader.patches(
+ parameters.configurations.map { it.bundle },
+ parameters.packageName
+ )
+
+ parameters.configurations.flatMap { config ->
+ val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
.filter { it.name in config.patches }
.associateBy { it.name }
- config.options.forEach { (patchName, opts) ->
- val patchOptions = patches[patchName]?.options
- ?: throw Exception("Patch with name $patchName does not exist.")
+ config.options.forEach { (patchName, opts) ->
+ val patchOptions = patches[patchName]?.options
+ ?: throw Exception("Patch with name $patchName does not exist.")
- opts.forEach { (key, value) ->
- patchOptions[key] = value
+ opts.forEach { (key, value) ->
+ patchOptions[key] = value
+ }
}
- }
- patches.values
+ patches.values
+ }
}
- events.progress(null, State.COMPLETED.name, null) // Loading patches
+ val session = runStep(StepId.ReadAPK, ::onEvent) {
+ Session(
+ cacheDir = parameters.cacheDir,
+ aaptPath = parameters.aaptPath,
+ frameworkDir = parameters.frameworkDir,
+ logger = logger,
+ input = File(parameters.inputFile),
+ onEvent = ::onEvent,
+ )
+ }
- Session(
- cacheDir = parameters.cacheDir,
- aaptPath = parameters.aaptPath,
- frameworkDir = parameters.frameworkDir,
- androidContext = context,
- logger = logger,
- input = File(parameters.inputFile),
- onPatchCompleted = { events.patchSucceeded() },
- onProgress = { name, state, message ->
- events.progress(name, state?.name, message)
- }
- ).use {
+ session.use {
it.run(File(parameters.outputFile), patchList)
}
@@ -119,7 +128,7 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
}
}
- val ipcInterface = PatcherProcess(appContext)
+ val ipcInterface = PatcherProcess()
appContext.sendBroadcast(Intent().apply {
action = ProcessRuntime.CONNECT_TO_APP_ACTION
diff --git a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
index 0708817d..7076e122 100644
--- a/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
+++ b/app/src/main/java/app/revanced/manager/patcher/worker/PatcherWorker.kt
@@ -29,14 +29,17 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.Worker
import app.revanced.manager.domain.worker.WorkerRepository
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
+import app.revanced.manager.patcher.ProgressEvent
+import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.Logger
+import app.revanced.manager.patcher.runStep
import app.revanced.manager.patcher.runtime.CoroutineRuntime
import app.revanced.manager.patcher.runtime.ProcessRuntime
+import app.revanced.manager.patcher.toRemoteError
import app.revanced.manager.plugin.downloader.GetScope
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
import app.revanced.manager.ui.model.SelectedApp
-import app.revanced.manager.ui.model.State
import app.revanced.manager.util.Options
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
@@ -48,8 +51,6 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
-typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
-
@OptIn(PluginHostApi::class)
class PatcherWorker(
context: Context,
@@ -71,11 +72,9 @@ class PatcherWorker(
val selectedPatches: PatchSelection,
val options: Options,
val logger: Logger,
- val onDownloadProgress: suspend (Pair?) -> Unit,
- val onPatchCompleted: suspend () -> Unit,
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
val setInputFile: suspend (File) -> Unit,
- val onProgress: ProgressEventHandler
+ val onEvent: (ProgressEvent) -> Unit,
) {
val packageName get() = input.packageName
}
@@ -140,10 +139,6 @@ class PatcherWorker(
}
private suspend fun runPatcher(args: Args): Result {
-
- fun updateProgress(name: String? = null, state: State? = null, message: String? = null) =
- args.onProgress(name, state, message)
-
val patchedApk = fs.tempDir.resolve("patched.apk")
return try {
@@ -163,51 +158,65 @@ class PatcherWorker(
args.input.version,
prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(),
- onDownload = args.onDownloadProgress
- ).also {
- args.setInputFile(it)
- updateProgress(state = State.COMPLETED) // Download APK
- }
+ onDownload = { progress ->
+ args.onEvent(
+ ProgressEvent.Progress(
+ stepId = StepId.DownloadAPK,
+ current = progress.first,
+ total = progress.second
+ )
+ )
+ }
+ ).also { args.setInputFile(it) }
val inputFile = when (val selectedApp = args.input) {
is SelectedApp.Download -> {
- val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
+ runStep(StepId.DownloadAPK, args.onEvent) {
+ val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
+ selectedApp.data
+ )
- download(plugin, data)
+ download(plugin, data)
+ }
}
is SelectedApp.Search -> {
- downloaderPluginRepository.loadedPluginsFlow.first()
- .firstNotNullOfOrNull { plugin ->
- try {
- val getScope = object : GetScope {
- override val pluginPackageName = plugin.packageName
- override val hostPackageName = applicationContext.packageName
- override suspend fun requestStartActivity(intent: Intent): Intent? {
- val result = args.handleStartActivityRequest(plugin, intent)
- return when (result.resultCode) {
- Activity.RESULT_OK -> result.data
- Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
- else -> throw UserInteractionException.Activity.NotCompleted(
- result.resultCode,
- result.data
- )
+ runStep(StepId.DownloadAPK, args.onEvent) {
+ downloaderPluginRepository.loadedPluginsFlow.first()
+ .firstNotNullOfOrNull { plugin ->
+ try {
+ val getScope = object : GetScope {
+ override val pluginPackageName = plugin.packageName
+ override val hostPackageName =
+ applicationContext.packageName
+
+ override suspend fun requestStartActivity(intent: Intent): Intent? {
+ val result =
+ args.handleStartActivityRequest(plugin, intent)
+ return when (result.resultCode) {
+ Activity.RESULT_OK -> result.data
+ Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
+ else -> throw UserInteractionException.Activity.NotCompleted(
+ result.resultCode,
+ result.data
+ )
+ }
}
}
- }
- withContext(Dispatchers.IO) {
- plugin.get(
- getScope,
- selectedApp.packageName,
- selectedApp.version
- )
- }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
- } catch (e: UserInteractionException.Activity.NotCompleted) {
- throw e
- } catch (_: UserInteractionException) {
- null
- }?.let { (data, _) -> download(plugin, data) }
- } ?: throw Exception("App is not available.")
+ withContext(Dispatchers.IO) {
+ plugin.get(
+ getScope,
+ selectedApp.packageName,
+ selectedApp.version
+ )
+ }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
+ } catch (e: UserInteractionException.Activity.NotCompleted) {
+ throw e
+ } catch (_: UserInteractionException) {
+ null
+ }?.let { (data, _) -> download(plugin, data) }
+ } ?: throw Exception("App is not available.")
+ }
}
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
@@ -227,12 +236,12 @@ class PatcherWorker(
args.selectedPatches,
args.options,
args.logger,
- args.onPatchCompleted,
- args.onProgress
+ args.onEvent,
)
- keystoreManager.sign(patchedApk, File(args.output))
- updateProgress(state = State.COMPLETED) // Signing
+ runStep(StepId.SignAPK, args.onEvent) {
+ keystoreManager.sign(patchedApk, File(args.output))
+ }
Log.i(tag, "Patching succeeded".logFmt())
Result.success()
@@ -241,11 +250,11 @@ class PatcherWorker(
tag,
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
)
- updateProgress(state = State.FAILED, message = e.originalStackTrace)
+ args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
Result.failure()
} catch (e: Exception) {
Log.e(tag, "An exception occurred while patching".logFmt(), e)
- updateProgress(state = State.FAILED, message = e.stackTraceToString())
+ args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step
Result.failure()
} finally {
patchedApk.delete()
diff --git a/app/src/main/java/app/revanced/manager/service/InstallService.kt b/app/src/main/java/app/revanced/manager/service/InstallService.kt
deleted file mode 100644
index 7bf2d213..00000000
--- a/app/src/main/java/app/revanced/manager/service/InstallService.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package app.revanced.manager.service
-
-import android.app.Service
-import android.content.Intent
-import android.content.pm.PackageInstaller
-import android.os.Build
-import android.os.IBinder
-
-@Suppress("DEPRECATION")
-class InstallService : Service() {
-
- override fun onStartCommand(
- intent: Intent, flags: Int, startId: Int
- ): Int {
- val extraStatus = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)
- val extraStatusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
- val extraPackageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
- when (extraStatus) {
- PackageInstaller.STATUS_PENDING_USER_ACTION -> {
- startActivity(if (Build.VERSION.SDK_INT >= 33) {
- intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
- } else {
- intent.getParcelableExtra(Intent.EXTRA_INTENT)
- }.apply {
- this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- })
- }
-
- else -> {
- sendBroadcast(Intent().apply {
- action = APP_INSTALL_ACTION
- `package` = packageName
- putExtra(EXTRA_INSTALL_STATUS, extraStatus)
- putExtra(EXTRA_INSTALL_STATUS_MESSAGE, extraStatusMessage)
- putExtra(EXTRA_PACKAGE_NAME, extraPackageName)
- })
- }
- }
- stopSelf()
- return START_NOT_STICKY
- }
-
- override fun onBind(intent: Intent?): IBinder? = null
-
- companion object {
- const val APP_INSTALL_ACTION = "APP_INSTALL_ACTION"
-
- const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
- const val EXTRA_INSTALL_STATUS_MESSAGE = "EXTRA_INSTALL_STATUS_MESSAGE"
- const val EXTRA_PACKAGE_NAME = "EXTRA_PACKAGE_NAME"
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt
index 2ae48ce6..d881bae5 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/InstallerStatusDialog.kt
@@ -1,5 +1,6 @@
package app.revanced.manager.ui.component
+import android.annotation.SuppressLint
import android.content.pm.PackageInstaller
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
@@ -79,7 +80,7 @@ private fun installerStatusDialogButton(
enum class DialogKind(
val flag: Int,
val title: Int,
- @StringRes val contentStringResId: Int,
+ @param:StringRes val contentStringResId: Int,
val icon: ImageVector = Icons.Outlined.ErrorOutline,
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
val dismissButton: InstallerStatusDialogButton? = null,
@@ -133,10 +134,8 @@ enum class DialogKind(
title = R.string.installation_storage_issue_dialog_title,
contentStringResId = R.string.installation_storage_issue_description,
),
-
- @RequiresApi(34)
FAILURE_TIMEOUT(
- flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
+ flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
title = R.string.installation_timeout_dialog_title,
contentStringResId = R.string.installation_timeout_description,
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
diff --git a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
index 537dd84c..fd62ecca 100644
--- a/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
+++ b/app/src/main/java/app/revanced/manager/ui/component/patcher/Steps.kt
@@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp
import app.revanced.manager.R
import app.revanced.manager.ui.component.ArrowButton
import app.revanced.manager.ui.component.LoadingIndicator
-import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.State
-import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
-import app.revanced.manager.ui.model.StepProgressProvider
+import app.revanced.manager.ui.model.Step
import java.util.Locale
import kotlin.math.floor
@@ -52,8 +50,6 @@ import kotlin.math.floor
fun Steps(
category: StepCategory,
steps: List,
- stepCount: Pair? = null,
- stepProgressProvider: StepProgressProvider,
isExpanded: Boolean = false,
onExpand: () -> Unit,
onClick: () -> Unit
@@ -67,8 +63,17 @@ fun Steps(
}
}
+ val filteredSteps = remember(steps) {
+ val failedCount = steps.count { it.state == State.FAILED }
+
+ steps.filter { step ->
+ // Show hidden steps if it's the only failed step.
+ !step.hide || (step.state == State.FAILED && failedCount == 1)
+ }
+ }
+
LaunchedEffect(state) {
- if (state == State.RUNNING)
+ if (state == State.RUNNING || state == State.FAILED)
onExpand()
}
@@ -92,13 +97,8 @@ fun Steps(
Spacer(modifier = Modifier.weight(1f))
- val stepProgress = remember(stepCount, steps) {
- stepCount?.let { (current, total) -> "$current/$total" }
- ?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
- }
-
Text(
- text = stepProgress,
+ text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
style = MaterialTheme.typography.labelSmall
)
@@ -112,23 +112,20 @@ fun Steps(
.fillMaxWidth()
.padding(top = 10.dp)
) {
- steps.forEachIndexed { index, step ->
- val (progress, progressText) = when (step.progressKey) {
- null -> null
- ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
- if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
- else null to "${downloaded.megaBytes} MB"
- }
+ filteredSteps.forEachIndexed { index, step ->
+ val (progress, progressText) = step.progress?.let { (current, total) ->
+ if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
+ else null to "${current.megaBytes} MB"
} ?: (null to null)
SubStep(
- name = step.name,
+ name = step.title,
state = step.state,
message = step.message,
progress = progress,
progressText = progressText,
isFirst = index == 0,
- isLast = index == steps.lastIndex,
+ isLast = index == filteredSteps.lastIndex,
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
index 3dbb390e..46403d72 100644
--- a/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
+++ b/app/src/main/java/app/revanced/manager/ui/model/PatcherStep.kt
@@ -3,6 +3,7 @@ package app.revanced.manager.ui.model
import android.os.Parcelable
import androidx.annotation.StringRes
import app.revanced.manager.R
+import app.revanced.manager.patcher.StepId
import kotlinx.parcelize.Parcelize
enum class StepCategory(@StringRes val displayName: Int) {
@@ -15,19 +16,20 @@ enum class State {
WAITING, RUNNING, FAILED, COMPLETED
}
-enum class ProgressKey {
- DOWNLOAD
-}
-
-interface StepProgressProvider {
- val downloadProgress: Pair?
-}
-
@Parcelize
data class Step(
- val name: String,
+ val id: StepId,
+ val title: String,
val category: StepCategory,
val state: State = State.WAITING,
val message: String? = null,
- val progressKey: ProgressKey? = null
-) : Parcelable
\ No newline at end of file
+ val progress: Pair? = null,
+ val hide: Boolean = false,
+) : Parcelable
+
+
+fun Step.withState(
+ state: State = this.state,
+ message: String? = this.message,
+ progress: Pair? = this.progress
+) = copy(state = state, message = message, progress = progress)
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
index c3839551..f3a9f059 100644
--- a/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
+++ b/app/src/main/java/app/revanced/manager/ui/screen/PatcherScreen.kt
@@ -87,7 +87,7 @@ fun PatcherScreen(
val steps by remember {
derivedStateOf {
- viewModel.steps.groupBy { it.category }
+ viewModel.steps.groupBy { it.category }.toList()
}
}
@@ -230,14 +230,12 @@ fun PatcherScreen(
contentPadding = PaddingValues(16.dp)
) {
items(
- items = steps.toList(),
+ items = steps,
key = { it.first }
) { (category, steps) ->
Steps(
category = category,
steps = steps,
- stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
- stepProgressProvider = viewModel,
isExpanded = expandedCategory == category,
onExpand = { expandCategory(category) },
onClick = {
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt
index 27b86263..a2df550c 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/InstalledAppInfoViewModel.kt
@@ -1,17 +1,11 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
import android.content.pm.PackageInfo
-import android.content.pm.PackageInstaller
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
@@ -19,7 +13,6 @@ import app.revanced.manager.data.room.apps.installed.InstallType
import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
-import app.revanced.manager.service.UninstallService
import app.revanced.manager.util.PM
import app.revanced.manager.util.PatchSelection
import app.revanced.manager.util.simpleMessage
@@ -30,6 +23,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
+import ru.solrudev.ackpine.session.Session
+import ru.solrudev.ackpine.uninstaller.UninstallFailure
class InstalledAppInfoViewModel(
packageName: String
@@ -87,51 +82,28 @@ class InstalledAppInfoViewModel(
fun uninstall() {
val app = installedApp ?: return
- when (app.installType) {
- InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
-
- InstallType.MOUNT -> viewModelScope.launch {
- rootInstaller.uninstall(app.currentPackageName)
- installedAppRepository.delete(app)
- onBackClick()
- }
- }
- }
-
- private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- when (intent?.action) {
- UninstallService.APP_UNINSTALL_ACTION -> {
- val extraStatus =
- intent.getIntExtra(UninstallService.EXTRA_UNINSTALL_STATUS, -999)
- val extraStatusMessage =
- intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
-
- if (extraStatus == PackageInstaller.STATUS_SUCCESS) {
- viewModelScope.launch {
- installedApp?.let {
- installedAppRepository.delete(it)
- }
- onBackClick()
+ viewModelScope.launch {
+ when (app.installType) {
+ InstallType.DEFAULT -> {
+ when (val result = pm.uninstallPackage(app.currentPackageName)) {
+ is Session.State.Failed -> {
+ val msg = result.failure.message.orEmpty()
+ context.toast(
+ this@InstalledAppInfoViewModel.context.getString(
+ R.string.uninstall_app_fail,
+ msg
+ )
+ )
+ return@launch
}
- } else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
- this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
+ Session.State.Succeeded -> {}
}
-
}
- }
- }
- }.also {
- ContextCompat.registerReceiver(
- context,
- it,
- IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
- ContextCompat.RECEIVER_NOT_EXPORTED
- )
- }
- override fun onCleared() {
- super.onCleared()
- context.unregisterReceiver(uninstallBroadcastReceiver)
+ InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
+ }
+ installedAppRepository.delete(app)
+ onBackClick()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
index 236129c5..194f4214 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatcherViewModel.kt
@@ -1,11 +1,9 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
-import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.PackageInstaller
+import android.content.pm.PackageInstaller as AndroidPackageInstaller
import android.net.Uri
import android.os.ParcelUuid
import android.util.Log
@@ -16,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
-import androidx.core.content.ContextCompat
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.map
@@ -32,32 +29,35 @@ import app.revanced.manager.data.room.apps.installed.InstalledApp
import app.revanced.manager.domain.installer.RootInstaller
import app.revanced.manager.domain.repository.InstalledAppRepository
import app.revanced.manager.domain.worker.WorkerRepository
+import app.revanced.manager.patcher.ProgressEvent
+import app.revanced.manager.patcher.StepId
import app.revanced.manager.patcher.logger.LogLevel
import app.revanced.manager.patcher.logger.Logger
import app.revanced.manager.patcher.worker.PatcherWorker
import app.revanced.manager.plugin.downloader.PluginHostApi
import app.revanced.manager.plugin.downloader.UserInteractionException
-import app.revanced.manager.service.InstallService
-import app.revanced.manager.service.UninstallService
import app.revanced.manager.ui.model.InstallerModel
-import app.revanced.manager.ui.model.ProgressKey
import app.revanced.manager.ui.model.SelectedApp
import app.revanced.manager.ui.model.State
-import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
-import app.revanced.manager.ui.model.StepProgressProvider
+import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.navigation.Patcher
+import app.revanced.manager.ui.model.withState
import app.revanced.manager.util.PM
+import app.revanced.manager.util.PatchSelection
+import app.revanced.manager.util.asCode
import app.revanced.manager.util.saveableVar
import app.revanced.manager.util.saver.snapshotStateListSaver
-import app.revanced.manager.util.simpleMessage
-import app.revanced.manager.util.tag
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
@@ -66,6 +66,15 @@ import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
+import ru.solrudev.ackpine.installer.InstallFailure
+import ru.solrudev.ackpine.installer.PackageInstaller
+import ru.solrudev.ackpine.installer.createSession
+import ru.solrudev.ackpine.installer.getSession
+import ru.solrudev.ackpine.session.ProgressSession
+import ru.solrudev.ackpine.session.Session
+import ru.solrudev.ackpine.session.await
+import ru.solrudev.ackpine.session.parameters.Confirmation
+import ru.solrudev.ackpine.uninstaller.UninstallFailure
import java.io.File
import java.nio.file.Files
import java.time.Duration
@@ -73,7 +82,7 @@ import java.time.Duration
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
class PatcherViewModel(
private val input: Patcher.ViewModelParams
-) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
+) : ViewModel(), KoinComponent, InstallerModel {
private val app: Application by inject()
private val fs: Filesystem by inject()
private val pm: PM by inject()
@@ -81,6 +90,7 @@ class PatcherViewModel(
private val installedAppRepository: InstalledAppRepository by inject()
private val rootInstaller: RootInstaller by inject()
private val savedStateHandle: SavedStateHandle = get()
+ private val ackpineInstaller: PackageInstaller = get()
private var installedApp: InstalledApp? = null
private val selectedApp = input.selectedApp
@@ -95,7 +105,6 @@ class PatcherViewModel(
mutableStateOf(null)
}
private set
- private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
var packageInstallerStatus: Int? by savedStateHandle.saveable(
key = "packageInstallerStatus",
stateSaver = autoSaver()
@@ -104,7 +113,7 @@ class PatcherViewModel(
}
private set
- var isInstalling by mutableStateOf(ongoingPmSession)
+ var isInstalling by mutableStateOf(false)
private set
private var currentActivityRequest: Pair, String>? by mutableStateOf(
@@ -123,6 +132,18 @@ class PatcherViewModel(
}
}
+ /**
+ * This coroutine scope is used to await installations.
+ * It should not be cancelled on system-initiated process death since that would cancel the installation process.
+ */
+ private val installerCoroutineScope = CoroutineScope(Dispatchers.Main)
+
+ /**
+ * Holds the package name of the Apk we are trying to install.
+ */
+ private var installerPkgName: String by savedStateHandle.saveableVar { "" }
+ private var installerSessionId: ParcelUuid? by savedStateHandle.saveableVar()
+
private var inputFile: File? by savedStateHandle.saveableVar()
private val outputFile = tempDir.resolve("output.apk")
@@ -138,35 +159,15 @@ class PatcherViewModel(
}
}
- private val patchCount = input.selectedPatches.values.sumOf { it.size }
- private var completedPatchCount by savedStateHandle.saveable {
- // SavedStateHandle.saveable only supports the boxed version.
- @Suppress("AutoboxingStateCreation") mutableStateOf(
- 0
- )
- }
- val patchesProgress get() = completedPatchCount to patchCount
- override var downloadProgress by savedStateHandle.saveable(
- key = "downloadProgress",
- stateSaver = autoSaver()
- ) {
- mutableStateOf?>(null)
- }
- private set
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
- generateSteps(
- app,
- input.selectedApp
- ).toMutableStateList()
+ generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
}
- private var currentStepIndex = 0
val progress by derivedStateOf {
- val current = steps.count {
- it.state == State.COMPLETED && it.category != StepCategory.PATCHING
- } + completedPatchCount
+ val steps = steps.filter { it.id != StepId.ExecutePatches }
- val total = steps.size - 1 + patchCount
+ val current = steps.count { it.state == State.COMPLETED }
+ val total = steps.size
current.toFloat() / total.toFloat()
}
@@ -174,67 +175,46 @@ class PatcherViewModel(
private val workManager = WorkManager.getInstance(app)
private val patcherWorkerId by savedStateHandle.saveable {
- ParcelUuid(workerRepository.launchExpedited(
- "patching", PatcherWorker.Args(
- input.selectedApp,
- outputFile.path,
- input.selectedPatches,
- input.options,
- logger,
- onDownloadProgress = {
- withContext(Dispatchers.Main) {
- downloadProgress = it
- }
- },
- onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
- setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
- handleStartActivityRequest = { plugin, intent ->
- withContext(Dispatchers.Main) {
- if (currentActivityRequest != null) throw Exception("Another request is already pending.")
- try {
- // Wait for the dialog interaction.
- val accepted = with(CompletableDeferred()) {
- currentActivityRequest = this to plugin.name
-
- await()
- }
- if (!accepted) throw UserInteractionException.RequestDenied()
-
- // Launch the activity and wait for the result.
+ ParcelUuid(
+ workerRepository.launchExpedited(
+ "patching", PatcherWorker.Args(
+ input.selectedApp,
+ outputFile.path,
+ input.selectedPatches,
+ input.options,
+ logger,
+ setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
+ handleStartActivityRequest = { plugin, intent ->
+ withContext(Dispatchers.Main) {
+ if (currentActivityRequest != null) throw Exception("Another request is already pending.")
try {
- with(CompletableDeferred()) {
- launchedActivity = this
- launchActivityChannel.send(intent)
+ // Wait for the dialog interaction.
+ val accepted = with(CompletableDeferred()) {
+ currentActivityRequest = this to plugin.name
+
await()
}
+ if (!accepted) throw UserInteractionException.RequestDenied()
+
+ // Launch the activity and wait for the result.
+ try {
+ with(CompletableDeferred()) {
+ launchedActivity = this
+ launchActivityChannel.send(intent)
+ await()
+ }
+ } finally {
+ launchedActivity = null
+ }
} finally {
- launchedActivity = null
+ currentActivityRequest = null
}
- } finally {
- currentActivityRequest = null
}
- }
- },
- onProgress = { name, state, message ->
- viewModelScope.launch {
- steps[currentStepIndex] = steps[currentStepIndex].run {
- copy(
- name = name ?: this.name,
- state = state ?: this.state,
- message = message ?: this.message
- )
- }
-
- if (state == State.COMPLETED && currentStepIndex != steps.lastIndex) {
- currentStepIndex++
-
- steps[currentStepIndex] =
- steps[currentStepIndex].copy(state = State.RUNNING)
- }
- }
- }
+ },
+ onEvent = ::handleProgressEvent,
+ )
)
- ))
+ )
}
val patcherSucceeded =
@@ -246,64 +226,26 @@ class PatcherViewModel(
}
}
- private val installerBroadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- when (intent?.action) {
- InstallService.APP_INSTALL_ACTION -> {
- val pmStatus = intent.getIntExtra(
- InstallService.EXTRA_INSTALL_STATUS,
- PackageInstaller.STATUS_FAILURE
- )
+ init {
+ // TODO: detect system-initiated process death during the patching process.
- intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
- ?.let(logger::trace)
-
- if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
- app.toast(app.getString(R.string.install_app_success))
- installedPackageName =
- intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
- viewModelScope.launch {
- installedAppRepository.addOrUpdate(
- installedPackageName!!,
- packageName,
- input.selectedApp.version
- ?: pm.getPackageInfo(outputFile)?.versionName!!,
- InstallType.DEFAULT,
- input.selectedPatches
- )
+ installerSessionId?.uuid?.let { id ->
+ viewModelScope.launch {
+ try {
+ isInstalling = true
+ uiSafe(app, R.string.install_app_fail, "Failed to install") {
+ // The process was killed during installation. Await the session again.
+ withContext(Dispatchers.IO) {
+ ackpineInstaller.getSession(id)
+ }?.let {
+ awaitInstallation(it)
}
- } else packageInstallerStatus = pmStatus
-
+ }
+ } finally {
isInstalling = false
}
-
- UninstallService.APP_UNINSTALL_ACTION -> {
- val pmStatus = intent.getIntExtra(
- UninstallService.EXTRA_UNINSTALL_STATUS,
- PackageInstaller.STATUS_FAILURE
- )
-
- intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
- ?.let(logger::trace)
-
- if (pmStatus != PackageInstaller.STATUS_SUCCESS)
- packageInstallerStatus = pmStatus
- }
}
}
- }
-
- init {
- // TODO: detect system-initiated process death during the patching process.
- ContextCompat.registerReceiver(
- app,
- installerBroadcastReceiver,
- IntentFilter().apply {
- addAction(InstallService.APP_INSTALL_ACTION)
- addAction(UninstallService.APP_UNINSTALL_ACTION)
- },
- ContextCompat.RECEIVER_NOT_EXPORTED
- )
viewModelScope.launch {
installedApp = installedAppRepository.get(packageName)
@@ -313,7 +255,6 @@ class PatcherViewModel(
@OptIn(DelicateCoroutinesApi::class)
override fun onCleared() {
super.onCleared()
- app.unregisterReceiver(installerBroadcastReceiver)
workManager.cancelWorkById(patcherWorkerId.uuid)
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
@@ -327,7 +268,37 @@ class PatcherViewModel(
}
}
+ private fun handleProgressEvent(event: ProgressEvent) = viewModelScope.launch {
+ val stepIndex = steps.indexOfFirst {
+ event.stepId?.let { id -> id == it.id }
+ ?: (it.state == State.RUNNING || it.state == State.WAITING)
+ }
+
+ if (stepIndex != -1) steps[stepIndex] = steps[stepIndex].run {
+ when (event) {
+ is ProgressEvent.Started -> withState(State.RUNNING)
+
+ is ProgressEvent.Progress -> withState(
+ message = event.message ?: message,
+ progress = event.current?.let { event.current to event.total } ?: progress
+ )
+
+ is ProgressEvent.Completed -> withState(State.COMPLETED, progress = null)
+
+ is ProgressEvent.Failed -> {
+ if (event.stepId == null && steps.any { it.state == State.FAILED }) return@launch
+ withState(
+ State.FAILED,
+ message = event.error.stackTrace,
+ progress = null
+ )
+ }
+ }
+ }
+ }
+
fun onBack() {
+ installerCoroutineScope.cancel()
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
tempDir.deleteRecursively()
}
@@ -372,44 +343,93 @@ class PatcherViewModel(
fun open() = installedPackageName?.let(pm::launch)
- fun install(installType: InstallType) = viewModelScope.launch {
- var pmInstallStarted = false
- try {
- isInstalling = true
+ private suspend fun startInstallation(file: File, packageName: String) {
+ val session = withContext(Dispatchers.IO) {
+ ackpineInstaller.createSession(Uri.fromFile(file)) {
+ confirmation = Confirmation.IMMEDIATE
+ }
+ }
+ withContext(Dispatchers.Main) {
+ installerPkgName = packageName
+ }
+ awaitInstallation(session)
+ }
- val currentPackageInfo = pm.getPackageInfo(outputFile)
- ?: throw Exception("Failed to load application info")
-
- // If the app is currently installed
- val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
- if (existingPackageInfo != null) {
- // Check if the app version is less than the installed version
- if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
- // Exit if the selected app version is less than the installed version
- packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
- return@launch
+ private suspend fun awaitInstallation(session: ProgressSession) = withContext(
+ Dispatchers.Main
+ ) {
+ val result = installerCoroutineScope.async {
+ try {
+ installerSessionId = ParcelUuid(session.id)
+ withContext(Dispatchers.IO) {
+ session.await()
}
+ } finally {
+ installerSessionId = null
+ }
+ }.await()
+
+ when (result) {
+ is Session.State.Failed -> {
+ result.failure.message?.let(logger::trace)
+ packageInstallerStatus = result.failure.asCode()
}
- when (installType) {
- InstallType.DEFAULT -> {
- // Check if the app is mounted as root
- // If it is, unmount it first, silently
- if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
- rootInstaller.unmount(packageName)
- }
+ Session.State.Succeeded -> {
+ app.toast(app.getString(R.string.install_app_success))
+ installedPackageName = installerPkgName
+ installedAppRepository.addOrUpdate(
+ installerPkgName,
+ packageName,
+ input.selectedApp.version
+ ?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
+ InstallType.DEFAULT,
+ input.selectedPatches
+ )
+ }
+ }
+ }
- // Install regularly
- pm.installApp(listOf(outputFile))
- pmInstallStarted = true
+ fun install(installType: InstallType) = viewModelScope.launch {
+ isInstalling = true
+ var needsRootUninstall = false
+ try {
+ uiSafe(app, R.string.install_app_fail, "Failed to install") {
+ val currentPackageInfo =
+ withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile) }
+ ?: throw Exception("Failed to load application info")
+
+ // If the app is currently installed
+ val existingPackageInfo =
+ withContext(Dispatchers.IO) { pm.getPackageInfo(currentPackageInfo.packageName) }
+ if (existingPackageInfo != null) {
+ // Check if the app version is less than the installed version
+ if (
+ pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(
+ existingPackageInfo
+ )
+ ) {
+ // Exit if the selected app version is less than the installed version
+ packageInstallerStatus = AndroidPackageInstaller.STATUS_FAILURE_CONFLICT
+ return@launch
+ }
}
- InstallType.MOUNT -> {
- try {
- val packageInfo = pm.getPackageInfo(outputFile)
- ?: throw Exception("Failed to load application info")
+ when (installType) {
+ InstallType.DEFAULT -> {
+ // Check if the app is mounted as root
+ // If it is, unmount it first, silently
+ if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
+ rootInstaller.unmount(packageName)
+ }
+
+ // Install regularly
+ startInstallation(outputFile, currentPackageInfo.packageName)
+ }
+
+ InstallType.MOUNT -> {
val label = with(pm) {
- packageInfo.label()
+ currentPackageInfo.label()
}
// Check for base APK, first check if the app is already installed
@@ -417,15 +437,17 @@ class PatcherViewModel(
// If the app is not installed, check if the output file is a base apk
if (currentPackageInfo.splitNames.isNotEmpty()) {
// Exit if there is no base APK package
- packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
+ packageInstallerStatus =
+ AndroidPackageInstaller.STATUS_FAILURE_INVALID
return@launch
}
}
val inputVersion = input.selectedApp.version
- ?: inputFile?.let(pm::getPackageInfo)?.versionName
+ ?: withContext(Dispatchers.IO) { inputFile?.let(pm::getPackageInfo)?.versionName }
?: throw Exception("Failed to determine input APK version")
+ needsRootUninstall = true
// Install as root
rootInstaller.install(
outputFile,
@@ -436,7 +458,7 @@ class PatcherViewModel(
)
installedAppRepository.addOrUpdate(
- packageInfo.packageName,
+ currentPackageInfo.packageName,
packageName,
inputVersion,
InstallType.MOUNT,
@@ -448,21 +470,20 @@ class PatcherViewModel(
installedPackageName = packageName
app.toast(app.getString(R.string.install_app_success))
- } catch (e: Exception) {
- Log.e(tag, "Failed to install as root", e)
- app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
- try {
- rootInstaller.uninstall(packageName)
- } catch (_: Exception) {
- }
+ needsRootUninstall = false
}
}
}
- } catch (e: Exception) {
- Log.e(tag, "Failed to install", e)
- app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
} finally {
- if (!pmInstallStarted) isInstalling = false
+ isInstalling = false
+ if (needsRootUninstall) {
+ try {
+ withContext(NonCancellable) {
+ rootInstaller.uninstall(packageName)
+ }
+ } catch (_: Exception) {
+ }
+ }
}
}
@@ -473,12 +494,27 @@ class PatcherViewModel(
override fun reinstall() {
viewModelScope.launch {
- uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
- pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
- ?: throw Exception("Failed to load application info")
-
- pm.installApp(listOf(outputFile))
+ try {
isInstalling = true
+ uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
+ val pkgName = withContext(Dispatchers.IO) {
+ pm.getPackageInfo(outputFile)?.packageName
+ ?: throw Exception("Failed to load application info")
+ }
+
+ when (val result = pm.uninstallPackage(pkgName)) {
+ is Session.State.Failed -> {
+ result.failure.message?.let(logger::trace)
+ packageInstallerStatus = result.failure.asCode()
+ return@launch
+ }
+
+ Session.State.Succeeded -> {}
+ }
+ startInstallation(outputFile, pkgName)
+ }
+ } finally {
+ isInstalling = false
}
}
}
@@ -497,34 +533,66 @@ class PatcherViewModel(
LogLevel.ERROR -> Log.e(TAG, msg)
}
- fun generateSteps(context: Context, selectedApp: SelectedApp): List {
- val needsDownload =
- selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
+ fun generateSteps(
+ context: Context,
+ selectedApp: SelectedApp,
+ selectedPatches: PatchSelection
+ ): List = buildList {
+ if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
+ add(
+ Step(
+ StepId.DownloadAPK,
+ context.getString(R.string.download_apk),
+ StepCategory.PREPARING
+ )
+ )
- return listOfNotNull(
- Step(
- context.getString(R.string.download_apk),
- StepCategory.PREPARING,
- state = State.RUNNING,
- progressKey = ProgressKey.DOWNLOAD,
- ).takeIf { needsDownload },
+ add(
Step(
+ StepId.LoadPatches,
context.getString(R.string.patcher_step_load_patches),
- StepCategory.PREPARING,
- state = if (needsDownload) State.WAITING else State.RUNNING,
- ),
+ StepCategory.PREPARING
+ )
+ )
+ add(
Step(
+ StepId.ReadAPK,
context.getString(R.string.patcher_step_unpack),
StepCategory.PREPARING
- ),
-
+ )
+ )
+ add(
Step(
+ StepId.ExecutePatches,
context.getString(R.string.execute_patches),
- StepCategory.PATCHING
- ),
+ StepCategory.PATCHING,
+ hide = true
+ )
+ )
- Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
- Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
+ selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
+ add(
+ Step(
+ StepId.ExecutePatch(index),
+ name,
+ StepCategory.PATCHING
+ )
+ )
+ }
+
+ add(
+ Step(
+ StepId.WriteAPK,
+ context.getString(R.string.patcher_step_write_patched),
+ StepCategory.SAVING
+ )
+ )
+ add(
+ Step(
+ StepId.SignAPK,
+ context.getString(R.string.patcher_step_sign_apk),
+ StepCategory.SAVING
+ )
)
}
}
diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt
index db31d654..186d9619 100644
--- a/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt
+++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/UpdateViewModel.kt
@@ -1,18 +1,13 @@
package app.revanced.manager.ui.viewmodel
import android.app.Application
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.PackageInstaller
+import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
-import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.revanced.manager.R
@@ -21,8 +16,6 @@ import app.revanced.manager.data.platform.NetworkInfo
import app.revanced.manager.network.api.ReVancedAPI
import app.revanced.manager.network.dto.ReVancedAsset
import app.revanced.manager.network.service.HttpService
-import app.revanced.manager.service.InstallService
-import app.revanced.manager.util.PM
import app.revanced.manager.util.toast
import app.revanced.manager.util.uiSafe
import io.ktor.client.plugins.onDownload
@@ -31,7 +24,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
import org.koin.core.component.inject
+import ru.solrudev.ackpine.installer.InstallFailure
+import ru.solrudev.ackpine.installer.PackageInstaller
+import ru.solrudev.ackpine.installer.createSession
+import ru.solrudev.ackpine.session.Session
+import ru.solrudev.ackpine.session.await
+import ru.solrudev.ackpine.session.parameters.Confirmation
class UpdateViewModel(
private val downloadOnScreenEntry: Boolean
@@ -39,10 +39,11 @@ class UpdateViewModel(
private val app: Application by inject()
private val reVancedAPI: ReVancedAPI by inject()
private val http: HttpService by inject()
- private val pm: PM by inject()
private val networkInfo: NetworkInfo by inject()
private val fs: Filesystem by inject()
+ private val ackpineInstaller: PackageInstaller = get()
+ // TODO: save state to handle process death.
var downloadedSize by mutableLongStateOf(0L)
private set
var totalSize by mutableLongStateOf(0L)
@@ -62,14 +63,17 @@ class UpdateViewModel(
private set
private val location = fs.tempDir.resolve("updater.apk")
- private val job = viewModelScope.launch {
- uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
- releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
- if (downloadOnScreenEntry) {
- downloadUpdate()
- } else {
- state = State.CAN_DOWNLOAD
+ init {
+ viewModelScope.launch {
+ uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
+ releaseInfo = reVancedAPI.getAppUpdate() ?: throw Exception("No update available")
+
+ if (downloadOnScreenEntry) {
+ downloadUpdate()
+ } else {
+ state = State.CAN_DOWNLOAD
+ }
}
}
}
@@ -98,50 +102,36 @@ class UpdateViewModel(
fun installUpdate() = viewModelScope.launch {
state = State.INSTALLING
+ val result = withContext(Dispatchers.IO) {
+ ackpineInstaller.createSession(Uri.fromFile(location)) {
+ confirmation = Confirmation.IMMEDIATE
+ }.await()
+ }
- pm.installApp(listOf(location))
- }
-
- private val installBroadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- intent?.let {
- val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
- val extra =
- intent.getStringExtra(InstallService.EXTRA_INSTALL_STATUS_MESSAGE)!!
-
- when(pmStatus) {
- PackageInstaller.STATUS_SUCCESS -> {
- app.toast(app.getString(R.string.install_app_success))
- state = State.SUCCESS
- }
- PackageInstaller.STATUS_FAILURE_ABORTED -> {
- state = State.CAN_INSTALL
- }
- else -> {
- app.toast(app.getString(R.string.install_app_fail, extra))
- installError = extra
- state = State.FAILED
- }
+ when (result) {
+ is Session.State.Failed -> when (val failure = result.failure) {
+ is InstallFailure.Aborted -> state = State.CAN_INSTALL
+ else -> {
+ val msg = failure.message.orEmpty()
+ app.toast(app.getString(R.string.install_app_fail, msg))
+ installError = msg
+ state = State.FAILED
}
}
+
+ Session.State.Succeeded -> {
+ app.toast(app.getString(R.string.install_app_success))
+ state = State.SUCCESS
+ }
}
}
- init {
- ContextCompat.registerReceiver(app, installBroadcastReceiver, IntentFilter().apply {
- addAction(InstallService.APP_INSTALL_ACTION)
- }, ContextCompat.RECEIVER_NOT_EXPORTED)
- }
-
override fun onCleared() {
super.onCleared()
- app.unregisterReceiver(installBroadcastReceiver)
-
- job.cancel()
location.delete()
}
- enum class State(@StringRes val title: Int) {
+ enum class State(@param:StringRes val title: Int) {
CAN_DOWNLOAD(R.string.update_available),
DOWNLOADING(R.string.downloading_manager_update),
CAN_INSTALL(R.string.ready_to_install_update),
diff --git a/app/src/main/java/app/revanced/manager/util/Ackpine.kt b/app/src/main/java/app/revanced/manager/util/Ackpine.kt
new file mode 100644
index 00000000..09e51080
--- /dev/null
+++ b/app/src/main/java/app/revanced/manager/util/Ackpine.kt
@@ -0,0 +1,30 @@
+package app.revanced.manager.util
+
+import android.annotation.SuppressLint
+import android.content.pm.PackageInstaller
+import ru.solrudev.ackpine.installer.InstallFailure
+import ru.solrudev.ackpine.uninstaller.UninstallFailure
+
+/**
+ * Converts an Ackpine installation failure into a PM status code
+ */
+fun InstallFailure.asCode() = when (this) {
+ is InstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
+ is InstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
+ is InstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
+ is InstallFailure.Incompatible -> PackageInstaller.STATUS_FAILURE_INCOMPATIBLE
+ is InstallFailure.Invalid -> PackageInstaller.STATUS_FAILURE_INVALID
+ is InstallFailure.Storage -> PackageInstaller.STATUS_FAILURE_STORAGE
+ is InstallFailure.Timeout -> @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT
+ else -> PackageInstaller.STATUS_FAILURE
+}
+
+/**
+ * Converts an Ackpine uninstallation failure into a PM status code
+ */
+fun UninstallFailure.asCode() = when (this) {
+ is UninstallFailure.Aborted -> PackageInstaller.STATUS_FAILURE_ABORTED
+ is UninstallFailure.Blocked -> PackageInstaller.STATUS_FAILURE_BLOCKED
+ is UninstallFailure.Conflict -> PackageInstaller.STATUS_FAILURE_CONFLICT
+ else -> PackageInstaller.STATUS_FAILURE
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/manager/util/PM.kt b/app/src/main/java/app/revanced/manager/util/PM.kt
index c2f16402..708210a8 100644
--- a/app/src/main/java/app/revanced/manager/util/PM.kt
+++ b/app/src/main/java/app/revanced/manager/util/PM.kt
@@ -2,11 +2,8 @@ package app.revanced.manager.util
import android.annotation.SuppressLint
import android.app.Application
-import android.app.PendingIntent
-import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
-import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PackageInfoFlags
import android.content.pm.PackageManager.NameNotFoundException
@@ -16,8 +13,6 @@ import android.os.Build
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import app.revanced.manager.domain.repository.PatchBundleRepository
-import app.revanced.manager.service.InstallService
-import app.revanced.manager.service.UninstallService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -25,10 +20,13 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
+import ru.solrudev.ackpine.session.await
+import ru.solrudev.ackpine.session.parameters.Confirmation
+import ru.solrudev.ackpine.uninstaller.PackageUninstaller
+import ru.solrudev.ackpine.uninstaller.createSession
+import ru.solrudev.ackpine.uninstaller.parameters.UninstallParametersDsl
import java.io.File
-private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
-
@Immutable
@Parcelize
data class AppInfo(
@@ -40,7 +38,8 @@ data class AppInfo(
@SuppressLint("QueryPermissionsNeeded")
class PM(
private val app: Application,
- patchBundleRepository: PatchBundleRepository
+ patchBundleRepository: PatchBundleRepository,
+ private val uninstaller: PackageUninstaller
) {
private val scope = CoroutineScope(Dispatchers.IO)
@@ -145,17 +144,11 @@ class PM(
false
)
- suspend fun installApp(apks: List) = withContext(Dispatchers.IO) {
- val packageInstaller = app.packageManager.packageInstaller
- packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
- apks.forEach { apk -> session.writeApk(apk) }
- session.commit(app.installIntentSender)
- }
- }
-
- fun uninstallPackage(pkg: String) {
- val packageInstaller = app.packageManager.packageInstaller
- packageInstaller.uninstall(pkg, app.uninstallIntentSender)
+ suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
+ uninstaller.createSession(pkg) {
+ confirmation = Confirmation.IMMEDIATE
+ config()
+ }.await()
}
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
@@ -164,44 +157,4 @@ class PM(
}
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
-
- private fun PackageInstaller.Session.writeApk(apk: File) {
- apk.inputStream().use { inputStream ->
- openWrite(apk.name, 0, apk.length()).use { outputStream ->
- inputStream.copyTo(outputStream, byteArraySize)
- fsync(outputStream)
- }
- }
- }
-
- private val intentFlags
- get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
- PendingIntent.FLAG_MUTABLE
- else
- 0
-
- private val sessionParams
- get() = PackageInstaller.SessionParams(
- PackageInstaller.SessionParams.MODE_FULL_INSTALL
- ).apply {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
- setRequestUpdateOwnership(true)
- setInstallReason(PackageManager.INSTALL_REASON_USER)
- }
-
- private val Context.installIntentSender
- get() = PendingIntent.getService(
- this,
- 0,
- Intent(this, InstallService::class.java),
- intentFlags
- ).intentSender
-
- private val Context.uninstallIntentSender
- get() = PendingIntent.getService(
- this,
- 0,
- Intent(this, UninstallService::class.java),
- intentFlags
- ).intentSender
}
diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt
index 27eb837d..5aeed744 100644
--- a/app/src/main/java/app/revanced/manager/util/Util.kt
+++ b/app/src/main/java/app/revanced/manager/util/Util.kt
@@ -33,6 +33,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import app.revanced.manager.R
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@@ -85,6 +86,8 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
try {
block()
+ } catch (e: CancellationException) {
+ throw e
} catch (error: Exception) {
// You can only toast on the main thread.
GlobalScope.launch(Dispatchers.Main) {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index de453ebb..1ca390cd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -37,6 +37,7 @@ kotlin-process = "1.5.1"
hidden-api-stub = "4.3.3"
binary-compatibility-validator = "0.17.0"
semver-parser = "3.0.0"
+ackpine = "0.18.5"
[libraries]
# AndroidX Core
@@ -133,6 +134,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
# Semantic versioning parser
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-parser" }
+# Ackpine
+ackpine-core = { module = "ru.solrudev.ackpine:ackpine-core", version.ref = "ackpine" }
+ackpine-ktx = { module = "ru.solrudev.ackpine:ackpine-ktx", version.ref = "ackpine" }
+
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }