Compare commits

..

1 Commits

Author SHA1 Message Date
Ax333l
05ace8180e fix: install dialog getting stuck 2025-12-29 21:53:06 +01:00
29 changed files with 334 additions and 747 deletions

View File

@@ -1,43 +0,0 @@
name: Pull strings
on:
schedule:
- cron: "0 0 * * 0"
workflow_dispatch:
jobs:
pull:
name: Pull strings
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: dev
clean: true
- name: Pull strings
uses: crowdin/github-action@v2
with:
config: crowdin.yml
upload_sources: false
download_translations: true
skip_ref_checkout: true
localization_branch_name: feat/translations
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Open pull request
if: github.event_name == 'workflow_dispatch'
uses: repo-sync/pull-request@v2
with:
source_branch: feat/translations
destination_branch: dev
pr_title: "chore: Sync translations"
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"

View File

@@ -1,26 +0,0 @@
name: Push strings
on:
workflow_dispatch:
push:
branches:
- dev
paths:
- app/src/main/res/values/strings.xml
jobs:
push:
name: Push strings
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Push strings
uses: crowdin/github-action@v2
with:
config: crowdin.yml
upload_sources: true
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -1,43 +1,3 @@
# app [1.26.0-dev.18](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.17...v1.26.0-dev.18) (2026-01-08)
### Bug Fixes
* Prevent trailing comma when no locales are generated ([b16931c](https://github.com/ReVanced/revanced-manager/commit/b16931ca79d5ce4d17c75f6dd3bf6f976b8ff7be))
### Features
* Add language settings ([#2913](https://github.com/ReVanced/revanced-manager/issues/2913)) ([df31b39](https://github.com/ReVanced/revanced-manager/commit/df31b39cc8c1fbf00bc3301468e8e7e4b283caf2))
# app [1.26.0-dev.17](https://github.com/ReVanced/revanced-manager/compare/v1.26.0-dev.16...v1.26.0-dev.17) (2026-01-06)
### Bug Fixes
* allow updating patches on metered networks ([9d9a0e8](https://github.com/ReVanced/revanced-manager/commit/9d9a0e81dbc9e73e6e3181f6bea9cabb69e49ea8))
# 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) # 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)

View File

@@ -149,6 +149,7 @@ android {
debug { debug {
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
resValue("string", "app_name", "ReVanced Manager (Debug)") resValue("string", "app_name", "ReVanced Manager (Debug)")
isPseudoLocalesEnabled = true
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L") buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
} }
@@ -242,8 +243,6 @@ android {
version = "3.22.1" version = "3.22.1"
} }
} }
sourceSets["main"].kotlin.srcDir(layout.buildDirectory.dir("generated/source/locales"))
} }
kotlin { kotlin {
@@ -251,46 +250,6 @@ kotlin {
} }
tasks { tasks {
val generateSupportedLocales by registering {
description = "Generate list of supported locales from resource directories"
val resDir = file("src/main/res")
val outputDir = layout.buildDirectory.dir("generated/source/locales")
inputs.dir(resDir)
outputs.dir(outputDir)
doLast {
val locales = resDir.listFiles()
.orEmpty()
.filter { it.isDirectory && it.name.matches(Regex("values-[a-z]{2}(-r[A-Z]{2})?")) }
.map { it.name.removePrefix("values-").replace("-r", "-") }
.sorted()
.joinToString("\n ") { "Locale.forLanguageTag(\"$it\")," }
val output = outputDir.get().asFile.resolve("app/revanced/manager/util/GeneratedLocales.kt")
output.parentFile.mkdirs()
output.writeText(
"""
|package app.revanced.manager.util
|
|import java.util.Locale
|
|object GeneratedLocales {
| val SUPPORTED_LOCALES = listOf(
| Locale.ENGLISH,$locales
| )
|}
""".trimMargin()
)
}
}
preBuild {
dependsOn(generateSupportedLocales)
}
// Needed by gradle-semantic-release-plugin. // Needed by gradle-semantic-release-plugin.
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435. // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
val publish by registering { val publish by registering {

View File

@@ -1 +1 @@
version = 1.26.0-dev.18 version = 1.26.0-dev.13

View File

@@ -1,4 +0,0 @@
// ProgressEventParcel.aidl
package app.revanced.manager.patcher;
parcelable ProgressEventParcel;

View File

@@ -1,12 +1,11 @@
// 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 event(in ProgressEventParcel event); void patchSucceeded();
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);
} }

View File

@@ -3,11 +3,11 @@ package app.revanced.manager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
@@ -63,7 +63,7 @@ import org.koin.androidx.compose.navigation.koinNavViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
class MainActivity : AppCompatActivity() { class MainActivity : ComponentActivity() {
@ExperimentalAnimationApi @ExperimentalAnimationApi
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@@ -15,5 +15,5 @@ class NetworkInfo(app: Application) {
/** /**
* Returns true if it is safe to download large files. * Returns true if it is safe to download large files.
*/ */
fun isSafe(ignoreMetered: Boolean) = isConnected() && (ignoreMetered || isUnmetered()) fun isSafe() = isConnected() && isUnmetered()
} }

View File

@@ -34,6 +34,4 @@ class PreferencesManager(
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet()) val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable) val showDeveloperSettings = booleanPreference("show_developer_settings", context.isDebuggable)
val allowMeteredNetworks = booleanPreference("allow_metered_networks", false)
} }

View File

@@ -286,29 +286,28 @@ class PatchBundleRepository(
State(sources.toPersistentMap(), info.toPersistentMap()) State(sources.toPersistentMap(), info.toPersistentMap())
} }
suspend fun createLocal(createStream: suspend () -> InputStream) = suspend fun createLocal(createStream: suspend () -> InputStream) = dispatchAction("Add bundle") {
dispatchAction("Add bundle") { with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) {
with(createEntity("", SourceInfo.Local).load() as LocalPatchBundle) { try {
try { createStream().use { patches -> replace(patches) }
createStream().use { patches -> replace(patches) } } catch (e: Exception) {
} catch (e: Exception) { if (e is CancellationException) throw e
if (e is CancellationException) throw e Log.e(tag, "Got exception while importing bundle", e)
Log.e(tag, "Got exception while importing bundle", e) withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
app.toast(app.getString(R.string.patches_replace_fail, e.simpleMessage()))
}
deleteLocalFile()
} }
}
doReload() deleteLocalFile()
}
} }
doReload()
}
suspend fun createRemote(url: String, autoUpdate: Boolean) = suspend fun createRemote(url: String, autoUpdate: Boolean) =
dispatchAction("Add bundle ($url)") { state -> dispatchAction("Add bundle ($url)") { state ->
val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle val src = createEntity("", SourceInfo.from(url), autoUpdate).load() as RemotePatchBundle
update(src, force = true) update(src)
state.copy(sources = state.sources.put(src.uid, src)) state.copy(sources = state.sources.put(src.uid, src))
} }
@@ -330,38 +329,32 @@ class PatchBundleRepository(
state.copy(sources = state.sources.put(uid, newSrc)) state.copy(sources = state.sources.put(uid, newSrc))
} }
suspend fun update( suspend fun update(vararg sources: RemotePatchBundle, showToast: Boolean = false) {
vararg sources: RemotePatchBundle,
showToast: Boolean = false,
force: Boolean = false
) {
val uids = sources.map { it.uid }.toSet() val uids = sources.map { it.uid }.toSet()
store.dispatch(Update(showToast = showToast, force = force) { it.uid in uids }) store.dispatch(Update(showToast = showToast) { it.uid in uids })
} }
suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true, redownload = true)) suspend fun redownloadRemoteBundles() = store.dispatch(Update(force = true))
/** /**
* Updates all bundles that should be automatically updated. * Updates all bundles that should be automatically updated.
*/ */
suspend fun updateCheck() = suspend fun updateCheck() = store.dispatch(Update { it.autoUpdate })
store.dispatch(Update(force = prefs.allowMeteredNetworks.get()) { it.autoUpdate })
private inner class Update( private inner class Update(
private val force: Boolean = false, private val force: Boolean = false,
private val redownload: Boolean = false,
private val showToast: Boolean = false, private val showToast: Boolean = false,
private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true }, private val predicate: (bundle: RemotePatchBundle) -> Boolean = { true },
) : Action<State> { ) : Action<State> {
private suspend fun toast(@StringRes id: Int, vararg args: Any?) = private suspend fun toast(@StringRes id: Int, vararg args: Any?) =
withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) } withContext(Dispatchers.Main) { app.toast(app.getString(id, *args)) }
override fun toString() = if (redownload) "Redownload remote bundles" else "Update check" override fun toString() = if (force) "Redownload remote bundles" else "Update check"
override suspend fun ActionContext.execute( override suspend fun ActionContext.execute(
current: State current: State
) = coroutineScope { ) = coroutineScope {
if (!networkInfo.isSafe(force)) { if (!networkInfo.isSafe()) {
Log.d(tag, "Skipping update check because the network is down or metered.") Log.d(tag, "Skipping update check because the network is down or metered.")
return@coroutineScope current return@coroutineScope current
} }
@@ -374,7 +367,7 @@ class PatchBundleRepository(
Log.d(tag, "Updating patch bundle: ${it.name}") Log.d(tag, "Updating patch bundle: ${it.name}")
val newVersion = with(it) { val newVersion = with(it) {
if (redownload) downloadLatest() else update() if (force) downloadLatest() else update()
} ?: return@async null } ?: return@async null
it to newVersion it to newVersion

View File

@@ -1,78 +0,0 @@
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
}

View File

@@ -1,9 +1,10 @@
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.patcher.Session.Companion.component1 import app.revanced.manager.R
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
@@ -21,10 +22,15 @@ 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 onEvent: (ProgressEvent) -> Unit, private val onPatchCompleted: suspend () -> 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(
@@ -36,68 +42,86 @@ 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) ->
val index = selectedPatches.indexOf(patch) if (patch !in selectedPatches) return@collect
if (index == -1) return@collect
if (exception != null) { if (exception != null) {
onEvent( updateProgress(
ProgressEvent.Failed( name = androidContext.getString(R.string.failed_to_execute_patch, patch.name),
StepId.ExecutePatch(index), state = State.FAILED,
exception.toRemoteError(), message = exception.stackTraceToString()
)
) )
logger.error("${patch.name} failed:") logger.error("${patch.name} failed:")
logger.error(exception.stackTraceToString()) logger.error(exception.stackTraceToString())
throw exception throw exception
} }
onEvent( nextPatchIndex++
ProgressEvent.Completed(
StepId.ExecutePatch(index), onPatchCompleted()
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) {
runStep(StepId.ExecutePatches, onEvent) { updateProgress(state = State.COMPLETED) // Unpacking
java.util.logging.Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
}
addHandler(logger.handler) java.util.logging.Logger.getLogger("").apply {
handlers.forEach {
it.close()
removeHandler(it)
} }
with(patcher) { addHandler(logger.handler)
logger.info("Merging integrations")
this += selectedPatches.toSet()
logger.info("Applying patches...")
applyPatchesVerbose(selectedPatches.sortedBy { it.name })
}
} }
runStep(StepId.WriteAPK, onEvent) { with(patcher) {
logger.info("Writing patched files...") logger.info("Merging integrations")
val result = patcher.get() this += selectedPatches.toSet()
val patched = tempDir.resolve("result.apk") logger.info("Applying patches...")
withContext(Dispatchers.IO) { applyPatchesVerbose(selectedPatches.sortedBy { it.name })
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() {

View File

@@ -1,12 +1,11 @@
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.runStep import app.revanced.manager.patcher.worker.ProgressEventHandler
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
@@ -14,7 +13,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(context: Context) : Runtime(context) { class CoroutineRuntime(private val context: Context) : Runtime(context) {
override suspend fun execute( override suspend fun execute(
inputFile: String, inputFile: String,
outputFile: String, outputFile: String,
@@ -22,50 +21,47 @@ class CoroutineRuntime(context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onEvent: (ProgressEvent) -> Unit, onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler,
) { ) {
val patchList = runStep(StepId.LoadPatches, onEvent) { val selectedBundles = selectedPatches.keys
val selectedBundles = selectedPatches.keys val bundles = bundles()
val bundles = bundles() val uids = bundles.entries.associate { (key, value) -> value to key }
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
} }
val session = runStep(StepId.ReadAPK, onEvent) { onProgress(null, State.COMPLETED, null) // Loading patches
Session(
cacheDir,
frameworkPath,
aaptPath,
logger,
File(inputFile),
onEvent,
)
}
session.use { s -> Session(
s.run( cacheDir,
frameworkPath,
aaptPath,
context,
logger,
File(inputFile),
onPatchCompleted = onPatchCompleted,
onProgress
).use { session ->
session.run(
File(outputFile), File(outputFile),
patchList patchList
) )

View File

@@ -10,13 +10,12 @@ 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.toEvent import app.revanced.manager.patcher.worker.ProgressEventHandler
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
@@ -67,7 +66,8 @@ class ProcessRuntime(private val context: Context) : Runtime(context) {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onEvent: (ProgressEvent) -> Unit, onPatchCompleted: suspend () -> 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,6 +111,7 @@ 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()
@@ -123,10 +124,13 @@ 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 event(event: ProgressEventParcel?) { override fun patchSucceeded() {
event?.let { onEvent(it.toEvent()) } scope.launch { onPatchCompleted() }
} }
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()

View File

@@ -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,6 +34,7 @@ sealed class Runtime(context: Context) : KoinComponent {
selectedPatches: PatchSelection, selectedPatches: PatchSelection,
options: Options, options: Options,
logger: Logger, logger: Logger,
onEvent: (ProgressEvent) -> Unit, onPatchCompleted: suspend () -> Unit,
onProgress: ProgressEventHandler,
) )
} }

View File

@@ -8,15 +8,12 @@ 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.patcher.toParcel import app.revanced.manager.ui.model.State
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -27,7 +24,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() : IPatcherProcess.Stub() { class PatcherProcess(private val context: Context) : IPatcherProcess.Stub() {
private var eventBinder: IPatcherEvents? = null private var eventBinder: IPatcherEvents? = null
private val scope = private val scope =
@@ -49,8 +46,6 @@ class PatcherProcess() : 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 {
@@ -61,42 +56,38 @@ class PatcherProcess() : IPatcherProcess.Stub() {
logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB") logger.info("Memory limit: ${Runtime.getRuntime().maxMemory() / (1024 * 1024)}MB")
val patchList = runStep(StepId.LoadPatches, ::onEvent) { val allPatches = PatchBundle.Loader.patches(parameters.configurations.map { it.bundle }, parameters.packageName)
val allPatches = PatchBundle.Loader.patches( val patchList = parameters.configurations.flatMap { config ->
parameters.configurations.map { it.bundle }, val patches = (allPatches[config.bundle] ?: return@flatMap emptyList())
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
} }
val session = runStep(StepId.ReadAPK, ::onEvent) { events.progress(null, State.COMPLETED.name, null) // Loading patches
Session(
cacheDir = parameters.cacheDir,
aaptPath = parameters.aaptPath,
frameworkDir = parameters.frameworkDir,
logger = logger,
input = File(parameters.inputFile),
onEvent = ::onEvent,
)
}
session.use { Session(
cacheDir = parameters.cacheDir,
aaptPath = parameters.aaptPath,
frameworkDir = parameters.frameworkDir,
androidContext = context,
logger = logger,
input = File(parameters.inputFile),
onPatchCompleted = { events.patchSucceeded() },
onProgress = { name, state, message ->
events.progress(name, state?.name, message)
}
).use {
it.run(File(parameters.outputFile), patchList) it.run(File(parameters.outputFile), patchList)
} }
@@ -128,7 +119,7 @@ class PatcherProcess() : IPatcherProcess.Stub() {
} }
} }
val ipcInterface = PatcherProcess() val ipcInterface = PatcherProcess(appContext)
appContext.sendBroadcast(Intent().apply { appContext.sendBroadcast(Intent().apply {
action = ProcessRuntime.CONNECT_TO_APP_ACTION action = ProcessRuntime.CONNECT_TO_APP_ACTION

View File

@@ -29,17 +29,14 @@ 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
@@ -51,6 +48,8 @@ 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,
@@ -72,9 +71,11 @@ 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 onEvent: (ProgressEvent) -> Unit, val onProgress: ProgressEventHandler
) { ) {
val packageName get() = input.packageName val packageName get() = input.packageName
} }
@@ -139,6 +140,10 @@ 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 {
@@ -158,65 +163,51 @@ class PatcherWorker(
args.input.version, args.input.version,
prefs.suggestedVersionSafeguard.get(), prefs.suggestedVersionSafeguard.get(),
!prefs.disablePatchVersionCompatCheck.get(), !prefs.disablePatchVersionCompatCheck.get(),
onDownload = { progress -> onDownload = args.onDownloadProgress
args.onEvent( ).also {
ProgressEvent.Progress( args.setInputFile(it)
stepId = StepId.DownloadAPK, updateProgress(state = State.COMPLETED) // Download APK
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 -> {
runStep(StepId.DownloadAPK, args.onEvent) { val (plugin, data) = downloaderPluginRepository.unwrapParceledData(selectedApp.data)
val (plugin, data) = downloaderPluginRepository.unwrapParceledData(
selectedApp.data
)
download(plugin, data) download(plugin, data)
}
} }
is SelectedApp.Search -> { is SelectedApp.Search -> {
runStep(StepId.DownloadAPK, args.onEvent) { downloaderPluginRepository.loadedPluginsFlow.first()
downloaderPluginRepository.loadedPluginsFlow.first() .firstNotNullOfOrNull { plugin ->
.firstNotNullOfOrNull { plugin -> try {
try { val getScope = object : GetScope {
val getScope = object : GetScope { override val pluginPackageName = plugin.packageName
override val pluginPackageName = plugin.packageName override val hostPackageName = applicationContext.packageName
override val hostPackageName = override suspend fun requestStartActivity(intent: Intent): Intent? {
applicationContext.packageName val result = args.handleStartActivityRequest(plugin, intent)
return when (result.resultCode) {
override suspend fun requestStartActivity(intent: Intent): Intent? { Activity.RESULT_OK -> result.data
val result = Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled()
args.handleStartActivityRequest(plugin, intent) else -> throw UserInteractionException.Activity.NotCompleted(
return when (result.resultCode) { result.resultCode,
Activity.RESULT_OK -> result.data result.data
Activity.RESULT_CANCELED -> throw UserInteractionException.Activity.Cancelled() )
else -> throw UserInteractionException.Activity.NotCompleted(
result.resultCode,
result.data
)
}
} }
} }
withContext(Dispatchers.IO) { }
plugin.get( withContext(Dispatchers.IO) {
getScope, plugin.get(
selectedApp.packageName, getScope,
selectedApp.version selectedApp.packageName,
) selectedApp.version
}?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version } )
} catch (e: UserInteractionException.Activity.NotCompleted) { }?.takeIf { (_, version) -> selectedApp.version == null || version == selectedApp.version }
throw e } catch (e: UserInteractionException.Activity.NotCompleted) {
} catch (_: UserInteractionException) { throw e
null } catch (_: UserInteractionException) {
}?.let { (data, _) -> download(plugin, data) } null
} ?: throw Exception("App is not available.") }?.let { (data, _) -> download(plugin, data) }
} } ?: throw Exception("App is not available.")
} }
is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) } is SelectedApp.Local -> selectedApp.file.also { args.setInputFile(it) }
@@ -236,12 +227,12 @@ class PatcherWorker(
args.selectedPatches, args.selectedPatches,
args.options, args.options,
args.logger, args.logger,
args.onEvent, args.onPatchCompleted,
args.onProgress
) )
runStep(StepId.SignAPK, args.onEvent) { keystoreManager.sign(patchedApk, File(args.output))
keystoreManager.sign(patchedApk, File(args.output)) updateProgress(state = State.COMPLETED) // Signing
}
Log.i(tag, "Patching succeeded".logFmt()) Log.i(tag, "Patching succeeded".logFmt())
Result.success() Result.success()
@@ -250,11 +241,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()
) )
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step updateProgress(state = State.FAILED, message = e.originalStackTrace)
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)
args.onEvent(ProgressEvent.Failed(null, e.toRemoteError())) // Fallback if exception doesn't occur within step updateProgress(state = State.FAILED, message = e.stackTraceToString())
Result.failure() Result.failure()
} finally { } finally {
patchedApk.delete() patchedApk.delete()

View File

@@ -39,9 +39,11 @@ 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.StepCategory
import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider
import java.util.Locale import java.util.Locale
import kotlin.math.floor import kotlin.math.floor
@@ -50,6 +52,8 @@ import kotlin.math.floor
fun Steps( fun Steps(
category: StepCategory, category: StepCategory,
steps: List<Step>, steps: List<Step>,
stepCount: Pair<Int, Int>? = null,
stepProgressProvider: StepProgressProvider,
isExpanded: Boolean = false, isExpanded: Boolean = false,
onExpand: () -> Unit, onExpand: () -> Unit,
onClick: () -> Unit onClick: () -> Unit
@@ -63,17 +67,8 @@ 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) { LaunchedEffect(state) {
if (state == State.RUNNING || state == State.FAILED) if (state == State.RUNNING)
onExpand() onExpand()
} }
@@ -97,8 +92,13 @@ fun Steps(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
val stepProgress = remember(stepCount, steps) {
stepCount?.let { (current, total) -> "$current/$total" }
?: "${steps.count { it.state == State.COMPLETED }}/${steps.size}"
}
Text( Text(
text = "${filteredSteps.count { it.state == State.COMPLETED }}/${filteredSteps.size}", text = stepProgress,
style = MaterialTheme.typography.labelSmall style = MaterialTheme.typography.labelSmall
) )
@@ -112,20 +112,23 @@ fun Steps(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 10.dp) .padding(top = 10.dp)
) { ) {
filteredSteps.forEachIndexed { index, step -> steps.forEachIndexed { index, step ->
val (progress, progressText) = step.progress?.let { (current, total) -> val (progress, progressText) = when (step.progressKey) {
if (total != null) current.toFloat() / total.toFloat() to "${current.megaBytes}/${total.megaBytes} MB" null -> null
else null to "${current.megaBytes} MB" ProgressKey.DOWNLOAD -> stepProgressProvider.downloadProgress?.let { (downloaded, total) ->
if (total != null) downloaded.toFloat() / total.toFloat() to "${downloaded.megaBytes}/${total.megaBytes} MB"
else null to "${downloaded.megaBytes} MB"
}
} ?: (null to null) } ?: (null to null)
SubStep( SubStep(
name = step.title, name = step.name,
state = step.state, state = step.state,
message = step.message, message = step.message,
progress = progress, progress = progress,
progressText = progressText, progressText = progressText,
isFirst = index == 0, isFirst = index == 0,
isLast = index == filteredSteps.lastIndex, isLast = index == steps.lastIndex,
) )
} }
} }

View File

@@ -3,7 +3,6 @@ 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) {
@@ -16,20 +15,19 @@ 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 id: StepId, val name: String,
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 progress: Pair<Long, Long?>? = null, val progressKey: ProgressKey? = null
val hide: Boolean = false, ) : Parcelable
) : 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)

View File

@@ -87,7 +87,7 @@ fun PatcherScreen(
val steps by remember { val steps by remember {
derivedStateOf { derivedStateOf {
viewModel.steps.groupBy { it.category }.toList() viewModel.steps.groupBy { it.category }
} }
} }
@@ -230,12 +230,14 @@ fun PatcherScreen(
contentPadding = PaddingValues(16.dp) contentPadding = PaddingValues(16.dp)
) { ) {
items( items(
items = steps, items = steps.toList(),
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,
stepProgressProvider = viewModel,
isExpanded = expandedCategory == category, isExpanded = expandedCategory == category,
onExpand = { expandCategory(category) }, onExpand = { expandCategory(category) },
onClick = { onClick = {

View File

@@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
@@ -21,7 +19,6 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -41,7 +38,6 @@ import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel import app.revanced.manager.ui.viewmodel.GeneralSettingsViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -52,7 +48,6 @@ fun GeneralSettingsScreen(
val prefs = viewModel.prefs val prefs = viewModel.prefs
val coroutineScope = viewModel.viewModelScope val coroutineScope = viewModel.viewModelScope
var showThemePicker by rememberSaveable { mutableStateOf(false) } var showThemePicker by rememberSaveable { mutableStateOf(false) }
var showLanguagePicker by rememberSaveable { mutableStateOf(false) }
if (showThemePicker) { if (showThemePicker) {
ThemePicker( ThemePicker(
@@ -60,17 +55,6 @@ fun GeneralSettingsScreen(
onConfirm = { viewModel.setTheme(it) } onConfirm = { viewModel.setTheme(it) }
) )
} }
if (showLanguagePicker) {
LanguagePicker(
supportedLocales = viewModel.getSupportedLocales(),
currentLocale = viewModel.getCurrentLocale(),
onDismiss = { showLanguagePicker = false },
onConfirm = { viewModel.setLocale(it) },
getDisplayName = { viewModel.getLocaleDisplayName(it) }
)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold( Scaffold(
@@ -90,24 +74,6 @@ fun GeneralSettingsScreen(
) { ) {
GroupHeader(stringResource(R.string.appearance)) GroupHeader(stringResource(R.string.appearance))
val currentLocale = viewModel.getCurrentLocale()
val currentLanguageDisplay = remember(currentLocale) {
currentLocale?.let { viewModel.getLocaleDisplayName(it) }
}
SettingsListItem(
modifier = Modifier.clickable { showLanguagePicker = true },
headlineContent = stringResource(R.string.language),
supportingContent = stringResource(R.string.language_description),
trailingContent = {
FilledTonalButton(onClick = { showLanguagePicker = true }) {
Text(
currentLanguageDisplay
?: stringResource(R.string.language_system_default)
)
}
}
)
val theme by prefs.theme.getAsState() val theme by prefs.theme.getAsState()
SettingsListItem( SettingsListItem(
modifier = Modifier.clickable { showThemePicker = true }, modifier = Modifier.clickable { showThemePicker = true },
@@ -139,14 +105,6 @@ fun GeneralSettingsScreen(
description = R.string.pure_black_theme_description description = R.string.pure_black_theme_description
) )
} }
GroupHeader(stringResource(R.string.networking))
BooleanItem(
preference = prefs.allowMeteredNetworks,
coroutineScope = coroutineScope,
headline = R.string.allow_metered_networks,
description = R.string.allow_metered_networks_description
)
} }
} }
} }
@@ -190,64 +148,4 @@ private fun ThemePicker(
} }
} }
) )
}
@Composable
private fun LanguagePicker(
supportedLocales: List<Locale>,
currentLocale: Locale?,
onDismiss: () -> Unit,
onConfirm: (Locale?) -> Unit,
getDisplayName: (Locale) -> String
) {
var selectedLocale by remember { mutableStateOf(currentLocale) }
val systemDefaultString = stringResource(R.string.language_system_default)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.language)) },
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedLocale = null },
verticalAlignment = Alignment.CenterVertically
) {
HapticRadioButton(
selected = selectedLocale == null,
onClick = { selectedLocale = null }
)
Text(systemDefaultString)
}
supportedLocales.forEach { locale ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { selectedLocale = locale },
verticalAlignment = Alignment.CenterVertically
) {
HapticRadioButton(
selected = selectedLocale == locale,
onClick = { selectedLocale = locale }
)
Text(getDisplayName(locale))
}
}
}
},
confirmButton = {
TextButton(
onClick = {
onConfirm(selectedLocale)
onDismiss()
}
) {
Text(stringResource(R.string.apply))
}
}
)
} }

View File

@@ -54,7 +54,6 @@ class BundleListViewModel : ViewModel(), KoinComponent {
patchBundleRepository.update( patchBundleRepository.update(
*getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(), *getSelectedSources().filterIsInstance<RemotePatchBundle>().toTypedArray(),
showToast = true, showToast = true,
force = true
) )
} }
} }
@@ -66,7 +65,7 @@ class BundleListViewModel : ViewModel(), KoinComponent {
fun update(src: PatchBundleSource) = viewModelScope.launch { fun update(src: PatchBundleSource) = viewModelScope.launch {
if (src !is RemotePatchBundle) return@launch if (src !is RemotePatchBundle) return@launch
patchBundleRepository.update(src, showToast = true, force = true) patchBundleRepository.update(src, showToast = true)
} }
enum class Event { enum class Event {

View File

@@ -1,26 +1,17 @@
package app.revanced.manager.ui.viewmodel package app.revanced.manager.ui.viewmodel
import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.manager.PreferencesManager
import app.revanced.manager.ui.theme.Theme import app.revanced.manager.ui.theme.Theme
import app.revanced.manager.util.SupportedLocales
import app.revanced.manager.util.resetListItemColorsCached import app.revanced.manager.util.resetListItemColorsCached
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale
class GeneralSettingsViewModel( class GeneralSettingsViewModel(
private val app: Application,
val prefs: PreferencesManager val prefs: PreferencesManager
) : ViewModel() { ) : ViewModel() {
fun setTheme(theme: Theme) = viewModelScope.launch { fun setTheme(theme: Theme) = viewModelScope.launch {
prefs.theme.update(theme) prefs.theme.update(theme)
resetListItemColorsCached() resetListItemColorsCached()
} }
fun getSupportedLocales() = SupportedLocales.getSupportedLocales(app)
fun getCurrentLocale() = SupportedLocales.getCurrentLocale()
fun setLocale(locale: Locale?) = SupportedLocales.setLocale(locale)
fun getLocaleDisplayName(locale: Locale) = SupportedLocales.getDisplayName(locale)
} }

View File

@@ -29,22 +29,20 @@ 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.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.StepCategory
import app.revanced.manager.ui.model.Step import app.revanced.manager.ui.model.Step
import app.revanced.manager.ui.model.StepCategory
import app.revanced.manager.ui.model.StepProgressProvider
import app.revanced.manager.ui.model.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.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
@@ -82,7 +80,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, InstallerModel { ) : ViewModel(), KoinComponent, StepProgressProvider, 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()
@@ -159,15 +157,35 @@ class PatcherViewModel(
} }
} }
val steps by savedStateHandle.saveable(saver = snapshotStateListSaver()) { private val patchCount = input.selectedPatches.values.sumOf { it.size }
generateSteps(app, input.selectedApp, input.selectedPatches).toMutableStateList() 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()) {
generateSteps(
app,
input.selectedApp
).toMutableStateList()
}
private var currentStepIndex = 0
val progress by derivedStateOf { val progress by derivedStateOf {
val steps = steps.filter { it.id != StepId.ExecutePatches } val current = steps.count {
it.state == State.COMPLETED && it.category != StepCategory.PATCHING
} + completedPatchCount
val current = steps.count { it.state == State.COMPLETED } val total = steps.size - 1 + patchCount
val total = steps.size
current.toFloat() / total.toFloat() current.toFloat() / total.toFloat()
} }
@@ -183,6 +201,12 @@ class PatcherViewModel(
input.selectedPatches, input.selectedPatches,
input.options, input.options,
logger, logger,
onDownloadProgress = {
withContext(Dispatchers.Main) {
downloadProgress = it
}
},
onPatchCompleted = { withContext(Dispatchers.Main) { completedPatchCount += 1 } },
setInputFile = { withContext(Dispatchers.Main) { inputFile = it } }, setInputFile = { withContext(Dispatchers.Main) { inputFile = it } },
handleStartActivityRequest = { plugin, intent -> handleStartActivityRequest = { plugin, intent ->
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -211,10 +235,26 @@ class PatcherViewModel(
} }
} }
}, },
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 =
@@ -268,35 +308,6 @@ 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() 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.
@@ -533,66 +544,34 @@ class PatcherViewModel(
LogLevel.ERROR -> Log.e(TAG, msg) LogLevel.ERROR -> Log.e(TAG, msg)
} }
fun generateSteps( fun generateSteps(context: Context, selectedApp: SelectedApp): List<Step> {
context: Context, val needsDownload =
selectedApp: SelectedApp, selectedApp is SelectedApp.Download || selectedApp is SelectedApp.Search
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
)
)
add( return listOfNotNull(
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 ),
)
)
selectedPatches.values.asSequence().flatten().sorted().forEachIndexed { index, name -> Step(context.getString(R.string.patcher_step_write_patched), StepCategory.SAVING),
add( Step(context.getString(R.string.patcher_step_sign_apk), StepCategory.SAVING)
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
)
) )
} }
} }

View File

@@ -82,7 +82,7 @@ class UpdateViewModel(
uiSafe(app, R.string.failed_to_download_update, "Failed to download update") { uiSafe(app, R.string.failed_to_download_update, "Failed to download update") {
val release = releaseInfo!! val release = releaseInfo!!
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!networkInfo.isSafe(false) && !ignoreInternetCheck) { if (!networkInfo.isSafe() && !ignoreInternetCheck) {
showInternetCheckDialog = true showInternetCheckDialog = true
} else { } else {
state = State.DOWNLOADING state = State.DOWNLOADING

View File

@@ -1,32 +0,0 @@
package app.revanced.manager.util
import android.content.Context
import android.os.Build
import android.os.LocaleList
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import java.util.Locale
object SupportedLocales {
fun getSupportedLocales(context: Context): List<Locale> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
runCatching {
android.app.LocaleConfig(context).supportedLocales?.toList()
}.getOrNull() ?: GeneratedLocales.SUPPORTED_LOCALES
} else {
GeneratedLocales.SUPPORTED_LOCALES
}
}
fun getCurrentLocale(): Locale? =
AppCompatDelegate.getApplicationLocales().takeIf { !it.isEmpty }?.get(0)
fun setLocale(locale: Locale?) = AppCompatDelegate.setApplicationLocales(
locale?.let { LocaleListCompat.create(it) } ?: LocaleListCompat.getEmptyLocaleList()
)
fun getDisplayName(locale: Locale) =
locale.getDisplayName(locale).replaceFirstChar { it.uppercase(locale) }
private fun LocaleList.toList() = (0 until size()).map { get(it) }
}

View File

@@ -14,7 +14,7 @@ Second \"item\" text"</string>
--> -->
<resources> <resources>
<string name="app_name">ReVanced Manager</string> <string name="app_name">ReVanced Manager</string>
<string name="patcher">Patcher test</string> <string name="patcher">Patcher</string>
<string name="patches">Patches</string> <string name="patches">Patches</string>
<string name="cli">CLI</string> <string name="cli">CLI</string>
<string name="manager">Manager</string> <string name="manager">Manager</string>
@@ -83,7 +83,7 @@ Second \"item\" text"</string>
<string name="auto_updates_dialog_note">These settings can be changed later.</string> <string name="auto_updates_dialog_note">These settings can be changed later.</string>
<string name="general">General</string> <string name="general">General</string>
<string name="general_description">Language, theme, dynamic color</string> <string name="general_description">Theme, dynamic color</string>
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="updates_description">Check for updates and view changelogs</string> <string name="updates_description">Check for updates and view changelogs</string>
<string name="downloads">Downloads</string> <string name="downloads">Downloads</string>
@@ -104,9 +104,6 @@ Second \"item\" text"</string>
<string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string> <string name="pure_black_theme_description">Use pure black backgrounds for dark theme</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="theme_description">Choose between light or dark theme</string> <string name="theme_description">Choose between light or dark theme</string>
<string name="language">Language</string>
<string name="language_description">Choose the app display language</string>
<string name="language_system_default">System default</string>
<string name="safeguards">Safeguards</string> <string name="safeguards">Safeguards</string>
<string name="patch_compat_check">Disable version compatibility check</string> <string name="patch_compat_check">Disable version compatibility check</string>
<string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string> <string name="patch_compat_check_description">Do not restrict patches to compatible app versions</string>
@@ -220,10 +217,6 @@ You will not be able to update the previously installed apps from this source."<
<string name="light">Light</string> <string name="light">Light</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="appearance">Appearance</string> <string name="appearance">Appearance</string>
<string name="networking">Networking</string>
<string name="allow_metered_networks">Allow metered networks</string>
<string name="allow_metered_networks_description">Permits automatic updates on metered networks.
The application might still warn about metered networks for manual operations.</string>
<string name="downloaded_apps">Downloaded apps</string> <string name="downloaded_apps">Downloaded apps</string>
<string name="process_runtime">Run Patcher in another process (experimental)</string> <string name="process_runtime">Run Patcher in another process (experimental)</string>
<string name="process_runtime_description">This is faster and allows Patcher to use more memory</string> <string name="process_runtime_description">This is faster and allows Patcher to use more memory</string>

View File

@@ -1,9 +0,0 @@
project_id_env: "CROWDIN_PROJECT_ID"
api_token_env: "CROWDIN_PERSONAL_TOKEN"
preserve_hierarchy: true
files:
- source: app/src/main/res/values/strings.xml
dest: manager.xml
translation: app/src/main/res/values-%android_code%/strings.xml
skip_untranslated_strings: true