mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2026-01-19 09:13:57 +00:00
Compare commits
8 Commits
v1.26.0-de
...
v1.26.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffa42099e3 | ||
|
|
11dd6e4064 | ||
|
|
35fb59b31d | ||
|
|
18a4df9af9 | ||
|
|
bd69b45a69 | ||
|
|
0d26df03f4 | ||
|
|
c436a7a100 | ||
|
|
dbb6c01e89 |
@@ -1,3 +1,31 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Update selected patch count when SelectionState changes ([#2896](https://github.com/ReVanced/revanced-manager/issues/2896)) ([0d26df0](https://github.com/ReVanced/revanced-manager/commit/0d26df03f463195dae550240c7f652680763079c))
|
||||||
|
|
||||||
|
# app [1.26.0-dev.13](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.12...v1.26.0-dev.13) (2025-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Make patcher screen design more consistent with inspiration ([#2805](https://github.com/ReVanced/revanced-manager/issues/2805)) ([dbb6c01](https://github.com/ReVanced/revanced-manager/commit/dbb6c01e89a5e710185ff4304de0ac9e19bed053))
|
||||||
|
|
||||||
# app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17)
|
# app [1.26.0-dev.12](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.11...v1.26.0-dev.12) (2025-12-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ dependencies {
|
|||||||
|
|
||||||
// Compose Icons
|
// Compose Icons
|
||||||
implementation(libs.compose.icons.fontawesome)
|
implementation(libs.compose.icons.fontawesome)
|
||||||
|
|
||||||
|
// Ackpine
|
||||||
|
implementation(libs.ackpine.core)
|
||||||
|
implementation(libs.ackpine.ktx)
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
version = 1.26.0-dev.12
|
version = 1.26.0-dev.16
|
||||||
|
|||||||
@@ -51,9 +51,6 @@
|
|||||||
|
|
||||||
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||||
|
|
||||||
<service android:name=".service.InstallService" />
|
|
||||||
<service android:name=".service.UninstallService" />
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="specialUse"
|
android:foregroundServiceType="specialUse"
|
||||||
@@ -75,5 +72,15 @@
|
|||||||
android:value="androidx.startup"
|
android:value="androidx.startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="ru.solrudev.ackpine.AckpineInitializer"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// ProgressEventParcel.aidl
|
||||||
|
package app.revanced.manager.patcher;
|
||||||
|
|
||||||
|
parcelable ProgressEventParcel;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
// IPatcherEvents.aidl
|
// IPatcherEvents.aidl
|
||||||
package app.revanced.manager.patcher.runtime.process;
|
package app.revanced.manager.patcher.runtime.process;
|
||||||
|
|
||||||
|
import app.revanced.manager.patcher.ProgressEventParcel;
|
||||||
|
|
||||||
// Interface for sending events back to the main app process.
|
// Interface for sending events back to the main app process.
|
||||||
oneway interface IPatcherEvents {
|
oneway interface IPatcherEvents {
|
||||||
void log(String level, String msg);
|
void log(String level, String msg);
|
||||||
void patchSucceeded();
|
void event(in ProgressEventParcel event);
|
||||||
void progress(String name, String state, String msg);
|
|
||||||
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||||
void finished(String exceptionStackTrace);
|
void finished(String exceptionStackTrace);
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,8 @@ class ManagerApplication : Application() {
|
|||||||
workerModule,
|
workerModule,
|
||||||
viewModelModule,
|
viewModelModule,
|
||||||
databaseModule,
|
databaseModule,
|
||||||
rootModule
|
rootModule,
|
||||||
|
ackpineModule
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
19
app/src/main/java/app/revanced/manager/di/AckpineModule.kt
Normal file
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <T> 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
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
package app.revanced.manager.patcher
|
package app.revanced.manager.patcher
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import app.revanced.library.ApkUtils.applyTo
|
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.patcher.logger.Logger
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherConfig
|
import app.revanced.patcher.PatcherConfig
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
@@ -22,15 +21,10 @@ class Session(
|
|||||||
cacheDir: String,
|
cacheDir: String,
|
||||||
frameworkDir: String,
|
frameworkDir: String,
|
||||||
aaptPath: String,
|
aaptPath: String,
|
||||||
private val androidContext: Context,
|
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val input: File,
|
private val input: File,
|
||||||
private val onPatchCompleted: suspend () -> Unit,
|
private val onEvent: (ProgressEvent) -> Unit,
|
||||||
private val onProgress: (name: String?, state: State?, message: String?) -> Unit
|
|
||||||
) : Closeable {
|
) : 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 tempDir = File(cacheDir).resolve("patcher").also { it.mkdirs() }
|
||||||
private val patcher = Patcher(
|
private val patcher = Patcher(
|
||||||
PatcherConfig(
|
PatcherConfig(
|
||||||
@@ -42,86 +36,68 @@ class Session(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun Patcher.applyPatchesVerbose(selectedPatches: PatchList) {
|
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) ->
|
this().collect { (patch, exception) ->
|
||||||
if (patch !in selectedPatches) return@collect
|
val index = selectedPatches.indexOf(patch)
|
||||||
|
if (index == -1) return@collect
|
||||||
|
|
||||||
if (exception != null) {
|
if (exception != null) {
|
||||||
updateProgress(
|
onEvent(
|
||||||
name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
|
ProgressEvent.Failed(
|
||||||
state = State.FAILED,
|
StepId.ExecutePatch(index),
|
||||||
message = exception.stackTraceToString()
|
exception.toRemoteError(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error("${patch.name} failed:")
|
logger.error("${patch.name} failed:")
|
||||||
logger.error(exception.stackTraceToString())
|
logger.error(exception.stackTraceToString())
|
||||||
throw exception
|
throw exception
|
||||||
}
|
}
|
||||||
|
|
||||||
nextPatchIndex++
|
onEvent(
|
||||||
|
ProgressEvent.Completed(
|
||||||
onPatchCompleted()
|
StepId.ExecutePatch(index),
|
||||||
|
|
||||||
selectedPatches.getOrNull(nextPatchIndex)?.let { nextPatch ->
|
|
||||||
updateProgress(
|
|
||||||
name = androidContext.getString(R.string.executing_patch, nextPatch.name)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
|
|
||||||
logger.info("${patch.name} succeeded")
|
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) {
|
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 {
|
addHandler(logger.handler)
|
||||||
handlers.forEach {
|
|
||||||
it.close()
|
|
||||||
removeHandler(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addHandler(logger.handler)
|
with(patcher) {
|
||||||
|
logger.info("Merging integrations")
|
||||||
|
this += selectedPatches.toSet()
|
||||||
|
|
||||||
|
logger.info("Applying patches...")
|
||||||
|
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(patcher) {
|
runStep(StepId.WriteAPK, onEvent) {
|
||||||
logger.info("Merging integrations")
|
logger.info("Writing patched files...")
|
||||||
this += selectedPatches.toSet()
|
val result = patcher.get()
|
||||||
|
|
||||||
logger.info("Applying patches...")
|
val patched = tempDir.resolve("result.apk")
|
||||||
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
|
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() {
|
override fun close() {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package app.revanced.manager.patcher.runtime
|
package app.revanced.manager.patcher.runtime
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.Session
|
import app.revanced.manager.patcher.Session
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -13,7 +14,7 @@ import java.io.File
|
|||||||
/**
|
/**
|
||||||
* Simple [Runtime] implementation that runs the patcher using coroutines.
|
* 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(
|
override suspend fun execute(
|
||||||
inputFile: String,
|
inputFile: String,
|
||||||
outputFile: String,
|
outputFile: String,
|
||||||
@@ -21,47 +22,50 @@ class CoroutineRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
) {
|
) {
|
||||||
val selectedBundles = selectedPatches.keys
|
val patchList = runStep(StepId.LoadPatches, onEvent) {
|
||||||
val bundles = bundles()
|
val selectedBundles = selectedPatches.keys
|
||||||
val uids = bundles.entries.associate { (key, value) -> value to key }
|
val bundles = bundles()
|
||||||
|
val uids = bundles.entries.associate { (key, value) -> value to key }
|
||||||
|
|
||||||
val allPatches =
|
val allPatches =
|
||||||
PatchBundle.Loader.patches(bundles.values, packageName)
|
PatchBundle.Loader.patches(bundles.values, packageName)
|
||||||
.mapKeys { (b, _) -> uids[b]!! }
|
.mapKeys { (b, _) -> uids[b]!! }
|
||||||
.filterKeys { it in selectedBundles }
|
.filterKeys { it in selectedBundles }
|
||||||
|
|
||||||
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
val patchList = selectedPatches.flatMap { (bundle, selected) ->
|
||||||
allPatches[bundle]?.filter { it.name in selected }
|
allPatches[bundle]?.filter { it.name in selected }
|
||||||
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
?: throw IllegalArgumentException("Patch bundle $bundle does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set all patch options.
|
// Set all patch options.
|
||||||
options.forEach { (bundle, bundlePatchOptions) ->
|
options.forEach { (bundle, bundlePatchOptions) ->
|
||||||
val patches = allPatches[bundle] ?: return@forEach
|
val patches = allPatches[bundle] ?: return@forEach
|
||||||
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
bundlePatchOptions.forEach { (patchName, configuredPatchOptions) ->
|
||||||
val patchOptions = patches.single { it.name == patchName }.options
|
val patchOptions = patches.single { it.name == patchName }.options
|
||||||
configuredPatchOptions.forEach { (key, value) ->
|
configuredPatchOptions.forEach { (key, value) ->
|
||||||
patchOptions[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(
|
session.use { s ->
|
||||||
cacheDir,
|
s.run(
|
||||||
frameworkPath,
|
|
||||||
aaptPath,
|
|
||||||
context,
|
|
||||||
logger,
|
|
||||||
File(inputFile),
|
|
||||||
onPatchCompleted = onPatchCompleted,
|
|
||||||
onProgress
|
|
||||||
).use { session ->
|
|
||||||
session.run(
|
|
||||||
File(outputFile),
|
File(outputFile),
|
||||||
patchList
|
patchList
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import app.revanced.manager.BuildConfig
|
|||||||
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
import app.revanced.manager.patcher.runtime.process.IPatcherEvents
|
||||||
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
import app.revanced.manager.patcher.runtime.process.IPatcherProcess
|
||||||
import app.revanced.manager.patcher.LibraryResolver
|
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.logger.Logger
|
||||||
import app.revanced.manager.patcher.runtime.process.Parameters
|
import app.revanced.manager.patcher.runtime.process.Parameters
|
||||||
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
import app.revanced.manager.patcher.runtime.process.PatchConfiguration
|
||||||
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
import app.revanced.manager.patcher.runtime.process.PatcherProcess
|
||||||
import app.revanced.manager.patcher.worker.ProgressEventHandler
|
import app.revanced.manager.patcher.toEvent
|
||||||
import app.revanced.manager.ui.model.State
|
|
||||||
import app.revanced.manager.util.Options
|
import app.revanced.manager.util.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -66,8 +67,7 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
) = coroutineScope {
|
) = coroutineScope {
|
||||||
// Get the location of our own Apk.
|
// Get the location of our own Apk.
|
||||||
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
val managerBaseApk = pm.getPackageInfo(context.packageName)!!.applicationInfo!!.sourceDir
|
||||||
@@ -111,7 +111,6 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val patching = CompletableDeferred<Unit>()
|
val patching = CompletableDeferred<Unit>()
|
||||||
val scope = this
|
|
||||||
|
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
val binder = awaitBinderConnection()
|
val binder = awaitBinderConnection()
|
||||||
@@ -124,13 +123,10 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
|
|||||||
val eventHandler = object : IPatcherEvents.Stub() {
|
val eventHandler = object : IPatcherEvents.Stub() {
|
||||||
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
override fun log(level: String, msg: String) = logger.log(enumValueOf(level), msg)
|
||||||
|
|
||||||
override fun patchSucceeded() {
|
override fun event(event: ProgressEventParcel?) {
|
||||||
scope.launch { onPatchCompleted() }
|
event?.let { onEvent(it.toEvent()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun progress(name: String?, state: String?, msg: String?) =
|
|
||||||
onProgress(name, state?.let { enumValueOf<State>(it) }, msg)
|
|
||||||
|
|
||||||
override fun finished(exceptionStackTrace: String?) {
|
override fun finished(exceptionStackTrace: String?) {
|
||||||
binder.exit()
|
binder.exit()
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import android.content.Context
|
|||||||
import app.revanced.manager.data.platform.Filesystem
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
import app.revanced.manager.domain.manager.PreferencesManager
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
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.aapt.Aapt
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
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.Options
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -34,7 +34,6 @@ sealed class Runtime(context: Context) : KoinComponent {
|
|||||||
selectedPatches: PatchSelection,
|
selectedPatches: PatchSelection,
|
||||||
options: Options,
|
options: Options,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
onPatchCompleted: suspend () -> Unit,
|
onEvent: (ProgressEvent) -> Unit,
|
||||||
onProgress: ProgressEventHandler,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,15 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import app.revanced.manager.BuildConfig
|
import app.revanced.manager.BuildConfig
|
||||||
|
import app.revanced.manager.patcher.ProgressEvent
|
||||||
import app.revanced.manager.patcher.Session
|
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.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.patch.PatchBundle
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
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.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -24,7 +27,7 @@ import kotlin.system.exitProcess
|
|||||||
/**
|
/**
|
||||||
* The main class that runs inside the runner process launched by [ProcessRuntime].
|
* 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 var eventBinder: IPatcherEvents? = null
|
||||||
|
|
||||||
private val scope =
|
private val scope =
|
||||||
@@ -46,6 +49,8 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
override fun exit() = exitProcess(0)
|
override fun exit() = exitProcess(0)
|
||||||
|
|
||||||
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
override fun start(parameters: Parameters, events: IPatcherEvents) {
|
||||||
|
fun onEvent(event: ProgressEvent) = events.event(event.toParcel())
|
||||||
|
|
||||||
eventBinder = events
|
eventBinder = events
|
||||||
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -56,38 +61,42 @@ class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
|
|||||||
|
|
||||||
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
|
||||||
|
|
||||||
val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
|
val patchList = runStep(StepId.LoadPatches, ::onEvent) {
|
||||||
val patchList = parameters.configurations.flatMap { config ->
|
val allPatches = PatchBundle.Loader.patches(
|
||||||
val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
|
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 }
|
.filter { it.name in config.patches }
|
||||||
.associateBy { it.name }
|
.associateBy { it.name }
|
||||||
|
|
||||||
config.options.forEach { (patchName, opts) ->
|
config.options.forEach { (patchName, opts) ->
|
||||||
val patchOptions = patches[patchName]?.options
|
val patchOptions = patches[patchName]?.options
|
||||||
?: throw Exception("Patch with name $patchName does not exist.")
|
?: throw Exception("Patch with name $patchName does not exist.")
|
||||||
|
|
||||||
opts.forEach { (key, value) ->
|
opts.forEach { (key, value) ->
|
||||||
patchOptions[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(
|
session.use {
|
||||||
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 {
|
|
||||||
it.run(File(parameters.outputFile), patchList)
|
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 {
|
appContext.sendBroadcast(Intent().apply {
|
||||||
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
action = ProcessRuntime.CONNECT_TO_APP_ACTION
|
||||||
|
|||||||
@@ -29,14 +29,17 @@ import app.revanced.manager.domain.repository.InstalledAppRepository
|
|||||||
import app.revanced.manager.domain.worker.Worker
|
import app.revanced.manager.domain.worker.Worker
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
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.logger.Logger
|
||||||
|
import app.revanced.manager.patcher.runStep
|
||||||
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
import app.revanced.manager.patcher.runtime.CoroutineRuntime
|
||||||
import app.revanced.manager.patcher.runtime.ProcessRuntime
|
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.GetScope
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
import app.revanced.manager.plugin.downloader.UserInteractionException
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
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.Options
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
@@ -48,8 +51,6 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
typealias ProgressEventHandler = (name: String?, state: State?, message: String?) -> Unit
|
|
||||||
|
|
||||||
@OptIn(PluginHostApi::class)
|
@OptIn(PluginHostApi::class)
|
||||||
class PatcherWorker(
|
class PatcherWorker(
|
||||||
context: Context,
|
context: Context,
|
||||||
@@ -71,11 +72,9 @@ class PatcherWorker(
|
|||||||
val selectedPatches: PatchSelection,
|
val selectedPatches: PatchSelection,
|
||||||
val options: Options,
|
val options: Options,
|
||||||
val logger: Logger,
|
val logger: Logger,
|
||||||
val onDownloadProgress: suspend (Pair<Long, Long?>?) -> Unit,
|
|
||||||
val onPatchCompleted: suspend () -> Unit,
|
|
||||||
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
val handleStartActivityRequest: suspend (LoadedDownloaderPlugin, Intent) -> ActivityResult,
|
||||||
val setInputFile: suspend (File) -> Unit,
|
val setInputFile: suspend (File) -> Unit,
|
||||||
val onProgress: ProgressEventHandler
|
val onEvent: (ProgressEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
val packageName get() = input.packageName
|
val packageName get() = input.packageName
|
||||||
}
|
}
|
||||||
@@ -140,10 +139,6 @@ class PatcherWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runPatcher(args: Args): Result {
|
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")
|
val patchedApk = fs.tempDir.resolve("patched.apk")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
@@ -163,51 +158,65 @@ class PatcherWorker(
|
|||||||
args.input.version,
|
args.input.version,
|
||||||
prefs.suggestedVersionSafeguard.get(),
|
prefs.suggestedVersionSafeguard.get(),
|
||||||
!prefs.disablePatchVersionCompatCheck.get(),
|
!prefs.disablePatchVersionCompatCheck.get(),
|
||||||
onDownload = args.onDownloadProgress
|
onDownload = { progress ->
|
||||||
).also {
|
args.onEvent(
|
||||||
args.setInputFile(it)
|
ProgressEvent.Progress(
|
||||||
updateProgress(state = State.COMPLETED) // Download APK
|
stepId = StepId.DownloadAPK,
|
||||||
}
|
current = progress.first,
|
||||||
|
total = progress.second
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).also { args.setInputFile(it) }
|
||||||
|
|
||||||
val inputFile = when (val selectedApp = args.input) {
|
val inputFile = when (val selectedApp = args.input) {
|
||||||
is SelectedApp.Download -> {
|
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 -> {
|
is SelectedApp.Search -> {
|
||||||
downloaderPluginRepository.loadedPluginsFlow.first()
|
runStep(StepId.DownloadAPK, args.onEvent) {
|
||||||
.firstNotNullOfOrNull { plugin ->
|
downloaderPluginRepository.loadedPluginsFlow.first()
|
||||||
try {
|
.firstNotNullOfOrNull { plugin ->
|
||||||
val getScope = object : GetScope {
|
try {
|
||||||
override val pluginPackageName = plugin.packageName
|
val getScope = object : GetScope {
|
||||||
override val hostPackageName = applicationContext.packageName
|
override val pluginPackageName = plugin.packageName
|
||||||
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
override val hostPackageName =
|
||||||
val result = args.handleStartActivityRequest(plugin, intent)
|
applicationContext.packageName
|
||||||
return when (result.resultCode) {
|
|
||||||
Activity.RESULT_OK -> result.data
|
override suspend fun requestStartActivity(intent: Intent): Intent? {
|
||||||
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
val result =
|
||||||
else -> throw UserInteractionException.Activity.NotCompleted(
|
args.handleStartActivityRequest(plugin, intent)
|
||||||
result.resultCode,
|
return when (result.resultCode) {
|
||||||
result.data
|
Activity.RESULT_OK -> result.data
|
||||||
)
|
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
|
||||||
|
else -> throw UserInteractionException.Activity.NotCompleted(
|
||||||
|
result.resultCode,
|
||||||
|
result.data
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
plugin.get(
|
||||||
plugin.get(
|
getScope,
|
||||||
getScope,
|
selectedApp.packageName,
|
||||||
selectedApp.packageName,
|
selectedApp.version
|
||||||
selectedApp.version
|
)
|
||||||
)
|
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
||||||
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
|
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
||||||
} catch (e: UserInteractionException.Activity.NotCompleted) {
|
throw e
|
||||||
throw e
|
} catch (_: UserInteractionException) {
|
||||||
} catch (_: UserInteractionException) {
|
null
|
||||||
null
|
}?.let { (data, _) -> download(plugin, data) }
|
||||||
}?.let { (data, _) -> download(plugin, data) }
|
} ?: throw Exception("App is not available.")
|
||||||
} ?: throw Exception("App is not available.")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
|
||||||
@@ -227,12 +236,12 @@ class PatcherWorker(
|
|||||||
args.selectedPatches,
|
args.selectedPatches,
|
||||||
args.options,
|
args.options,
|
||||||
args.logger,
|
args.logger,
|
||||||
args.onPatchCompleted,
|
args.onEvent,
|
||||||
args.onProgress
|
|
||||||
)
|
)
|
||||||
|
|
||||||
keystoreManager.sign(patchedApk, File(args.output))
|
runStep(StepId.SignAPK, args.onEvent) {
|
||||||
updateProgress(state = State.COMPLETED) // Signing
|
keystoreManager.sign(patchedApk, File(args.output))
|
||||||
|
}
|
||||||
|
|
||||||
Log.i(tag, "Patching succeeded".logFmt())
|
Log.i(tag, "Patching succeeded".logFmt())
|
||||||
Result.success()
|
Result.success()
|
||||||
@@ -241,11 +250,11 @@ class PatcherWorker(
|
|||||||
tag,
|
tag,
|
||||||
"An exception occurred in the remote process while patching. ${e.originalStackTrace}".logFmt()
|
"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()
|
Result.failure()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "An exception occurred while patching".logFmt(), e)
|
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()
|
Result.failure()
|
||||||
} finally {
|
} finally {
|
||||||
patchedApk.delete()
|
patchedApk.delete()
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.manager.ui.component
|
package app.revanced.manager.ui.component
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -79,7 +80,7 @@ private fun installerStatusDialogButton(
|
|||||||
enum class DialogKind(
|
enum class DialogKind(
|
||||||
val flag: Int,
|
val flag: Int,
|
||||||
val title: Int,
|
val title: Int,
|
||||||
@StringRes val contentStringResId: Int,
|
@param:StringRes val contentStringResId: Int,
|
||||||
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
val icon: ImageVector = Icons.Outlined.ErrorOutline,
|
||||||
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
val confirmButton: InstallerStatusDialogButton = installerStatusDialogButton(R.string.ok),
|
||||||
val dismissButton: InstallerStatusDialogButton? = null,
|
val dismissButton: InstallerStatusDialogButton? = null,
|
||||||
@@ -133,10 +134,8 @@ enum class DialogKind(
|
|||||||
title = R.string.installation_storage_issue_dialog_title,
|
title = R.string.installation_storage_issue_dialog_title,
|
||||||
contentStringResId = R.string.installation_storage_issue_description,
|
contentStringResId = R.string.installation_storage_issue_description,
|
||||||
),
|
),
|
||||||
|
|
||||||
@RequiresApi(34)
|
|
||||||
FAILURE_TIMEOUT(
|
FAILURE_TIMEOUT(
|
||||||
flag = PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
flag = @SuppressLint("InlinedApi") PackageInstaller.STATUS_FAILURE_TIMEOUT,
|
||||||
title = R.string.installation_timeout_dialog_title,
|
title = R.string.installation_timeout_dialog_title,
|
||||||
contentStringResId = R.string.installation_timeout_description,
|
contentStringResId = R.string.installation_timeout_description,
|
||||||
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
confirmButton = installerStatusDialogButton(R.string.install_app) { model ->
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package app.revanced.manager.ui.component.patcher
|
package app.revanced.manager.ui.component.patcher
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Cancel
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
@@ -21,6 +20,7 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -39,11 +39,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
import app.revanced.manager.ui.component.ArrowButton
|
import app.revanced.manager.ui.component.ArrowButton
|
||||||
import app.revanced.manager.ui.component.LoadingIndicator
|
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.State
|
||||||
import app.revanced.manager.ui.model.Step
|
|
||||||
import app.revanced.manager.ui.model.StepCategory
|
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 java.util.Locale
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
@@ -52,21 +50,10 @@ import kotlin.math.floor
|
|||||||
fun Steps(
|
fun Steps(
|
||||||
category: StepCategory,
|
category: StepCategory,
|
||||||
steps: List<Step>,
|
steps: List<Step>,
|
||||||
stepCount: Pair<Int, Int>? = null,
|
isExpanded: Boolean = false,
|
||||||
stepProgressProvider: StepProgressProvider
|
onExpand: () -> Unit,
|
||||||
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
var expanded by rememberSaveable { mutableStateOf(true) }
|
|
||||||
|
|
||||||
val categoryColor by animateColorAsState(
|
|
||||||
if (expanded) MaterialTheme.colorScheme.surfaceContainerHigh else Color.Transparent,
|
|
||||||
label = "category"
|
|
||||||
)
|
|
||||||
|
|
||||||
val cardColor by animateColorAsState(
|
|
||||||
if (expanded) MaterialTheme.colorScheme.surfaceContainer else Color.Transparent,
|
|
||||||
label = "card"
|
|
||||||
)
|
|
||||||
|
|
||||||
val state = remember(steps) {
|
val state = remember(steps) {
|
||||||
when {
|
when {
|
||||||
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
steps.all { it.state == State.COMPLETED } -> State.COMPLETED
|
||||||
@@ -76,62 +63,69 @@ 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 || state == State.FAILED)
|
||||||
|
onExpand()
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(MaterialTheme.shapes.large)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(cardColor)
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clickable(true, onClick = onClick)
|
||||||
.clickable { expanded = !expanded }
|
.fillMaxWidth()
|
||||||
.background(categoryColor)
|
.padding(20.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
StepIcon(state = state, size = 24.dp)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) {
|
|
||||||
StepIcon(state = state, size = 24.dp)
|
|
||||||
|
|
||||||
Text(stringResource(category.displayName))
|
Text(stringResource(category.displayName))
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
val stepProgress = remember(stepCount, steps) {
|
Text(
|
||||||
stepCount?.let { (current, total) -> "$current/$total" }
|
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}",
|
||||||
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
|
style = MaterialTheme.typography.labelSmall
|
||||||
}
|
)
|
||||||
|
|
||||||
Text(
|
ArrowButton(modifier = Modifier.size(24.dp), expanded = isExpanded, onClick = null)
|
||||||
text = stepProgress,
|
|
||||||
style = MaterialTheme.typography.labelSmall
|
|
||||||
)
|
|
||||||
|
|
||||||
ArrowButton(modifier = Modifier.size(24.dp), expanded = expanded, onClick = null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(visible = expanded) {
|
AnimatedVisibility(visible = isExpanded) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.background.copy(0.6f))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 10.dp)
|
||||||
) {
|
) {
|
||||||
steps.forEach { step ->
|
filteredSteps.forEachIndexed { index, step ->
|
||||||
val (progress, progressText) = when (step.progressKey) {
|
val (progress, progressText) = step.progress?.let { (current, total) ->
|
||||||
null -> null
|
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB"
|
||||||
ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
|
else null to "${current.megaBytes} MB"
|
||||||
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
|
|
||||||
else null to "${downloaded.megaBytes} MB"
|
|
||||||
}
|
|
||||||
} ?: (null to null)
|
} ?: (null to null)
|
||||||
|
|
||||||
SubStep(
|
SubStep(
|
||||||
name = step.name,
|
name = step.title,
|
||||||
state = step.state,
|
state = step.state,
|
||||||
message = step.message,
|
message = step.message,
|
||||||
progress = progress,
|
progress = progress,
|
||||||
progressText = progressText
|
progressText = progressText,
|
||||||
|
isFirst = index == 0,
|
||||||
|
isLast = index == filteredSteps.lastIndex,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +139,9 @@ fun SubStep(
|
|||||||
state: State,
|
state: State,
|
||||||
message: String? = null,
|
message: String? = null,
|
||||||
progress: Float? = null,
|
progress: Float? = null,
|
||||||
progressText: String? = null
|
progressText: String? = null,
|
||||||
|
isFirst: Boolean = false,
|
||||||
|
isLast: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
var messageExpanded by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
@@ -156,22 +152,22 @@ fun SubStep(
|
|||||||
clickable { messageExpanded = !messageExpanded }
|
clickable { messageExpanded = !messageExpanded }
|
||||||
else this
|
else this
|
||||||
}
|
}
|
||||||
|
.padding(top = if (isFirst) 10.dp else 8.dp, bottom = if (isLast) 20.dp else 8.dp)
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
StepIcon(
|
||||||
modifier = Modifier.size(24.dp),
|
size = 18.dp,
|
||||||
contentAlignment = Alignment.Center
|
state = state,
|
||||||
) {
|
progress = progress,
|
||||||
StepIcon(state, progress, size = 20.dp)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = name,
|
text = name,
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f, true),
|
modifier = Modifier.weight(1f, true),
|
||||||
@@ -201,7 +197,7 @@ fun SubStep(
|
|||||||
text = message.orEmpty(),
|
text = message.orEmpty(),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.secondary,
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
modifier = Modifier.padding(horizontal = 52.dp, vertical = 8.dp)
|
modifier = Modifier.padding(horizontal = 36.dp, vertical = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,40 +207,44 @@ fun SubStep(
|
|||||||
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
fun StepIcon(state: State, progress: Float? = null, size: Dp) {
|
||||||
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
val strokeWidth = Dp(floor(size.value / 10) + 1)
|
||||||
|
|
||||||
when (state) {
|
Crossfade(targetState = state, label = "State CrossFade") { state ->
|
||||||
State.COMPLETED -> Icon(
|
when (state) {
|
||||||
Icons.Filled.CheckCircle,
|
State.COMPLETED -> Icon(
|
||||||
contentDescription = stringResource(R.string.step_completed),
|
Icons.Filled.CheckCircle,
|
||||||
tint = MaterialTheme.colorScheme.surfaceTint,
|
contentDescription = stringResource(R.string.step_completed),
|
||||||
modifier = Modifier.size(size)
|
tint = Color(0xFF59B463),
|
||||||
)
|
modifier = Modifier.size(size)
|
||||||
|
|
||||||
State.FAILED -> Icon(
|
|
||||||
Icons.Filled.Cancel,
|
|
||||||
contentDescription = stringResource(R.string.step_failed),
|
|
||||||
tint = MaterialTheme.colorScheme.error,
|
|
||||||
modifier = Modifier.size(size)
|
|
||||||
)
|
|
||||||
|
|
||||||
State.WAITING -> Icon(
|
|
||||||
Icons.Outlined.Circle,
|
|
||||||
contentDescription = stringResource(R.string.step_waiting),
|
|
||||||
tint = MaterialTheme.colorScheme.surfaceVariant,
|
|
||||||
modifier = Modifier.size(size)
|
|
||||||
)
|
|
||||||
|
|
||||||
State.RUNNING ->
|
|
||||||
LoadingIndicator(
|
|
||||||
modifier = stringResource(R.string.step_running).let { description ->
|
|
||||||
Modifier
|
|
||||||
.size(size)
|
|
||||||
.semantics {
|
|
||||||
contentDescription = description
|
|
||||||
}
|
|
||||||
},
|
|
||||||
progress = { progress },
|
|
||||||
strokeWidth = strokeWidth
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
State.FAILED -> Icon(
|
||||||
|
Icons.Filled.Cancel,
|
||||||
|
contentDescription = stringResource(R.string.step_failed),
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(size)
|
||||||
|
)
|
||||||
|
|
||||||
|
State.WAITING -> Icon(
|
||||||
|
Icons.Outlined.Circle,
|
||||||
|
contentDescription = stringResource(R.string.step_waiting),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface.copy(.2f),
|
||||||
|
modifier = Modifier.size(size)
|
||||||
|
)
|
||||||
|
|
||||||
|
State.RUNNING -> {
|
||||||
|
LoadingIndicator(
|
||||||
|
modifier = stringResource(R.string.step_running).let { description ->
|
||||||
|
Modifier
|
||||||
|
.size(size)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = description
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
progress = { progress },
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.revanced.manager.ui.model
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.patcher.StepId
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
enum class StepCategory(@StringRes val displayName: Int) {
|
enum class StepCategory(@StringRes val displayName: Int) {
|
||||||
@@ -15,19 +16,20 @@ enum class State {
|
|||||||
WAITING, RUNNING, FAILED, COMPLETED
|
WAITING, RUNNING, FAILED, COMPLETED
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ProgressKey {
|
|
||||||
DOWNLOAD
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StepProgressProvider {
|
|
||||||
val downloadProgress: Pair<Long, Long?>?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Step(
|
data class Step(
|
||||||
val name: String,
|
val id: StepId,
|
||||||
|
val title: String,
|
||||||
val category: StepCategory,
|
val category: StepCategory,
|
||||||
val state: State = State.WAITING,
|
val state: State = State.WAITING,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
val progressKey: ProgressKey? = null
|
val progress: Pair<Long, Long?>? = null,
|
||||||
) : Parcelable
|
val hide: Boolean = false,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
|
||||||
|
fun Step.withState(
|
||||||
|
state: State = this.state,
|
||||||
|
message: String? = this.message,
|
||||||
|
progress: Pair<Long, Long?>? = this.progress
|
||||||
|
) = copy(state = state, message = message, progress = progress)
|
||||||
@@ -87,7 +87,7 @@ fun PatcherScreen(
|
|||||||
|
|
||||||
val steps by remember {
|
val steps by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
viewModel.steps.groupBy { it.category }
|
viewModel.steps.groupBy { it.category }.toList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +213,12 @@ fun PatcherScreen(
|
|||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
var expandedCategory by rememberSaveable { mutableStateOf<StepCategory?>(null) }
|
||||||
|
|
||||||
|
val expandCategory: (StepCategory?) -> Unit = { category ->
|
||||||
|
expandedCategory = category
|
||||||
|
}
|
||||||
|
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
progress = { viewModel.progress },
|
progress = { viewModel.progress },
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
@@ -224,14 +230,17 @@ fun PatcherScreen(
|
|||||||
contentPadding = PaddingValues(16.dp)
|
contentPadding = PaddingValues(16.dp)
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
items = steps.toList(),
|
items = steps,
|
||||||
key = { it.first }
|
key = { it.first }
|
||||||
) { (category, steps) ->
|
) { (category, steps) ->
|
||||||
Steps(
|
Steps(
|
||||||
category = category,
|
category = category,
|
||||||
steps = steps,
|
steps = steps,
|
||||||
stepCount = if (category == StepCategory.PATCHING) viewModel.patchesProgress else null,
|
isExpanded = expandedCategory == category,
|
||||||
stepProgressProvider = viewModel
|
onExpand = { expandCategory(category) },
|
||||||
|
onClick = {
|
||||||
|
expandCategory(if (expandedCategory == category) null else category)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -76,12 +77,12 @@ fun SelectedAppInfoScreen(
|
|||||||
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
val bundles by vm.bundleInfoFlow.collectAsStateWithLifecycle(emptyList())
|
||||||
|
|
||||||
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
val allowIncompatiblePatches by vm.prefs.disablePatchVersionCompatCheck.getAsState()
|
||||||
val patches = remember(bundles, allowIncompatiblePatches) {
|
val patches by remember {
|
||||||
vm.getPatches(bundles, allowIncompatiblePatches)
|
derivedStateOf {
|
||||||
}
|
vm.getPatches(bundles, allowIncompatiblePatches)
|
||||||
val selectedPatchCount = remember(patches) {
|
}
|
||||||
patches.values.sumOf { it.size }
|
|
||||||
}
|
}
|
||||||
|
val selectedPatchCount = patches.values.sumOf { it.size }
|
||||||
|
|
||||||
val launcher = rememberLauncherForActivityResult(
|
val launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
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.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
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.data.room.apps.installed.InstalledApp
|
||||||
import app.revanced.manager.domain.installer.RootInstaller
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.service.UninstallService
|
|
||||||
import app.revanced.manager.util.PM
|
import app.revanced.manager.util.PM
|
||||||
import app.revanced.manager.util.PatchSelection
|
import app.revanced.manager.util.PatchSelection
|
||||||
import app.revanced.manager.util.simpleMessage
|
import app.revanced.manager.util.simpleMessage
|
||||||
@@ -30,6 +23,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import ru.solrudev.ackpine.session.Session
|
||||||
|
import ru.solrudev.ackpine.uninstaller.UninstallFailure
|
||||||
|
|
||||||
class InstalledAppInfoViewModel(
|
class InstalledAppInfoViewModel(
|
||||||
packageName: String
|
packageName: String
|
||||||
@@ -87,51 +82,28 @@ class InstalledAppInfoViewModel(
|
|||||||
|
|
||||||
fun uninstall() {
|
fun uninstall() {
|
||||||
val app = installedApp ?: return
|
val app = installedApp ?: return
|
||||||
when (app.installType) {
|
viewModelScope.launch {
|
||||||
InstallType.DEFAULT -> pm.uninstallPackage(app.currentPackageName)
|
when (app.installType) {
|
||||||
|
InstallType.DEFAULT -> {
|
||||||
InstallType.MOUNT -> viewModelScope.launch {
|
when (val result = pm.uninstallPackage(app.currentPackageName)) {
|
||||||
rootInstaller.uninstall(app.currentPackageName)
|
is Session.State.Failed<UninstallFailure> -> {
|
||||||
installedAppRepository.delete(app)
|
val msg = result.failure.message.orEmpty()
|
||||||
onBackClick()
|
context.toast(
|
||||||
}
|
this@InstalledAppInfoViewModel.context.getString(
|
||||||
}
|
R.string.uninstall_app_fail,
|
||||||
}
|
msg
|
||||||
|
)
|
||||||
private val uninstallBroadcastReceiver = object : BroadcastReceiver() {
|
)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
return@launch
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
} else if (extraStatus != PackageInstaller.STATUS_FAILURE_ABORTED) {
|
Session.State.Succeeded -> {}
|
||||||
this@InstalledAppInfoViewModel.context.toast(this@InstalledAppInfoViewModel.context.getString(R.string.uninstall_app_fail, extraStatusMessage))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
ContextCompat.registerReceiver(
|
|
||||||
context,
|
|
||||||
it,
|
|
||||||
IntentFilter(UninstallService.APP_UNINSTALL_ACTION),
|
|
||||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
InstallType.MOUNT -> rootInstaller.uninstall(app.currentPackageName)
|
||||||
super.onCleared()
|
}
|
||||||
context.unregisterReceiver(uninstallBroadcastReceiver)
|
installedAppRepository.delete(app)
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.pm.PackageInstaller as AndroidPackageInstaller
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -16,7 +14,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.saveable.autoSaver
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.toMutableStateList
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.map
|
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.installer.RootInstaller
|
||||||
import app.revanced.manager.domain.repository.InstalledAppRepository
|
import app.revanced.manager.domain.repository.InstalledAppRepository
|
||||||
import app.revanced.manager.domain.worker.WorkerRepository
|
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.LogLevel
|
||||||
import app.revanced.manager.patcher.logger.Logger
|
import app.revanced.manager.patcher.logger.Logger
|
||||||
import app.revanced.manager.patcher.worker.PatcherWorker
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
import app.revanced.manager.plugin.downloader.PluginHostApi
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
import app.revanced.manager.plugin.downloader.UserInteractionException
|
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.InstallerModel
|
||||||
import app.revanced.manager.ui.model.ProgressKey
|
|
||||||
import app.revanced.manager.ui.model.SelectedApp
|
import app.revanced.manager.ui.model.SelectedApp
|
||||||
import app.revanced.manager.ui.model.State
|
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.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.navigation.Patcher
|
||||||
|
import app.revanced.manager.ui.model.withState
|
||||||
import app.revanced.manager.util.PM
|
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.saveableVar
|
||||||
import app.revanced.manager.util.saver.snapshotStateListSaver
|
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.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -66,6 +66,15 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
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.io.File
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -73,7 +82,7 @@ import java.time.Duration
|
|||||||
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
@OptIn(SavedStateHandleSaveableApi::class, PluginHostApi::class)
|
||||||
class PatcherViewModel(
|
class PatcherViewModel(
|
||||||
private val input: Patcher.ViewModelParams
|
private val input: Patcher.ViewModelParams
|
||||||
) : ViewModel(), KoinComponent, StepProgressProvider, InstallerModel {
|
) : ViewModel(), KoinComponent, InstallerModel {
|
||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val fs: Filesystem by inject()
|
private val fs: Filesystem by inject()
|
||||||
private val pm: PM by inject()
|
private val pm: PM by inject()
|
||||||
@@ -81,6 +90,7 @@ class PatcherViewModel(
|
|||||||
private val installedAppRepository: InstalledAppRepository by inject()
|
private val installedAppRepository: InstalledAppRepository by inject()
|
||||||
private val rootInstaller: RootInstaller by inject()
|
private val rootInstaller: RootInstaller by inject()
|
||||||
private val savedStateHandle: SavedStateHandle = get()
|
private val savedStateHandle: SavedStateHandle = get()
|
||||||
|
private val ackpineInstaller: PackageInstaller = get()
|
||||||
|
|
||||||
private var installedApp: InstalledApp? = null
|
private var installedApp: InstalledApp? = null
|
||||||
private val selectedApp = input.selectedApp
|
private val selectedApp = input.selectedApp
|
||||||
@@ -95,7 +105,6 @@ class PatcherViewModel(
|
|||||||
mutableStateOf<String?>(null)
|
mutableStateOf<String?>(null)
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
private var ongoingPmSession: Boolean by savedStateHandle.saveableVar { false }
|
|
||||||
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
var packageInstallerStatus: Int? by savedStateHandle.saveable(
|
||||||
key = "packageInstallerStatus",
|
key = "packageInstallerStatus",
|
||||||
stateSaver = autoSaver()
|
stateSaver = autoSaver()
|
||||||
@@ -104,7 +113,7 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isInstalling by mutableStateOf(ongoingPmSession)
|
var isInstalling by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, String>? by mutableStateOf(
|
private var currentActivityRequest: Pair<CompletableDeferred<Boolean>, 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 var inputFile: File? by savedStateHandle.saveableVar()
|
||||||
private val outputFile = tempDir.resolve("output.apk")
|
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<Pair<Long, Long?>?>(null)
|
|
||||||
}
|
|
||||||
private set
|
|
||||||
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) {
|
||||||
generateSteps(
|
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList()
|
||||||
app,
|
|
||||||
input.selectedApp
|
|
||||||
).toMutableStateList()
|
|
||||||
}
|
}
|
||||||
private var currentStepIndex = 0
|
|
||||||
|
|
||||||
val progress by derivedStateOf {
|
val progress by derivedStateOf {
|
||||||
val current = steps.count {
|
val steps = steps.filter { it.id != StepId.ExecutePatches }
|
||||||
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
|
|
||||||
} + completedPatchCount
|
|
||||||
|
|
||||||
val total = steps.size - 1 + patchCount
|
val current = steps.count { it.state == State.COMPLETED }
|
||||||
|
val total = steps.size
|
||||||
|
|
||||||
current.toFloat() / total.toFloat()
|
current.toFloat() / total.toFloat()
|
||||||
}
|
}
|
||||||
@@ -174,67 +175,46 @@ class PatcherViewModel(
|
|||||||
private val workManager = WorkManager.getInstance(app)
|
private val workManager = WorkManager.getInstance(app)
|
||||||
|
|
||||||
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
private val patcherWorkerId by savedStateHandle.saveable<ParcelUuid> {
|
||||||
ParcelUuid(workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
ParcelUuid(
|
||||||
"patching", PatcherWorker.Args(
|
workerRepository.launchExpedited<PatcherWorker, PatcherWorker.Args>(
|
||||||
input.selectedApp,
|
"patching", PatcherWorker.Args(
|
||||||
outputFile.path,
|
input.selectedApp,
|
||||||
input.selectedPatches,
|
outputFile.path,
|
||||||
input.options,
|
input.selectedPatches,
|
||||||
logger,
|
input.options,
|
||||||
onDownloadProgress = {
|
logger,
|
||||||
withContext(Dispatchers.Main) {
|
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
|
||||||
downloadProgress = it
|
handleStartActivityRequest = { plugin, intent ->
|
||||||
}
|
withContext(Dispatchers.Main) {
|
||||||
},
|
if (currentActivityRequest != null) throw Exception("Another request is already pending.")
|
||||||
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<Boolean>()) {
|
|
||||||
currentActivityRequest = this to plugin.name
|
|
||||||
|
|
||||||
await()
|
|
||||||
}
|
|
||||||
if (!accepted) throw UserInteractionException.RequestDenied()
|
|
||||||
|
|
||||||
// Launch the activity and wait for the result.
|
|
||||||
try {
|
try {
|
||||||
with(CompletableDeferred<ActivityResult>()) {
|
// Wait for the dialog interaction.
|
||||||
launchedActivity = this
|
val accepted = with(CompletableDeferred<Boolean>()) {
|
||||||
launchActivityChannel.send(intent)
|
currentActivityRequest = this to plugin.name
|
||||||
|
|
||||||
await()
|
await()
|
||||||
}
|
}
|
||||||
|
if (!accepted) throw UserInteractionException.RequestDenied()
|
||||||
|
|
||||||
|
// Launch the activity and wait for the result.
|
||||||
|
try {
|
||||||
|
with(CompletableDeferred<ActivityResult>()) {
|
||||||
|
launchedActivity = this
|
||||||
|
launchActivityChannel.send(intent)
|
||||||
|
await()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
launchedActivity = null
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
launchedActivity = null
|
currentActivityRequest = null
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
currentActivityRequest = null
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
onEvent = ::handleProgressEvent,
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val patcherSucceeded =
|
val patcherSucceeded =
|
||||||
@@ -246,64 +226,26 @@ class PatcherViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val installerBroadcastReceiver = object : BroadcastReceiver() {
|
init {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
// TODO: detect system-initiated process death during the patching process.
|
||||||
when (intent?.action) {
|
|
||||||
InstallService.APP_INSTALL_ACTION -> {
|
|
||||||
val pmStatus = intent.getIntExtra(
|
|
||||||
InstallService.EXTRA_INSTALL_STATUS,
|
|
||||||
PackageInstaller.STATUS_FAILURE
|
|
||||||
)
|
|
||||||
|
|
||||||
intent.getStringExtra(UninstallService.EXTRA_UNINSTALL_STATUS_MESSAGE)
|
installerSessionId?.uuid?.let { id ->
|
||||||
?.let(logger::trace)
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
|
isInstalling = true
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
uiSafe(app, R.string.install_app_fail, "Failed to install") {
|
||||||
installedPackageName =
|
// The process was killed during installation. Await the session again.
|
||||||
intent.getStringExtra(InstallService.EXTRA_PACKAGE_NAME)
|
withContext(Dispatchers.IO) {
|
||||||
viewModelScope.launch {
|
ackpineInstaller.getSession(id)
|
||||||
installedAppRepository.addOrUpdate(
|
}?.let {
|
||||||
installedPackageName!!,
|
awaitInstallation(it)
|
||||||
packageName,
|
|
||||||
input.selectedApp.version
|
|
||||||
?: pm.getPackageInfo(outputFile)?.versionName!!,
|
|
||||||
InstallType.DEFAULT,
|
|
||||||
input.selectedPatches
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else packageInstallerStatus = pmStatus
|
}
|
||||||
|
} finally {
|
||||||
isInstalling = false
|
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 {
|
viewModelScope.launch {
|
||||||
installedApp = installedAppRepository.get(packageName)
|
installedApp = installedAppRepository.get(packageName)
|
||||||
@@ -313,7 +255,6 @@ class PatcherViewModel(
|
|||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installerBroadcastReceiver)
|
|
||||||
workManager.cancelWorkById(patcherWorkerId.uuid)
|
workManager.cancelWorkById(patcherWorkerId.uuid)
|
||||||
|
|
||||||
if (input.selectedApp is SelectedApp.Installed && installedApp?.installType == InstallType.MOUNT) {
|
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() {
|
fun onBack() {
|
||||||
|
installerCoroutineScope.cancel()
|
||||||
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
// tempDir cannot be deleted inside onCleared because it gets called on system-initiated process death.
|
||||||
tempDir.deleteRecursively()
|
tempDir.deleteRecursively()
|
||||||
}
|
}
|
||||||
@@ -372,44 +343,93 @@ class PatcherViewModel(
|
|||||||
|
|
||||||
fun open() = installedPackageName?.let(pm::launch)
|
fun open() = installedPackageName?.let(pm::launch)
|
||||||
|
|
||||||
fun install(installType: InstallType) = viewModelScope.launch {
|
private suspend fun startInstallation(file: File, packageName: String) {
|
||||||
var pmInstallStarted = false
|
val session = withContext(Dispatchers.IO) {
|
||||||
try {
|
ackpineInstaller.createSession(Uri.fromFile(file)) {
|
||||||
isInstalling = true
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
installerPkgName = packageName
|
||||||
|
}
|
||||||
|
awaitInstallation(session)
|
||||||
|
}
|
||||||
|
|
||||||
val currentPackageInfo = pm.getPackageInfo(outputFile)
|
private suspend fun awaitInstallation(session: ProgressSession<InstallFailure>) = withContext(
|
||||||
?: throw Exception("Failed to load application info")
|
Dispatchers.Main
|
||||||
|
) {
|
||||||
// If the app is currently installed
|
val result = installerCoroutineScope.async {
|
||||||
val existingPackageInfo = pm.getPackageInfo(currentPackageInfo.packageName)
|
try {
|
||||||
if (existingPackageInfo != null) {
|
installerSessionId = ParcelUuid(session.id)
|
||||||
// Check if the app version is less than the installed version
|
withContext(Dispatchers.IO) {
|
||||||
if (pm.getVersionCode(currentPackageInfo) < pm.getVersionCode(existingPackageInfo)) {
|
session.await()
|
||||||
// Exit if the selected app version is less than the installed version
|
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_CONFLICT
|
|
||||||
return@launch
|
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
installerSessionId = null
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
when (result) {
|
||||||
|
is Session.State.Failed<InstallFailure> -> {
|
||||||
|
result.failure.message?.let(logger::trace)
|
||||||
|
packageInstallerStatus = result.failure.asCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
when (installType) {
|
Session.State.Succeeded -> {
|
||||||
InstallType.DEFAULT -> {
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
// Check if the app is mounted as root
|
installedPackageName = installerPkgName
|
||||||
// If it is, unmount it first, silently
|
installedAppRepository.addOrUpdate(
|
||||||
if (rootInstaller.hasRootAccess() && rootInstaller.isAppMounted(packageName)) {
|
installerPkgName,
|
||||||
rootInstaller.unmount(packageName)
|
packageName,
|
||||||
}
|
input.selectedApp.version
|
||||||
|
?: withContext(Dispatchers.IO) { pm.getPackageInfo(outputFile)?.versionName!! },
|
||||||
|
InstallType.DEFAULT,
|
||||||
|
input.selectedPatches
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Install regularly
|
fun install(installType: InstallType) = viewModelScope.launch {
|
||||||
pm.installApp(listOf(outputFile))
|
isInstalling = true
|
||||||
pmInstallStarted = 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 -> {
|
when (installType) {
|
||||||
try {
|
InstallType.DEFAULT -> {
|
||||||
val packageInfo = pm.getPackageInfo(outputFile)
|
// Check if the app is mounted as root
|
||||||
?: throw Exception("Failed to load application info")
|
// 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) {
|
val label = with(pm) {
|
||||||
packageInfo.label()
|
currentPackageInfo.label()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for base APK, first check if the app is already installed
|
// 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 the app is not installed, check if the output file is a base apk
|
||||||
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
if (currentPackageInfo.splitNames.isNotEmpty()) {
|
||||||
// Exit if there is no base APK package
|
// Exit if there is no base APK package
|
||||||
packageInstallerStatus = PackageInstaller.STATUS_FAILURE_INVALID
|
packageInstallerStatus =
|
||||||
|
AndroidPackageInstaller.STATUS_FAILURE_INVALID
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val inputVersion = input.selectedApp.version
|
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")
|
?: throw Exception("Failed to determine input APK version")
|
||||||
|
|
||||||
|
needsRootUninstall = true
|
||||||
// Install as root
|
// Install as root
|
||||||
rootInstaller.install(
|
rootInstaller.install(
|
||||||
outputFile,
|
outputFile,
|
||||||
@@ -436,7 +458,7 @@ class PatcherViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
installedAppRepository.addOrUpdate(
|
installedAppRepository.addOrUpdate(
|
||||||
packageInfo.packageName,
|
currentPackageInfo.packageName,
|
||||||
packageName,
|
packageName,
|
||||||
inputVersion,
|
inputVersion,
|
||||||
InstallType.MOUNT,
|
InstallType.MOUNT,
|
||||||
@@ -448,21 +470,20 @@ class PatcherViewModel(
|
|||||||
installedPackageName = packageName
|
installedPackageName = packageName
|
||||||
|
|
||||||
app.toast(app.getString(R.string.install_app_success))
|
app.toast(app.getString(R.string.install_app_success))
|
||||||
} catch (e: Exception) {
|
needsRootUninstall = false
|
||||||
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) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(tag, "Failed to install", e)
|
|
||||||
app.toast(app.getString(R.string.install_app_fail, e.simpleMessage()))
|
|
||||||
} finally {
|
} 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() {
|
override fun reinstall() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
uiSafe(app, R.string.reinstall_app_fail, "Failed to reinstall") {
|
try {
|
||||||
pm.getPackageInfo(outputFile)?.packageName?.let { pm.uninstallPackage(it) }
|
|
||||||
?: throw Exception("Failed to load application info")
|
|
||||||
|
|
||||||
pm.installApp(listOf(outputFile))
|
|
||||||
isInstalling = true
|
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<UninstallFailure> -> {
|
||||||
|
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)
|
LogLevel.ERROR -> Log.e(TAG, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
|
fun generateSteps(
|
||||||
val needsDownload =
|
context: Context,
|
||||||
selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
|
selectedApp: SelectedApp,
|
||||||
|
selectedPatches: PatchSelection
|
||||||
|
): List<Step> = buildList {
|
||||||
|
if (selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search)
|
||||||
|
add(
|
||||||
|
Step(
|
||||||
|
StepId.DownloadAPK,
|
||||||
|
context.getString(R.string.download_apk),
|
||||||
|
StepCategory.PREPARING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return listOfNotNull(
|
add(
|
||||||
Step(
|
|
||||||
context.getString(R.string.download_apk),
|
|
||||||
StepCategory.PREPARING,
|
|
||||||
state = State.RUNNING,
|
|
||||||
progressKey = ProgressKey.DOWNLOAD,
|
|
||||||
).takeIf { needsDownload },
|
|
||||||
Step(
|
Step(
|
||||||
|
StepId.LoadPatches,
|
||||||
context.getString(R.string.patcher_step_load_patches),
|
context.getString(R.string.patcher_step_load_patches),
|
||||||
StepCategory.PREPARING,
|
StepCategory.PREPARING
|
||||||
state = if (needsDownload) State.WAITING else State.RUNNING,
|
)
|
||||||
),
|
)
|
||||||
|
add(
|
||||||
Step(
|
Step(
|
||||||
|
StepId.ReadAPK,
|
||||||
context.getString(R.string.patcher_step_unpack),
|
context.getString(R.string.patcher_step_unpack),
|
||||||
StepCategory.PREPARING
|
StepCategory.PREPARING
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
Step(
|
Step(
|
||||||
|
StepId.ExecutePatches,
|
||||||
context.getString(R.string.execute_patches),
|
context.getString(R.string.execute_patches),
|
||||||
StepCategory.PATCHING
|
StepCategory.PATCHING,
|
||||||
),
|
hide = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
|
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name ->
|
||||||
Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
|
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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.os.Parcelable
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -129,8 +128,6 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var options: Options by savedStateHandle.saveable {
|
var options: Options by savedStateHandle.saveable {
|
||||||
val state = mutableStateOf<Options>(emptyMap())
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
if (!persistConfiguration) return@launch // TODO: save options for patched apps.
|
||||||
val bundlePatches = bundleInfoFlow.first()
|
val bundlePatches = bundleInfoFlow.first()
|
||||||
@@ -141,7 +138,7 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
mutableStateOf(emptyMap())
|
||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -149,8 +146,6 @@ class SelectedAppInfoViewModel(
|
|||||||
if (input.patches != null)
|
if (input.patches != null)
|
||||||
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
return@saveable mutableStateOf(SelectionState.Customized(input.patches))
|
||||||
|
|
||||||
val selection: MutableState<SelectionState> = mutableStateOf(SelectionState.Default)
|
|
||||||
|
|
||||||
// Try to get the previous selection if customization is enabled.
|
// Try to get the previous selection if customization is enabled.
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!prefs.disableSelectionWarning.get()) return@launch
|
if (!prefs.disableSelectionWarning.get()) return@launch
|
||||||
@@ -160,7 +155,7 @@ class SelectedAppInfoViewModel(
|
|||||||
selectionState = SelectionState.Customized(previous)
|
selectionState = SelectionState.Customized(previous)
|
||||||
}
|
}
|
||||||
|
|
||||||
selection
|
mutableStateOf(SelectionState.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
var showSourceSelector by mutableStateOf(false)
|
var showSourceSelector by mutableStateOf(false)
|
||||||
@@ -311,7 +306,7 @@ class SelectedAppInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class Error(@StringRes val resourceId: Int) {
|
enum class Error(@param:StringRes val resourceId: Int) {
|
||||||
NoPlugins(R.string.downloader_no_plugins_available)
|
NoPlugins(R.string.downloader_no_plugins_available)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
package app.revanced.manager.ui.viewmodel
|
package app.revanced.manager.ui.viewmodel
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.BroadcastReceiver
|
import android.net.Uri
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableLongStateOf
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.revanced.manager.R
|
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.api.ReVancedAPI
|
||||||
import app.revanced.manager.network.dto.ReVancedAsset
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
import app.revanced.manager.network.service.HttpService
|
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.toast
|
||||||
import app.revanced.manager.util.uiSafe
|
import app.revanced.manager.util.uiSafe
|
||||||
import io.ktor.client.plugins.onDownload
|
import io.ktor.client.plugins.onDownload
|
||||||
@@ -31,7 +24,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.get
|
||||||
import org.koin.core.component.inject
|
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(
|
class UpdateViewModel(
|
||||||
private val downloadOnScreenEntry: Boolean
|
private val downloadOnScreenEntry: Boolean
|
||||||
@@ -39,10 +39,11 @@ class UpdateViewModel(
|
|||||||
private val app: Application by inject()
|
private val app: Application by inject()
|
||||||
private val reVancedAPI: ReVancedAPI by inject()
|
private val reVancedAPI: ReVancedAPI by inject()
|
||||||
private val http: HttpService by inject()
|
private val http: HttpService by inject()
|
||||||
private val pm: PM by inject()
|
|
||||||
private val networkInfo: NetworkInfo by inject()
|
private val networkInfo: NetworkInfo by inject()
|
||||||
private val fs: Filesystem 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)
|
var downloadedSize by mutableLongStateOf(0L)
|
||||||
private set
|
private set
|
||||||
var totalSize by mutableLongStateOf(0L)
|
var totalSize by mutableLongStateOf(0L)
|
||||||
@@ -62,14 +63,17 @@ class UpdateViewModel(
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
private val location = fs.tempDir.resolve("updater.apk")
|
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) {
|
init {
|
||||||
downloadUpdate()
|
viewModelScope.launch {
|
||||||
} else {
|
uiSafe(app, R.string.download_manager_failed, "Failed to download ReVanced Manager") {
|
||||||
state = State.CAN_DOWNLOAD
|
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 {
|
fun installUpdate() = viewModelScope.launch {
|
||||||
state = State.INSTALLING
|
state = State.INSTALLING
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
ackpineInstaller.createSession(Uri.fromFile(location)) {
|
||||||
|
confirmation = Confirmation.IMMEDIATE
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
|
||||||
pm.installApp(listOf(location))
|
when (result) {
|
||||||
}
|
is Session.State.Failed<InstallFailure> -> when (val failure = result.failure) {
|
||||||
|
is InstallFailure.Aborted -> state = State.CAN_INSTALL
|
||||||
private val installBroadcastReceiver = object : BroadcastReceiver() {
|
else -> {
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
val msg = failure.message.orEmpty()
|
||||||
intent?.let {
|
app.toast(app.getString(R.string.install_app_fail, msg))
|
||||||
val pmStatus = intent.getIntExtra(InstallService.EXTRA_INSTALL_STATUS, -999)
|
installError = msg
|
||||||
val extra =
|
state = State.FAILED
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
app.unregisterReceiver(installBroadcastReceiver)
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
location.delete()
|
location.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class State(@StringRes val title: Int) {
|
enum class State(@param:StringRes val title: Int) {
|
||||||
CAN_DOWNLOAD(R.string.update_available),
|
CAN_DOWNLOAD(R.string.update_available),
|
||||||
DOWNLOADING(R.string.downloading_manager_update),
|
DOWNLOADING(R.string.downloading_manager_update),
|
||||||
CAN_INSTALL(R.string.ready_to_install_update),
|
CAN_INSTALL(R.string.ready_to_install_update),
|
||||||
|
|||||||
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
30
app/src/main/java/app/revanced/manager/util/Ackpine.kt
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -2,11 +2,8 @@ package app.revanced.manager.util
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.PackageInfoFlags
|
import android.content.pm.PackageManager.PackageInfoFlags
|
||||||
import android.content.pm.PackageManager.NameNotFoundException
|
import android.content.pm.PackageManager.NameNotFoundException
|
||||||
@@ -16,8 +13,6 @@ import android.os.Build
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import app.revanced.manager.domain.repository.PatchBundleRepository
|
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.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
@@ -25,10 +20,13 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
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
|
import java.io.File
|
||||||
|
|
||||||
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
|
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class AppInfo(
|
data class AppInfo(
|
||||||
@@ -40,7 +38,8 @@ data class AppInfo(
|
|||||||
@SuppressLint("QueryPermissionsNeeded")
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
class PM(
|
class PM(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
patchBundleRepository: PatchBundleRepository
|
patchBundleRepository: PatchBundleRepository,
|
||||||
|
private val uninstaller: PackageUninstaller
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.IO)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
@@ -145,17 +144,11 @@ class PM(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun installApp(apks: List<File>) = withContext(Dispatchers.IO) {
|
suspend fun uninstallPackage(pkg: String, config: UninstallParametersDsl.() -> Unit = {}) = withContext(Dispatchers.IO) {
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
uninstaller.createSession(pkg) {
|
||||||
packageInstaller.openSession(packageInstaller.createSession(sessionParams)).use { session ->
|
confirmation = Confirmation.IMMEDIATE
|
||||||
apks.forEach { apk -> session.writeApk(apk) }
|
config()
|
||||||
session.commit(app.installIntentSender)
|
}.await()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun uninstallPackage(pkg: String) {
|
|
||||||
val packageInstaller = app.packageManager.packageInstaller
|
|
||||||
packageInstaller.uninstall(pkg, app.uninstallIntentSender)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
fun launch(pkg: String) = app.packageManager.getLaunchIntentForPackage(pkg)?.let {
|
||||||
@@ -164,44 +157,4 @@ class PM(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canInstallPackages() = app.packageManager.canRequestPackageInstalls()
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import app.revanced.manager.R
|
import app.revanced.manager.R
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -82,6 +83,8 @@ fun Context.toast(string: String, duration: Int = Toast.LENGTH_SHORT) {
|
|||||||
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
inline fun uiSafe(context: Context, @StringRes toastMsg: Int, logMsg: String, block: () -> Unit) {
|
||||||
try {
|
try {
|
||||||
block()
|
block()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (error: Exception) {
|
} catch (error: Exception) {
|
||||||
// You can only toast on the main thread.
|
// You can only toast on the main thread.
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
GlobalScope.launch(Dispatchers.Main) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ kotlin-process = "1.5.1"
|
|||||||
hidden-api-stub = "4.3.3"
|
hidden-api-stub = "4.3.3"
|
||||||
binary-compatibility-validator = "0.17.0"
|
binary-compatibility-validator = "0.17.0"
|
||||||
semver-parser = "3.0.0"
|
semver-parser = "3.0.0"
|
||||||
|
ackpine = "0.18.5"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
# AndroidX Core
|
# AndroidX Core
|
||||||
@@ -133,6 +134,10 @@ compose-icons-fontawesome = { group = "com.github.BenjaminHalko.compose-icons",
|
|||||||
# Semantic versioning parser
|
# Semantic versioning parser
|
||||||
semver-parser = { module = "io.github.z4kn4fein:semver", version.ref = "semver-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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
|
||||||
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
|
||||||
|
|||||||
Reference in New Issue
Block a user