mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-06-18 05:23:12 +10:00
Manager UI Updates & New patch logic <3
This commit is contained in:
@@ -102,8 +102,6 @@ class ManagerApplication : Application() {
|
||||
.build()
|
||||
}
|
||||
|
||||
// Schedule periodic update check only when the user has opted in,
|
||||
// so the disabled state survives app restarts instead of being re-enqueued.
|
||||
if (get<PreferencesManager>().autoUpdateCheck) {
|
||||
UpdateCheckWorker.schedule(this)
|
||||
} else {
|
||||
|
||||
@@ -47,8 +47,6 @@ fun Scope.provideHttpClient() = HttpClient(OkHttp) {
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
val addresses = Dns.SYSTEM.lookup(hostname)
|
||||
|
||||
// Github's nameservers do not respond to IPv6 requests for raw.githubusercontent.com,
|
||||
// which causes CIO, Android and OkHTTP to all hang
|
||||
return if (hostname == "raw.githubusercontent.com") {
|
||||
addresses.filterIsInstance<Inet4Address>()
|
||||
} else {
|
||||
@@ -77,7 +75,7 @@ fun Scope.provideHttpClient() = HttpClient(OkHttp) {
|
||||
// Default storage is in-memory
|
||||
}
|
||||
|
||||
// Custom plugin to allow overriding response cache headers, and force caching
|
||||
// Custom plugin to allow overriding response cache headers (force caching)
|
||||
install("OverrideCacheControl") {
|
||||
receivePipeline.intercept(HttpReceivePipeline.Before) { response ->
|
||||
val customCacheControl = response.call.attributes.getOrNull(CustomCacheControl)
|
||||
|
||||
+1
-2
@@ -107,8 +107,7 @@ class DhizukuInstaller(
|
||||
)
|
||||
}
|
||||
|
||||
// Unregister PMResultReceiver when this coroutine finishes or errors
|
||||
// Explicitly cancel the install session if it did not finish.
|
||||
// cancel the install session if it did not finish.
|
||||
continuation.invokeOnCancellation {
|
||||
context.unregisterReceiver(relayReceiver)
|
||||
sessionCallback?.let { packageInstaller.unregisterSessionCallback(it) }
|
||||
|
||||
@@ -39,8 +39,6 @@ class PMResultReceiver(
|
||||
context.showToast(if (!isUninstall) R.string.installer_install_success else R.string.installer_uninstall_success)
|
||||
}
|
||||
|
||||
// The reason we don't do this in PMIntentReceiver is we can't tell whether it was
|
||||
// an old session that for which `abandonSession(...)` was called
|
||||
is InstallerResult.Cancelled -> {
|
||||
context.showToast(if (!isUninstall) R.string.installer_install_aborted else R.string.installer_uninstall_aborted)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class RootInstaller(private val context: Context) : Installer {
|
||||
}
|
||||
|
||||
private companion object {
|
||||
// We spoof Google Play Store to prevent unnecessary checks
|
||||
// spoof Google Play Store to prevent unnecessary checks
|
||||
const val PLAY_PACKAGE_NAME = "com.android.vending"
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* The package name of Google Play Store.
|
||||
* We spoof our installer to this when installing through Shizuku to prevent
|
||||
* spoof our installer to this when installing through Shizuku to prevent
|
||||
* potentially unnecessary scans/checks.
|
||||
*/
|
||||
private const val PLAY_PACKAGE_NAME = "com.android.vending"
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ class CopyDependenciesStep : Step(), KoinComponent {
|
||||
val targetFileStorageId = storageManager.getUuidForPath(apk)
|
||||
val fileSize = srcApk.length()
|
||||
|
||||
// We request 3.5x the size of the APK, to give space for the following:
|
||||
// request 3.5x the size of the APK, to give space for the following:
|
||||
// 1) A copy of the APK
|
||||
// 2) Modifying the copied APK (whether this is necessary I'm not sure)
|
||||
// 2) Extracting native libs and other various operations
|
||||
|
||||
+2
-2
@@ -272,7 +272,7 @@ class SmaliPatchStep(
|
||||
val matchPos = findContextMatch(result, sourceLines, delta.source.position)
|
||||
?: throw Exception(
|
||||
"Fuzzy match failed: could not locate context for hunk near line " +
|
||||
"${delta.source.position + 1} (${sourceLines.size} lines)"
|
||||
"${delta.source.position + 1} (${sourceLines.size} lines)"
|
||||
)
|
||||
|
||||
repeat(sourceLines.size) { result.removeAt(matchPos) }
|
||||
@@ -288,7 +288,7 @@ class SmaliPatchStep(
|
||||
// Try at the exact hint first.
|
||||
if (matchesAt(target, sourceLines, hint)) return hint
|
||||
|
||||
// Walk outward from the hint, alternating below/above, until we find a unique match.
|
||||
// Walk outward from the hint, alternating below/above, until finds a unique match.
|
||||
val maxRadius = target.size
|
||||
for (offset in 1..maxRadius) {
|
||||
val below = hint + offset
|
||||
|
||||
@@ -25,7 +25,7 @@ object ManifestPatcher {
|
||||
appName: String,
|
||||
debuggable: Boolean,
|
||||
): ByteArray {
|
||||
// Extract original package name so we can rewrite every reference to it
|
||||
// Extract original package name to rewrite every reference to it
|
||||
// (permissions, provider authorities) to the new packageName.
|
||||
var originalPackage: String? = null
|
||||
AxmlReader(manifestBytes).accept(object : AxmlVisitor() {
|
||||
@@ -58,11 +58,12 @@ object ManifestPatcher {
|
||||
COMPILE_SDK_VERSION_CODENAME to "6.0-2438415"
|
||||
)
|
||||
) {
|
||||
// Drop split-only manifest attributes — we merged all splits into one APK
|
||||
// Drop split-only manifest attributes because merged all splits into one APK
|
||||
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||
if (name == IS_SPLIT_REQUIRED || name == REQUIRED_SPLIT_TYPES || name == SPLIT_TYPES) return
|
||||
super.attr(ns, name, resourceId, type, value)
|
||||
}
|
||||
|
||||
private var addExternalStoragePerm = false
|
||||
|
||||
override fun child(ns: String?, name: String): NodeVisitor {
|
||||
@@ -72,7 +73,13 @@ object ManifestPatcher {
|
||||
if (addExternalStoragePerm) {
|
||||
super
|
||||
.child(null, "uses-permission")
|
||||
.attr(ANDROID_NAMESPACE, "name", android.R.attr.name, TYPE_STRING, Manifest.permission.MANAGE_EXTERNAL_STORAGE)
|
||||
.attr(
|
||||
ANDROID_NAMESPACE,
|
||||
"name",
|
||||
android.R.attr.name,
|
||||
TYPE_STRING,
|
||||
Manifest.permission.MANAGE_EXTERNAL_STORAGE
|
||||
)
|
||||
addExternalStoragePerm = false
|
||||
}
|
||||
|
||||
@@ -141,7 +148,13 @@ object ManifestPatcher {
|
||||
if (addMetadata) {
|
||||
addMetadata = false
|
||||
super.child(ANDROID_NAMESPACE, "meta-data").apply {
|
||||
attr(ANDROID_NAMESPACE, "name", android.R.attr.name, TYPE_STRING, "isRadiantLyrics")
|
||||
attr(
|
||||
ANDROID_NAMESPACE,
|
||||
"name",
|
||||
android.R.attr.name,
|
||||
TYPE_STRING,
|
||||
"isRadiantLyrics"
|
||||
)
|
||||
attr(ANDROID_NAMESPACE, "value", android.R.attr.value, TYPE_INT_BOOLEAN, 1)
|
||||
}
|
||||
}
|
||||
@@ -149,7 +162,13 @@ object ManifestPatcher {
|
||||
return when (name) {
|
||||
"activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName))
|
||||
"provider" -> object : NodeVisitor(visitor) {
|
||||
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||
override fun attr(
|
||||
ns: String?,
|
||||
name: String,
|
||||
resourceId: Int,
|
||||
type: Int,
|
||||
value: Any?
|
||||
) {
|
||||
super.attr(
|
||||
ns, name, resourceId, type,
|
||||
if (name == "authorities") {
|
||||
@@ -173,11 +192,23 @@ object ManifestPatcher {
|
||||
TYPE_INT_BOOLEAN,
|
||||
1
|
||||
)
|
||||
if (addDebuggable) super.attr(ANDROID_NAMESPACE, DEBUGGABLE, android.R.attr.debuggable, TYPE_INT_BOOLEAN, 1)
|
||||
if (addDebuggable) super.attr(
|
||||
ANDROID_NAMESPACE,
|
||||
DEBUGGABLE,
|
||||
android.R.attr.debuggable,
|
||||
TYPE_INT_BOOLEAN,
|
||||
1
|
||||
)
|
||||
|
||||
// Disable AOT (Necessary for AOSP Android 15)
|
||||
if (Build.VERSION.SDK_INT >= 29 && addUseEmbeddedDex) {
|
||||
super.attr(ANDROID_NAMESPACE, USE_EMBEDDED_DEX, android.R.attr.useEmbeddedDex, TYPE_INT_BOOLEAN, 1)
|
||||
super.attr(
|
||||
ANDROID_NAMESPACE,
|
||||
USE_EMBEDDED_DEX,
|
||||
android.R.attr.useEmbeddedDex,
|
||||
TYPE_INT_BOOLEAN,
|
||||
1
|
||||
)
|
||||
}
|
||||
|
||||
if (addExtractNativeLibs) super.attr(
|
||||
@@ -209,7 +240,13 @@ object ManifestPatcher {
|
||||
val replace = attrs.containsKey(name)
|
||||
val newValue = attrs[name]
|
||||
|
||||
super.attr(ns, name, resourceId, if (newValue is String) TYPE_STRING else type, if (replace) newValue else value)
|
||||
super.attr(
|
||||
ns,
|
||||
name,
|
||||
resourceId,
|
||||
if (newValue is String) TYPE_STRING else type,
|
||||
if (replace) newValue else value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -150,15 +150,13 @@ class PatchingScreenModel(
|
||||
.toUnsafeImmutable()
|
||||
mainThread { steps = newSteps }
|
||||
|
||||
// Intentionally delay to show the state change of the first step when it runs in the UI.
|
||||
// Without this, on a fast internet connection the step just immediately shows as "Success".
|
||||
// Tiny delay so the first step's progress is visible on fast connections
|
||||
delay(400)
|
||||
|
||||
// Execute all the steps and catch any errors
|
||||
val error = when (val error = runner.executeAll()) {
|
||||
null -> {
|
||||
// If install step is marked skipped then the installation was manually aborted
|
||||
// and if so, immediately close install screen
|
||||
// Skipped install step means user aborted, close screen
|
||||
if (runner.getStep<InstallStep>().state == StepState.Skipped) {
|
||||
mutableState.value = PatchingScreenState.CloseScreen
|
||||
|
||||
@@ -215,6 +213,7 @@ class PatchingScreenModel(
|
||||
R.string.fun_fact_10,
|
||||
R.string.fun_fact_11,
|
||||
R.string.fun_fact_12,
|
||||
R.string.fun_fact_13,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+41
-14
@@ -8,37 +8,64 @@ enum class KnownPatch(
|
||||
@StringRes val titleRes: Int,
|
||||
@StringRes val descRes: Int,
|
||||
val requires: List<KnownPatch> = emptyList(),
|
||||
val disables: List<KnownPatch> = emptyList(),
|
||||
) {
|
||||
// Dependency-first order (later refs need backward resolution)
|
||||
LyricsDisableCover(
|
||||
fileNames = listOf("lyrics-disable-cover.patch"),
|
||||
titleRes = R.string.patch_lyrics_disable_cover_title,
|
||||
descRes = R.string.patch_lyrics_disable_cover_desc,
|
||||
),
|
||||
LyricsProgressPill(
|
||||
LyricsReplaceLyricsButton(
|
||||
fileNames = listOf(
|
||||
"lyrics-progress-pill.patch",
|
||||
"lyrics-fade-region.patch",
|
||||
"lyrics-active-line-only.patch",
|
||||
),
|
||||
titleRes = R.string.patch_lyrics_progress_pill_title,
|
||||
descRes = R.string.patch_lyrics_progress_pill_desc,
|
||||
requires = listOf(LyricsDisableCover),
|
||||
),
|
||||
LyricsReplaceLyricButton(
|
||||
fileNames = listOf(
|
||||
"lyrics-replace-lyric-button-1-remove.patch",
|
||||
"lyrics-replace-lyric-button-2-sparkle.patch",
|
||||
"lyrics-replace-lyrics-button.patch",
|
||||
"lyrics-sparkle-conditional-visibility.patch",
|
||||
),
|
||||
titleRes = R.string.patch_lyrics_replace_button_title,
|
||||
descRes = R.string.patch_lyrics_replace_button_desc,
|
||||
),
|
||||
LyricsReplaceShareButton(
|
||||
fileNames = listOf("lyrics-replace-share-button.patch"),
|
||||
titleRes = R.string.patch_lyrics_replace_share_button_title,
|
||||
descRes = R.string.patch_lyrics_replace_share_button_desc,
|
||||
),
|
||||
PlayerBackdrop(
|
||||
fileNames = listOf("player-backdrop.patch"),
|
||||
titleRes = R.string.patch_player_backdrop_title,
|
||||
descRes = R.string.patch_player_backdrop_desc,
|
||||
),
|
||||
DebugMenuUnlock(
|
||||
fileNames = listOf("debug-menu-unlock.patch"),
|
||||
titleRes = R.string.patch_debug_menu_unlock_title,
|
||||
descRes = R.string.patch_debug_menu_unlock_desc,
|
||||
),
|
||||
LyricsProgressPill(
|
||||
fileNames = listOf(
|
||||
"lyrics-progress-pill.patch",
|
||||
"lyrics-fade-region.patch",
|
||||
),
|
||||
titleRes = R.string.patch_lyrics_progress_pill_title,
|
||||
descRes = R.string.patch_lyrics_progress_pill_desc,
|
||||
requires = listOf(LyricsDisableCover, LyricsReplaceShareButton),
|
||||
),
|
||||
EnableLegacyUi(
|
||||
fileNames = listOf("enable-legacy-ui.patch"),
|
||||
titleRes = R.string.patch_enable_legacy_ui_title,
|
||||
descRes = R.string.patch_enable_legacy_ui_desc,
|
||||
requires = listOf(DebugMenuUnlock),
|
||||
disables = listOf(
|
||||
LyricsDisableCover,
|
||||
LyricsReplaceLyricsButton,
|
||||
LyricsReplaceShareButton,
|
||||
PlayerBackdrop,
|
||||
LyricsProgressPill,
|
||||
),
|
||||
);
|
||||
|
||||
companion object {
|
||||
val All: List<KnownPatch> = entries.sortedBy { it.fileNames.first() }
|
||||
// Alphabetical by first filename, but pin DebugMenuUnlock to the bottom
|
||||
val All: List<KnownPatch> = entries.sortedWith(
|
||||
compareBy({ it == DebugMenuUnlock }, { it.fileNames.first() })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+17
-29
@@ -18,7 +18,6 @@ class PatchOptionsModel(
|
||||
private val context: Context,
|
||||
private val prefs: PreferencesManager,
|
||||
) : ScreenModel {
|
||||
// ---------- Package name state ----------
|
||||
var packageName by mutableStateOf(prefilledOptions.packageName)
|
||||
private set
|
||||
|
||||
@@ -30,7 +29,6 @@ class PatchOptionsModel(
|
||||
fetchPkgNameStateDebounced()
|
||||
}
|
||||
|
||||
// ---------- App name state ----------
|
||||
var appName by mutableStateOf(prefilledOptions.appName)
|
||||
private set
|
||||
|
||||
@@ -42,7 +40,6 @@ class PatchOptionsModel(
|
||||
appNameIsError = newAppName.length !in (1..150)
|
||||
}
|
||||
|
||||
// ---------- Debuggable state ----------
|
||||
var debuggable by mutableStateOf(prefilledOptions.debuggable)
|
||||
private set
|
||||
|
||||
@@ -50,7 +47,6 @@ class PatchOptionsModel(
|
||||
debuggable = value
|
||||
}
|
||||
|
||||
// ---------- Patch selection state ----------
|
||||
var disabledPatches by mutableStateOf(prefilledOptions.disabledPatches)
|
||||
private set
|
||||
|
||||
@@ -58,38 +54,33 @@ class PatchOptionsModel(
|
||||
patch.fileNames.none { it in disabledPatches }
|
||||
|
||||
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
|
||||
val units = if (enabled) {
|
||||
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
|
||||
buildSet {
|
||||
fun addWithDeps(p: KnownPatch) {
|
||||
if (add(p)) p.requires.forEach(::addWithDeps)
|
||||
}
|
||||
addWithDeps(patch)
|
||||
fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) }
|
||||
walk(seed)
|
||||
}
|
||||
|
||||
val enableUnits: Set<KnownPatch>
|
||||
val disableUnits: Set<KnownPatch>
|
||||
if (enabled) {
|
||||
enableUnits = closure(patch) { it.requires }
|
||||
disableUnits = enableUnits.flatMap { it.disables }
|
||||
.flatMapTo(mutableSetOf()) { d ->
|
||||
closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } }
|
||||
}
|
||||
} else {
|
||||
buildSet {
|
||||
fun addWithDependents(p: KnownPatch) {
|
||||
if (add(p)) {
|
||||
KnownPatch.All
|
||||
.filter { p in it.requires }
|
||||
.forEach(::addWithDependents)
|
||||
}
|
||||
}
|
||||
addWithDependents(patch)
|
||||
}
|
||||
enableUnits = emptySet()
|
||||
disableUnits = closure(patch) { p -> KnownPatch.All.filter { p in it.requires } }
|
||||
}
|
||||
|
||||
val affectedFiles = units.flatMap { it.fileNames }.toSet()
|
||||
disabledPatches = if (enabled) {
|
||||
disabledPatches - affectedFiles
|
||||
} else {
|
||||
disabledPatches + affectedFiles
|
||||
}
|
||||
val enableFiles = enableUnits.flatMap { it.fileNames }.toSet()
|
||||
val disableFiles = disableUnits.flatMap { it.fileNames }.toSet()
|
||||
disabledPatches = (disabledPatches - enableFiles) + disableFiles
|
||||
}
|
||||
|
||||
val enabledPatchCount: Int
|
||||
get() = KnownPatch.All.count { isPatchEnabled(it) }
|
||||
|
||||
// ---------- Custom components state ----------
|
||||
var customInjector by mutableStateOf<PatchComponent?>(null)
|
||||
private set
|
||||
var customPatches by mutableStateOf<PatchComponent?>(null)
|
||||
@@ -113,7 +104,6 @@ class PatchOptionsModel(
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Config generation ----------
|
||||
val isConfigValid by derivedStateOf {
|
||||
val invalidChecks = arrayOf(
|
||||
packageNameState == PackageNameState.Invalid,
|
||||
@@ -136,11 +126,9 @@ class PatchOptionsModel(
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Other ----------
|
||||
val isDevMode: Boolean
|
||||
get() = prefs.devMode
|
||||
|
||||
// A throttled variant of fetchPkgNameState()
|
||||
private val fetchPkgNameStateDebounced: () -> Unit =
|
||||
screenModelScope.debounce(100L, function = ::fetchPkgNameState)
|
||||
|
||||
|
||||
+1
@@ -1,5 +1,6 @@
|
||||
package com.meowarex.rlmobile.ui.screens.patchopts.components
|
||||
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ fun InstallersDialog(
|
||||
}
|
||||
|
||||
InstallerSetting.Intent -> {
|
||||
// We don't know whether this device supports this method until we try.
|
||||
// don't know whether this device supports this method
|
||||
}
|
||||
|
||||
InstallerSetting.Shizuku -> {
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ class UpdaterViewModel(
|
||||
// Fetch releases from GitHub (60s local cache)
|
||||
val releases = github.getManagerReleases().getOrThrow()
|
||||
|
||||
// Find the latest release — version is parsed from the release title (e.g. "v1.0.5")
|
||||
// Find the latest release by parsed version
|
||||
val (version, release, apkUrl) = releases
|
||||
.mapNotNull { release ->
|
||||
val version = SemVer.parseOrNull(release.name?.removePrefix("v") ?: "")
|
||||
|
||||
Reference in New Issue
Block a user