From 523f0a342b0552e1af456b54ef47a042398c7ff9 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Thu, 2 Jul 2026 15:49:18 +0200 Subject: [PATCH 1/5] feat(patch): waze integration patch --- .../patcher/steps/patch/PatchManifestStep.kt | 5 + .../rlmobile/patcher/util/ManifestPatcher.kt | 97 ++++++++ .../ui/screens/patchopts/KnownPatch.kt | 11 + Manager/app/src/main/res/values/strings.xml | 2 + patches/data.json | 2 +- .../extension/radiant/WazeInitReceiver.smali | 87 +++++++ .../radiant/WazeServiceConnection.smali | 222 ++++++++++++++++++ patches/manifest.json | 9 + patches/waze-media-browser.patch | 27 +++ 9 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 patches/extension/radiant/WazeInitReceiver.smali create mode 100644 patches/extension/radiant/WazeServiceConnection.smali create mode 100644 patches/waze-media-browser.patch diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/PatchManifestStep.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/PatchManifestStep.kt index 37d2334..e360f2d 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/PatchManifestStep.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/PatchManifestStep.kt @@ -6,6 +6,7 @@ import com.meowarex.rlmobile.patcher.steps.StepGroup import com.meowarex.rlmobile.patcher.steps.base.Step import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep import com.meowarex.rlmobile.patcher.util.ManifestPatcher +import com.meowarex.rlmobile.ui.screens.patchopts.KnownPatch import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions import com.github.diamondminer88.zip.ZipReader import com.github.diamondminer88.zip.ZipWriter @@ -26,11 +27,15 @@ class PatchManifestStep(private val options: PatchOptions) : Step() { ?: throw IllegalArgumentException("No manifest found in APK") container.log("Patching manifest") + val enableWazeIntegration = + options.patchStates[KnownPatch.WazeIntegration.name] ?: KnownPatch.WazeIntegration.default.isEnabled + val patchedManifest = ManifestPatcher.patchManifest( manifestBytes = manifest, packageName = options.packageName, appName = options.appName, debuggable = options.debuggable, + enableWazeIntegration = enableWazeIntegration, ) container.log("Repacking apk with patched manifest") diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt index bc53f2f..81a625e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/util/ManifestPatcher.kt @@ -18,12 +18,16 @@ object ManifestPatcher { private const val IS_SPLIT_REQUIRED = "isSplitRequired" private const val REQUIRED_SPLIT_TYPES = "requiredSplitTypes" private const val SPLIT_TYPES = "splitTypes" + private const val WAZE_PACKAGE = "com.waze" + private const val WAZE_INIT_RECEIVER = "radiant.WazeInitReceiver" + private const val WAZE_ACTION_INIT = "com.waze.sdk.audio.ACTION_INIT" fun patchManifest( manifestBytes: ByteArray, packageName: String, appName: String, debuggable: Boolean, + enableWazeIntegration: Boolean, ): ByteArray { // Extract original package name to rewrite every reference to it // (permissions, provider authorities) to the new packageName. @@ -84,6 +88,49 @@ object ManifestPatcher { } return when (name) { + "queries" -> object : NodeVisitor(nv) { + private var hasWazePackage = false + + override fun child(ns: String?, name: String): NodeVisitor { + val visitor = super.child(ns, name) + + return if (enableWazeIntegration && name == "package") { + object : NodeVisitor(visitor) { + override fun attr( + ns: String?, + name: String?, + resourceId: Int, + type: Int, + value: Any?, + ) { + if (name == "name" && value == WAZE_PACKAGE) { + hasWazePackage = true + } + super.attr(ns, name, resourceId, type, value) + } + } + } else { + visitor + } + } + + override fun end() { + if (enableWazeIntegration && !hasWazePackage) { + super.child(null, "package").apply { + attr( + ANDROID_NAMESPACE, + "name", + android.R.attr.name, + TYPE_STRING, + WAZE_PACKAGE, + ) + end() + } + } + super.end() + } + } + "uses-permission" -> object : NodeVisitor(nv) { override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, value: Any?) { if (name != "maxSdkVersion") { @@ -132,6 +179,7 @@ object ManifestPatcher { private var addUseEmbeddedDex = true private var addExtractNativeLibs = true private var addMetadata = true + private var hasWazeInitReceiver = false override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage = false @@ -161,6 +209,22 @@ object ManifestPatcher { return when (name) { "activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName)) + + "receiver" -> object : NodeVisitor(visitor) { + override fun attr( + ns: String?, + name: String, + resourceId: Int, + type: Int, + value: Any? + ) { + if (name == "name" && value == WAZE_INIT_RECEIVER) { + hasWazeInitReceiver = true + } + super.attr(ns, name, resourceId, type, value) + } + } + "provider" -> object : NodeVisitor(visitor) { override fun attr( ns: String?, @@ -219,6 +283,39 @@ object ManifestPatcher { 0 ) + if (enableWazeIntegration && !hasWazeInitReceiver) { + super.child(null, "receiver").apply { + attr( + ANDROID_NAMESPACE, + "name", + android.R.attr.name, + TYPE_STRING, + WAZE_INIT_RECEIVER, + ) + attr( + ANDROID_NAMESPACE, + "exported", + android.R.attr.exported, + TYPE_INT_BOOLEAN, + 1, + ) + child(null, "intent-filter").apply { + child(null, "action").apply { + attr( + ANDROID_NAMESPACE, + "name", + android.R.attr.name, + TYPE_STRING, + WAZE_ACTION_INIT, + ) + end() + } + end() + } + end() + } + } + super.end() } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index 212fe07..3f1c27e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -130,6 +130,17 @@ enum class KnownPatch( descRes = R.string.patch_debug_menu_unlock_desc, default = Disabled, ), + WazeIntegration( + order = 90, + fileNames = listOf("waze-media-browser.patch"), + extensionFiles = listOf( + "radiant/WazeInitReceiver.smali", + "radiant/WazeServiceConnection.smali", + ), + titleRes = R.string.patch_waze_integration_title, + descRes = R.string.patch_waze_integration_desc, + default = Disabled, + ), LyricsProgressPill( order = 40, fileNames = listOf( diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index 2aaa020..a6db93e 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -286,6 +286,8 @@ Reveals TIDAL\'s hidden Debug Menu in Settings. (Unlocks Feature Flags & the legacy Debug Options) + Waze Integration + Control TIDAL playback from the Waze app Enable Legacy UI ()V + .locals 0 + + invoke-direct {p0}, Landroid/content/BroadcastReceiver;->()V + + return-void +.end method + + +# virtual methods +.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V + .locals 5 + + if-eqz p1, :return + + if-eqz p2, :return + + const-string v0, "com.waze.sdk.audio.ACTION_INIT" + + invoke-virtual {p2}, Landroid/content/Intent;->getAction()Ljava/lang/String; + + move-result-object v1 + + invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z + + move-result v0 + + if-eqz v0, :return + + const-string v0, "token" + + invoke-virtual {p2, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String; + + move-result-object v0 + + if-eqz v0, :return + + invoke-virtual {v0}, Ljava/lang/String;->length()I + + move-result v1 + + if-lez v1, :return + + invoke-virtual {p1}, Landroid/content/Context;->getApplicationContext()Landroid/content/Context; + + move-result-object v1 + + if-eqz v1, :context_ready + + move-object p1, v1 + + :context_ready + new-instance v1, Landroid/content/Intent; + + invoke-direct {v1}, Landroid/content/Intent;->()V + + const-string v2, "com.waze" + + const-string v3, "com.waze.sdk.SdkService" + + invoke-virtual {v1, v2, v3}, Landroid/content/Intent;->setClassName(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent; + + new-instance v2, Lradiant/WazeServiceConnection; + + invoke-direct {v2, p1, v0}, Lradiant/WazeServiceConnection;->(Landroid/content/Context;Ljava/lang/String;)V + + :try_start_0 + const/4 v3, 0x1 + + invoke-virtual {p1, v1, v2, v3}, Landroid/content/Context;->bindService(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z + :try_end_0 + .catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0 + + goto :return + + :catch_0 + move-exception v4 + + :return + return-void +.end method diff --git a/patches/extension/radiant/WazeServiceConnection.smali b/patches/extension/radiant/WazeServiceConnection.smali new file mode 100644 index 0000000..74ca8c1 --- /dev/null +++ b/patches/extension/radiant/WazeServiceConnection.smali @@ -0,0 +1,222 @@ +.class public final Lradiant/WazeServiceConnection; +.super Ljava/lang/Object; +.source "WazeServiceConnection.smali" + +# interfaces +.implements Landroid/content/ServiceConnection; + + +# instance fields +.field private final context:Landroid/content/Context; + +.field private final token:Ljava/lang/String; + + +# direct methods +.method public constructor (Landroid/content/Context;Ljava/lang/String;)V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + iput-object p1, p0, Lradiant/WazeServiceConnection;->context:Landroid/content/Context; + + iput-object p2, p0, Lradiant/WazeServiceConnection;->token:Ljava/lang/String; + + return-void +.end method + +.method private addOpenMeIntent(Landroid/os/Bundle;)V + .locals 4 + + iget-object v0, p0, Lradiant/WazeServiceConnection;->context:Landroid/content/Context; + + invoke-virtual {v0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager; + + move-result-object v1 + + invoke-virtual {v0}, Landroid/content/Context;->getPackageName()Ljava/lang/String; + + move-result-object v2 + + invoke-virtual {v1, v2}, Landroid/content/pm/PackageManager;->getLaunchIntentForPackage(Ljava/lang/String;)Landroid/content/Intent; + + move-result-object v1 + + if-eqz v1, :return + + const/high16 v2, 0x10000000 + + invoke-virtual {v1, v2}, Landroid/content/Intent;->addFlags(I)Landroid/content/Intent; + + const v2, 0xc000000 + + const/4 v3, 0x0 + + invoke-static {v0, v3, v1, v2}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent; + + move-result-object v1 + + const-string v2, "openMeIntent" + + invoke-virtual {p1, v2, v1}, Landroid/os/Bundle;->putParcelable(Ljava/lang/String;Landroid/os/Parcelable;)V + + :return + return-void +.end method + +.method private createClientMessenger()Landroid/os/Messenger; + .locals 3 + + new-instance v0, Landroid/os/Handler; + + invoke-static {}, Landroid/os/Looper;->getMainLooper()Landroid/os/Looper; + + move-result-object v1 + + invoke-direct {v0, v1}, Landroid/os/Handler;->(Landroid/os/Looper;)V + + new-instance v2, Landroid/os/Messenger; + + invoke-direct {v2, v0}, Landroid/os/Messenger;->(Landroid/os/Handler;)V + + return-object v2 +.end method + +.method private createConfigBundle()Landroid/os/Bundle; + .locals 3 + + new-instance v0, Landroid/os/Bundle; + + invoke-direct {v0}, Landroid/os/Bundle;->()V + + const-string v1, "versionCode" + + const/16 v2, 0xa + + invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V + + invoke-direct {p0, v0}, Lradiant/WazeServiceConnection;->addOpenMeIntent(Landroid/os/Bundle;)V + + return-object v0 +.end method + +.method private register(Landroid/os/IBinder;)V + .locals 5 + + if-eqz p1, :return + + invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel; + + move-result-object v0 + + invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel; + + move-result-object v1 + + const-string v2, "com.waze.sdk.ISdkService" + + invoke-virtual {v0, v2}, Landroid/os/Parcel;->writeInterfaceToken(Ljava/lang/String;)V + + iget-object v2, p0, Lradiant/WazeServiceConnection;->token:Ljava/lang/String; + + invoke-virtual {v0, v2}, Landroid/os/Parcel;->writeString(Ljava/lang/String;)V + + invoke-direct {p0}, Lradiant/WazeServiceConnection;->createConfigBundle()Landroid/os/Bundle; + + move-result-object v2 + + invoke-static {v0, v2}, Lradiant/WazeServiceConnection;->writeParcelable(Landroid/os/Parcel;Landroid/os/Parcelable;)V + + invoke-direct {p0}, Lradiant/WazeServiceConnection;->createClientMessenger()Landroid/os/Messenger; + + move-result-object v2 + + invoke-static {v0, v2}, Lradiant/WazeServiceConnection;->writeParcelable(Landroid/os/Parcel;Landroid/os/Parcelable;)V + + const/4 v2, 0x1 + + const/4 v3, 0x0 + + invoke-interface {p1, v2, v0, v1, v3}, Landroid/os/IBinder;->transact(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z + + move-result v4 + + if-eqz v4, :cleanup + + invoke-virtual {v1}, Landroid/os/Parcel;->readException()V + + :cleanup + invoke-virtual {v1}, Landroid/os/Parcel;->recycle()V + + invoke-virtual {v0}, Landroid/os/Parcel;->recycle()V + + :return + return-void +.end method + +.method private safeUnbind()V + .locals 1 + + :try_start_0 + iget-object v0, p0, Lradiant/WazeServiceConnection;->context:Landroid/content/Context; + + invoke-virtual {v0, p0}, Landroid/content/Context;->unbindService(Landroid/content/ServiceConnection;)V + :try_end_0 + .catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0 + + goto :return + + :catch_0 + move-exception v0 + + :return + return-void +.end method + +.method private static writeParcelable(Landroid/os/Parcel;Landroid/os/Parcelable;)V + .locals 2 + + const/4 v0, 0x0 + + if-eqz p1, :null_value + + const/4 v1, 0x1 + + invoke-virtual {p0, v1}, Landroid/os/Parcel;->writeInt(I)V + + invoke-interface {p1, p0, v0}, Landroid/os/Parcelable;->writeToParcel(Landroid/os/Parcel;I)V + + return-void + + :null_value + invoke-virtual {p0, v0}, Landroid/os/Parcel;->writeInt(I)V + + return-void +.end method + + +# virtual methods +.method public onServiceConnected(Landroid/content/ComponentName;Landroid/os/IBinder;)V + .locals 1 + + :try_start_0 + invoke-direct {p0, p2}, Lradiant/WazeServiceConnection;->register(Landroid/os/IBinder;)V + :try_end_0 + .catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0 + + goto :unbind + + :catch_0 + move-exception v0 + + :unbind + invoke-direct {p0}, Lradiant/WazeServiceConnection;->safeUnbind()V + + return-void +.end method + +.method public onServiceDisconnected(Landroid/content/ComponentName;)V + .locals 0 + + return-void +.end method diff --git a/patches/manifest.json b/patches/manifest.json index d2d880e..757ca27 100644 --- a/patches/manifest.json +++ b/patches/manifest.json @@ -212,6 +212,15 @@ "description": "Inverts the auto-hide behavior on the lyrics screen, playback controls stay visible by default and only hide when you tap them off.", "default": true }, + { + "id": "WazeIntegration", + "order": 90, + "fileNames": ["waze-media-browser.patch"], + "extensionFiles": ["radiant/WazeInitReceiver.smali", "radiant/WazeServiceConnection.smali"], + "title": "Waze Integration", + "description": "Control TIDAL playback from the Waze app", + "default": true + }, { "id": "DebugMenuUnlock", "order": 100, diff --git a/patches/waze-media-browser.patch b/patches/waze-media-browser.patch new file mode 100644 index 0000000..1f2c5a4 --- /dev/null +++ b/patches/waze-media-browser.patch @@ -0,0 +1,27 @@ +--- a/pb/c.smali ++++ b/pb/c.smali +@@ -700,0 +701,24 @@ ++ iget-object v15, v1, Lcom/aspiro/wamp/util/y$a;->b:Ljava/lang/String; ++ ++ const-string/jumbo v14, "com.waze" ++ ++ invoke-static {v15, v14}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # check caller ++ ++ move-result v12 ++ ++ if-eqz v12, :rl_waze_done ++ ++ iget-object v15, v1, Lcom/aspiro/wamp/util/y$a;->d:Ljava/lang/String; # caller cert ++ ++ const-string v14, "03:63:7f:6c:5d:8f:60:4e:6f:db:79:a6:ff:bf:a5:78:de:4e:31:8f:8d:a2:2f:c6:10:66:65:24:7f:88:07:d7" # Waze cert ++ ++ invoke-static {v15, v14}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # check cert ++ ++ move-result v12 # cert match ++ ++ if-eqz v12, :rl_waze_done # reject mismatch ++ ++ goto :goto_9 # trust Waze ++ ++ :rl_waze_done ++ From 75f81875265fb0999db6059508de3a5d8c3c4723 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Thu, 2 Jul 2026 20:26:12 +0200 Subject: [PATCH 2/5] feat(waze patch): waze media menu browse source options --- .../ui/screens/patchopts/KnownPatch.kt | 16 ++++++ .../ui/screens/patchopts/PatchOption.kt | 2 +- .../ui/screens/patchopts/PatchSpec.kt | 2 +- .../components/PatchAdvancedOptionsSheet.kt | 33 +++++++++++ Manager/app/src/main/res/values/strings.xml | 4 ++ patches/manifest.json | 17 +++++- patches/waze-media-browser.patch | 56 +++++++++++++++---- 7 files changed, 115 insertions(+), 15 deletions(-) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index 3f1c27e..fce43e7 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -140,6 +140,22 @@ enum class KnownPatch( titleRes = R.string.patch_waze_integration_title, descRes = R.string.patch_waze_integration_desc, default = Disabled, + advancedOptions = listOf( + PatchOption.Choice( + key = "browse_root", + titleRes = R.string.patch_waze_browse_root_title, + entries = listOf( + ChoiceEntry(R.string.patch_waze_root_auto, value = "ROOT_AUTO"), + ChoiceEntry(R.string.patch_waze_root_home, value = "HOME_V2::home_page_v2_id"), + ChoiceEntry( + R.string.patch_waze_root_recents, + value = "RECENTLY_PLAYED::home/pages/CONTINUE_LISTEN_TO/view-all", + ), + ), + defaultIndex = 0, + token = "RL_WAZE_ROOT_ID", + ), + ), ), LyricsProgressPill( order = 40, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt index 4ce2e8e..78d895b 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt @@ -59,7 +59,7 @@ sealed interface PatchOption { data class Choice( override val key: String, @StringRes override val titleRes: Int, - @StringRes override val descRes: Int, + @StringRes override val descRes: Int = 0, val entries: List, val defaultIndex: Int = 0, val requiresOption: String? = null, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt index 22aa3d7..c0bca4c 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt @@ -187,7 +187,7 @@ private fun PatchOption.toSpec(resolve: (Int) -> String): OptionSpec = when (thi is PatchOption.Choice -> OptionSpec.Choice( key = key, title = resolve(titleRes), - description = resolve(descRes), + description = if (descRes != 0) resolve(descRes) else "", entries = entries.map { resolve(it.labelRes) }, defaultIndex = defaultIndex, values = entries.map { it.value ?: "" }, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt index 6c1b8cf..f21d4d5 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt @@ -378,6 +378,39 @@ private fun ChoiceOptionRow( textAlign = TextAlign.Center, ) } + if (entries.size > 3) { + var expanded by rememberSaveable { mutableStateOf(false) } + val selectedLabel = entries.getOrNull(selectedIndex).orEmpty() + + Box(modifier = Modifier.fillMaxWidth()) { + OutlinedButton( + onClick = { expanded = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = selectedLabel, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth(), + ) { + entries.forEachIndexed { index, entry -> + DropdownMenuItem( + text = { Text(entry) }, + onClick = { + onSelect(index) + expanded = false + }, + ) + } + } + } + return@Column + } SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { entries.forEachIndexed { index, entry -> SegmentedButton( diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index a6db93e..3241b18 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -288,6 +288,10 @@ >Reveals TIDAL\'s hidden Debug Menu in Settings. (Unlocks Feature Flags & the legacy Debug Options) Waze Integration Control TIDAL playback from the Waze app + Media Menu + Auto Menu + Home + Recents Enable Legacy UI b:Ljava/lang/String; +@@ -801,6 +801,26 @@ +- + -+ const-string/jumbo v14, "com.waze" + .line 236 + :goto_8 ++ const-string/jumbo v5, "com.waze" + -+ invoke-static {v15, v14}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # check caller ++ invoke-static {v3, v5}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # package match + -+ move-result v12 ++ move-result v5 + -+ if-eqz v12, :rl_waze_done ++ if-eqz v5, :rl_waze_done + -+ iget-object v15, v1, Lcom/aspiro/wamp/util/y$a;->d:Ljava/lang/String; # caller cert ++ const-string/jumbo v5, "03:63:7f:6c:5d:8f:60:4e:6f:db:79:a6:ff:bf:a5:78:de:4e:31:8f:8d:a2:2f:c6:10:66:65:24:7f:88:07:d7" # Waze cert + -+ const-string v14, "03:63:7f:6c:5d:8f:60:4e:6f:db:79:a6:ff:bf:a5:78:de:4e:31:8f:8d:a2:2f:c6:10:66:65:24:7f:88:07:d7" # Waze cert ++ invoke-static {v2, v5}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # cert match + -+ invoke-static {v15, v14}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # check cert ++ move-result v5 + -+ move-result v12 # cert match -+ -+ if-eqz v12, :rl_waze_done # reject mismatch ++ if-eqz v5, :rl_waze_done + + goto :goto_9 # trust Waze + + :rl_waze_done + + invoke-static {}, Landroid/os/Process;->myUid()I +- ++ + .line 237 +@@ -957,6 +977,26 @@ + :goto_b + if-eqz v2, :cond_14 +- ++ ++ const-string/jumbo v1, "com.waze" ++ ++ invoke-static {v3, v1}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # package match ++ ++ move-result v1 ++ ++ if-eqz v1, :rl_waze_recent_root_done_2 ++ ++ new-instance v1, Landroidx/media/MediaBrowserServiceCompat$BrowserRoot; ++ ++ const-string/jumbo v4, "__RL_WAZE_ROOT_ID__" ++ ++ const/4 v5, 0x0 ++ ++ invoke-direct {v1, v4, v5}, Landroidx/media/MediaBrowserServiceCompat$BrowserRoot;->(Ljava/lang/String;Landroid/os/Bundle;)V ++ ++ return-object v1 ++ ++ :rl_waze_recent_root_done_2 ++ + .line 306 + .line 307 + iget-object v1, v0, Lpb/c;->c:Ljava/util/Map; From 03271bf26d4949ec999d8fbca74a685405cdbf5f Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Thu, 2 Jul 2026 20:44:24 +0200 Subject: [PATCH 3/5] feat(waze patch): Tidal cyan accent color for waze --- patches/extension/radiant/WazeServiceConnection.smali | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/patches/extension/radiant/WazeServiceConnection.smali b/patches/extension/radiant/WazeServiceConnection.smali index 74ca8c1..e3fb618 100644 --- a/patches/extension/radiant/WazeServiceConnection.smali +++ b/patches/extension/radiant/WazeServiceConnection.smali @@ -95,6 +95,12 @@ invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V + const-string v1, "themeColor" + + const v2, -0xde0114 # TIDAL cyan #ff21feec + + invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V + invoke-direct {p0, v0}, Lradiant/WazeServiceConnection;->addOpenMeIntent(Landroid/os/Bundle;)V return-object v0 From 50b99b6193d80730f2594b66bb97dae98b769e77 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Thu, 2 Jul 2026 22:56:09 +0200 Subject: [PATCH 4/5] feat(waze patch): customizable themeColor --- .../patcher/steps/patch/SmaliPatchStep.kt | 14 +- .../ui/screens/patchopts/KnownPatch.kt | 6 + .../ui/screens/patchopts/PatchOption.kt | 8 + .../ui/screens/patchopts/PatchOptions.kt | 7 + .../ui/screens/patchopts/PatchOptionsModel.kt | 10 + .../ui/screens/patchopts/PatchSpec.kt | 24 +++ .../components/PatchAdvancedOptionsSheet.kt | 176 ++++++++++++++++++ Manager/app/src/main/res/values/strings.xml | 7 + .../radiant/WazeServiceConnection.smali | 2 +- patches/manifest.json | 7 + 10 files changed, 259 insertions(+), 2 deletions(-) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt index 8e4361e..e97e626 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt @@ -95,7 +95,19 @@ class SmaliPatchStep( val entry = zip.openEntry(patchFile) ?: throw FileNotFoundException("Missing zip entry: $patchFile") out.canonicalFile.parentFile?.mkdirs() - out.writeBytes(entry.read()) + var smaliText = entry.read() + .decodeToString() + .replace("\r\n", "\n") + for ((token, value) in substitutions) { + if (smaliText.contains(token)) { + smaliText = smaliText.replace(token, value) + container.log("Applied substitution $token -> $value in $patchFile") + } + } + UNRESOLVED_TOKEN.find(smaliText)?.let { match -> + throw Error("Unresolved option placeholder ${match.value} in $patchFile") + } + out.writeText(smaliText) container.log("Extracted extension smali: $relative") continue } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index fce43e7..5bb3193 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -155,6 +155,12 @@ enum class KnownPatch( defaultIndex = 0, token = "RL_WAZE_ROOT_ID", ), + PatchOption.Color( + key = "accent_color", + titleRes = R.string.patch_waze_accent_color_title, + default = -16747037, // Waze default #ff0075e3 + token = "RL_WAZE_THEME_COLOR", + ), ), ), LyricsProgressPill( diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt index 78d895b..9fcabf2 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt @@ -65,6 +65,14 @@ sealed interface PatchOption { val requiresOption: String? = null, val token: String? = null, ) : PatchOption + + data class Color( + override val key: String, + @StringRes override val titleRes: Int, + @StringRes override val descRes: Int = 0, + val default: Int, + val token: String? = null, + ) : PatchOption } data class ChoiceEntry( diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt index 96a1292..243a9d0 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt @@ -56,6 +56,9 @@ data class PatchOptions( (optionInts["${spec.id}/${option.key}"] ?: option.defaultIndex) .coerceIn(0, option.entries.lastIndex.coerceAtLeast(0)) + fun colorValue(spec: PatchSpec, option: OptionSpec.Color): Int = + optionInts["${spec.id}/${option.key}"] ?: option.default + fun isToggleOn(spec: PatchSpec, option: OptionSpec.Toggle): Boolean = optionBools["${spec.id}/${option.key}"] ?: option.default @@ -135,6 +138,10 @@ data class PatchOptions( val index = if (gatedOff) 0 else choiceIndex(spec, option) put("__${token}__", option.values.getOrNull(index) ?: continue) } + is OptionSpec.Color -> { + val token = option.token ?: continue + put("__${token}__", colorValue(spec, option).toString()) + } } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt index 3f6462e..8065d4e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt @@ -153,11 +153,19 @@ class PatchOptionsModel( optionInts = optionInts + (keyOf(spec, option) to index) } + fun colorValue(spec: PatchSpec, option: OptionSpec.Color): Int = + optionInts[keyOf(spec, option)] ?: option.default + + fun setColorValue(spec: PatchSpec, option: OptionSpec.Color, value: Int) { + optionInts = optionInts + (keyOf(spec, option) to value) + } + fun isAdvancedModified(spec: PatchSpec): Boolean = spec.advancedOptions.any { option -> when (option) { is OptionSpec.Toggle -> toggleValue(spec, option) != option.default is OptionSpec.Slider -> sliderValue(spec, option) != option.default is OptionSpec.Choice -> choiceValue(spec, option) != option.defaultIndex + is OptionSpec.Color -> colorValue(spec, option) != option.default } } @@ -175,6 +183,8 @@ class PatchOptionsModel( setSlider = ::setSliderValue, choice = ::choiceValue, setChoice = ::setChoiceValue, + color = ::colorValue, + setColor = ::setColorValue, isModified = ::isAdvancedModified, reset = ::resetAdvanced, ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt index c0bca4c..12bb65e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt @@ -106,6 +106,18 @@ sealed interface OptionSpec { val requiresOption: String? = null, val token: String? = null, ) : OptionSpec + + @Immutable + @Serializable + @SerialName("color") + data class Color( + override val key: String, + override val title: String = "", + override val description: String = "", + val default: Int = 0, + /** Placeholder name (without the surrounding `__`) baked into the `.patch`/extension files. */ + val token: String? = null, + ) : OptionSpec } @Serializable @@ -194,6 +206,14 @@ private fun PatchOption.toSpec(resolve: (Int) -> String): OptionSpec = when (thi requiresOption = requiresOption, token = token, ) + + is PatchOption.Color -> OptionSpec.Color( + key = key, + title = resolve(titleRes), + description = if (descRes != 0) resolve(descRes) else "", + default = default, + token = token, + ) } @Immutable @@ -258,6 +278,8 @@ class PatchOptionState( val setSlider: (PatchSpec, OptionSpec.Slider, Float) -> Unit, val choice: (PatchSpec, OptionSpec.Choice) -> Int, val setChoice: (PatchSpec, OptionSpec.Choice, Int) -> Unit, + val color: (PatchSpec, OptionSpec.Color) -> Int, + val setColor: (PatchSpec, OptionSpec.Color, Int) -> Unit, val isModified: (PatchSpec) -> Boolean, val reset: (PatchSpec) -> Unit, ) { @@ -270,6 +292,8 @@ class PatchOptionState( setSlider = { _, _, _ -> }, choice = { _, option -> option.defaultIndex }, setChoice = { _, _, _ -> }, + color = { _, option -> option.default }, + setColor = { _, _, _ -> }, isModified = { false }, reset = {}, ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt index f21d4d5..20e1ee7 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* @@ -15,11 +17,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -196,6 +202,12 @@ fun PatchAdvancedOptionsSheet( onSelect = { state.setChoice(patch, option, it) }, ) } + + is OptionSpec.Color -> ColorOptionRow( + title = option.title, + color = state.color(patch, option), + onColorChange = { state.setColor(patch, option, it) }, + ) } } @@ -433,6 +445,150 @@ private fun ChoiceOptionRow( } } +@Composable +private fun ColorOptionRow( + title: String, + color: Int, + onColorChange: (Int) -> Unit, +) { + var showPicker by rememberSaveable { mutableStateOf(false) } + val selectedColor = color.withOpaqueAlpha() + val materialYouColor = MaterialTheme.colorScheme.primary.toArgb().withOpaqueAlpha() + val presets = listOf( + stringResource(R.string.patch_waze_color_waze_default), + stringResource(R.string.patch_waze_color_tidal_cyan), + stringResource(R.string.patch_waze_color_material_you), + ) + val presetColors = listOf(WAZE_DEFAULT, TIDAL_CYAN, materialYouColor) + val selectedPreset = presetColors.indexOf(selectedColor) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .clickable { showPicker = true } + .padding(vertical = 4.dp), + ) { + ColorSwatch(color = selectedColor) + Text( + text = selectedColor.toRgbHex(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + } + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + presets.forEachIndexed { index, label -> + SegmentedButton( + selected = index == selectedPreset, + onClick = { onColorChange(presetColors[index]) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = presets.size), + icon = {}, + label = { + Text( + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelMedium, + ) + }, + ) + } + } + } + + if (showPicker) { + ColorPickerDialog( + initialColor = selectedColor, + onDismiss = { showPicker = false }, + onColorSelected = { + onColorChange(it) + showPicker = false + }, + ) + } +} + +@Composable +private fun ColorSwatch(color: Int, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(32.dp) + .clip(CircleShape) + .background(Color(color)) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape), + ) +} + +@Composable +private fun ColorPickerDialog( + initialColor: Int, + onDismiss: () -> Unit, + onColorSelected: (Int) -> Unit, +) { + var color by rememberSaveable(initialColor) { mutableIntStateOf(initialColor.withOpaqueAlpha()) } + var hex by rememberSaveable(initialColor) { mutableStateOf(color.toRgbHex()) } + val parsedHex = parseRgbHex(hex) + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.patch_color_picker_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + ColorSwatch(color = parsedHex ?: color, modifier = Modifier.size(40.dp)) + OutlinedTextField( + value = hex, + onValueChange = { value -> + hex = value + parseRgbHex(value)?.let { color = it } + }, + label = { Text(stringResource(R.string.patch_color_picker_hex)) }, + singleLine = true, + isError = parsedHex == null, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Characters, + keyboardType = KeyboardType.Ascii, + ), + supportingText = if (parsedHex == null) { + { Text(stringResource(R.string.patch_color_picker_hex_error)) } + } else null, + modifier = Modifier.weight(1f), + ) + } + } + }, + confirmButton = { + TextButton( + enabled = parsedHex != null, + onClick = { parsedHex?.let(onColorSelected) }, + ) { + Text(stringResource(R.string.action_done)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_cancel)) + } + }, + ) +} + @Composable private fun OptionText( title: String, @@ -465,6 +621,26 @@ private fun formatSliderValue(option: OptionSpec.Slider, value: Float): String { } } +private const val OPAQUE_ALPHA = -0x1000000 +private const val WAZE_DEFAULT = -16747037 // #ff0075e3 +private const val TIDAL_CYAN = -14549268 // #ff21feec + +private fun Int.withOpaqueAlpha(): Int = (this and 0x00FFFFFF) or OPAQUE_ALPHA + +private fun Int.toRgbHex(): String = "#" + + (this and 0x00FFFFFF).toString(16).uppercase().padStart(6, '0') + +private fun parseRgbHex(value: String): Int? { + val clean = value.trim() + .removePrefix("#") + .removePrefix("0x") + .removePrefix("0X") + if (clean.length != 6 && clean.length != 8) return null + val parsed = clean.toLongOrNull(16) ?: return null + val rgb = if (clean.length == 8) parsed and 0x00FFFFFFL else parsed + return (0xFF000000L or rgb).toInt() +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun OptionLockDialog(lock: OptionLock, onDismiss: () -> Unit) { diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index 3241b18..f1aa74a 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -289,9 +289,16 @@ Waze Integration Control TIDAL playback from the Waze app Media Menu + Accent Color + Waze Default + TIDAL Cyan + Material You Auto Menu Home Recents + Custom color + Hex color + Use #RRGGBB or #AARRGGBB. Enable Legacy UI putInt(Ljava/lang/String;I)V diff --git a/patches/manifest.json b/patches/manifest.json index 4b81738..23bfda2 100644 --- a/patches/manifest.json +++ b/patches/manifest.json @@ -233,6 +233,13 @@ ], "defaultIndex": 0, "token": "RL_WAZE_ROOT_ID" + }, + { + "type": "color", + "key": "accent_color", + "title": "Accent Color", + "default": -16747037, + "token": "RL_WAZE_THEME_COLOR" } ] }, From 08aee155c9283d972caf4ee38b857ecb7b1ee3f9 Mon Sep 17 00:00:00 2001 From: meowarex Date: Fri, 3 Jul 2026 20:19:16 +1000 Subject: [PATCH 5/5] Integrations UI + Package Name Warnings/Gate <3 --- .../screens/PatchOptionsScreenPreview.kt | 4 +- .../ui/screens/patchopts/KnownPatch.kt | 4 + .../ui/screens/patchopts/PatchOptionsModel.kt | 32 +++++-- .../screens/patchopts/PatchOptionsScreen.kt | 59 +++++++++++-- .../ui/screens/patchopts/PatchSpec.kt | 13 ++- .../components/PackageNameUnlockDialog.kt | 88 +++++++++++++++++++ .../components/PatchSelectionAccordion.kt | 22 ++++- .../components/options/TextPatchOption.kt | 39 +++++++- .../app/src/main/res/drawable/ic_healing.xml | 9 ++ Manager/app/src/main/res/values/strings.xml | 9 ++ patches/manifest.json | 2 + 11 files changed, 261 insertions(+), 20 deletions(-) create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt create mode 100644 Manager/app/src/main/res/drawable/ic_healing.xml diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt index d0c8379..49a95cb 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt @@ -35,12 +35,14 @@ private fun PatchOptionsScreenPreview( packageName = parameters.packageName, packageNameState = parameters.packageNameState, setPackageName = {}, + packageNameLocked = parameters.packageName == PatchOptions.Default.packageName, + onUnlockPackageName = {}, + onLockPackageName = {}, customTidalApk = parameters.customTidalApk, onSelectCustomTidalApk = {}, customPatches = parameters.customPatches, onSelectCustomPatches = {}, specs = specs, - enabledPatchCount = specs.size, isPatchEnabled = { true }, onTogglePatch = { _, _ -> }, patchLockState = { PatchLock.Free }, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index 5bb3193..bcdf2e1 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -24,6 +24,8 @@ enum class KnownPatch( val variants: List = emptyList(), val defaultVariantIndex: Int = 0, val advancedOptions: List = emptyList(), + val category: String = PatchSpec.CATEGORY_PATCH, + val pathLocked: Boolean = false, ) { LyricsDisableCover( order = 41, @@ -140,6 +142,8 @@ enum class KnownPatch( titleRes = R.string.patch_waze_integration_title, descRes = R.string.patch_waze_integration_desc, default = Disabled, + category = PatchSpec.CATEGORY_INTEGRATION, + pathLocked = true, advancedOptions = listOf( PatchOption.Choice( key = "browse_root", diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt index 8065d4e..b545efc 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt @@ -43,6 +43,24 @@ class PatchOptionsModel( fetchPkgNameStateDebounced() } + /** Patches flagged [PatchSpec.pathLocked] only work under the stock package name. */ + fun isBlockedByPackageName(spec: PatchSpec): Boolean = + spec.pathLocked && packageName != PatchOptions.Default.packageName + + /** The package-name field starts locked & editing requires confirming the unlock dialog */ + var packageNameLocked by mutableStateOf(prefilledOptions.packageName == PatchOptions.Default.packageName) + private set + + fun unlockPackageName() { + packageNameLocked = false + } + + /** Re-locking snaps the package name back to the stock default */ + fun lockPackageName() { + packageNameLocked = true + changePackageName(PatchOptions.Default.packageName) + } + var appName by mutableStateOf(prefilledOptions.appName) private set @@ -83,7 +101,7 @@ class PatchOptionsModel( .coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0)) fun isPatchEnabled(spec: PatchSpec): Boolean = - patchStates[spec.id] ?: spec.defaultEnabled + !isBlockedByPackageName(spec) && (patchStates[spec.id] ?: spec.defaultEnabled) fun setPatchEnabled(spec: PatchSpec, enabled: Boolean) { val byId = specs.associateBy { it.id } @@ -190,6 +208,7 @@ class PatchOptionsModel( ) fun lockState(spec: PatchSpec): PatchLock { + if (isBlockedByPackageName(spec)) return PatchLock.RequiresDefaultPackage if (spec.variants.isNotEmpty()) return PatchLock.Free val byId = specs.associateBy { it.id } @@ -216,9 +235,6 @@ class PatchOptionsModel( return PatchLock.Free } - val enabledPatchCount: Int - get() = specs.count { isPatchEnabled(it) } - var customTidalApk by mutableStateOf(null) private set var customPatches by mutableStateOf(null) @@ -333,7 +349,10 @@ class PatchOptionsModel( debuggable = debuggable, customTidalApk = customTidalApk, customPatches = customPatches, - patchStates = patchStates, + // Hard-disable package-name-bound patches so the patcher can't apply + patchStates = patchStates.toMutableMap().apply { + specs.filter { isBlockedByPackageName(it) }.forEach { this[it.id] = false } + }, selectedVariants = selectedVariants, optionFloats = optionFloats, optionBools = optionBools, @@ -395,4 +414,7 @@ sealed class PatchLock { data object Free : PatchLock() data class LockedOn(val by: PatchSpec) : PatchLock() data class LockedOff(val by: PatchSpec) : PatchLock() + + /** Locked off because the install uses a non-default package name */ + data object RequiresDefaultPackage : PatchLock() } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt index 91c204c..628d450 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt @@ -19,7 +19,9 @@ import com.meowarex.rlmobile.R import com.meowarex.rlmobile.ui.components.* import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen +import androidx.compose.runtime.saveable.rememberSaveable import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameStateLabel +import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameUnlockDialog import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchOptionsAppBar import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchSelectionAccordion import com.meowarex.rlmobile.ui.screens.patchopts.components.options.* @@ -55,13 +57,16 @@ class PatchOptionsScreen( packageNameState = model.packageNameState, setPackageName = model::changePackageName, + packageNameLocked = model.packageNameLocked, + onUnlockPackageName = model::unlockPackageName, + onLockPackageName = model::lockPackageName, + customTidalApk = model.customTidalApk, customPatches = model.customPatches, onSelectCustomTidalApk = { model.selectCustomTidalApk(navigator) }, onSelectCustomPatches = { model.selectCustomPatches(navigator) }, specs = model.specs, - enabledPatchCount = model.enabledPatchCount, isPatchEnabled = model::isPatchEnabled, onTogglePatch = model::setPatchEnabled, patchLockState = model::lockState, @@ -93,13 +98,16 @@ fun PatchOptionsScreenContent( packageNameState: PackageNameState, setPackageName: (String) -> Unit, + packageNameLocked: Boolean, + onUnlockPackageName: () -> Unit, + onLockPackageName: () -> Unit, + customTidalApk: PatchComponent?, onSelectCustomTidalApk: () -> Unit, customPatches: PatchComponent?, onSelectCustomPatches: () -> Unit, specs: List, - enabledPatchCount: Int, isPatchEnabled: (PatchSpec) -> Boolean, onTogglePatch: (PatchSpec, Boolean) -> Unit, patchLockState: (PatchSpec) -> PatchLock, @@ -110,6 +118,18 @@ fun PatchOptionsScreenContent( isConfigValid: Boolean, onInstall: () -> Unit, ) { + var showPkgUnlockDialog by rememberSaveable { mutableStateOf(false) } + if (showPkgUnlockDialog) { + PackageNameUnlockDialog( + blockedTitles = specs.filter { it.pathLocked }.map { it.title }, + onConfirm = { + showPkgUnlockDialog = false + onUnlockPackageName() + }, + onDismiss = { showPkgUnlockDialog = false }, + ) + } + Scaffold( topBar = { PatchOptionsAppBar(isUpdate = isUpdate) }, ) { paddingValues -> @@ -159,6 +179,10 @@ fun PatchOptionsScreenContent( valueIsDefault = packageNameIsDefault, onValueChange = setPackageName, onValueReset = { setPackageName(PatchOptions.Default.packageName) }, + locked = packageNameLocked, + onLockClick = { + if (packageNameLocked) showPkgUnlockDialog = true else onLockPackageName() + }, ) { PackageNameStateLabel( state = packageNameState, @@ -167,17 +191,40 @@ fun PatchOptionsScreenContent( } } + val (integrations, patches) = remember(specs) { specs.partition { it.isIntegration } } + + if (integrations.isNotEmpty()) { + PatchSelectionAccordion( + iconRes = R.drawable.ic_extension, + titleRes = R.string.patchopts_integrations_title, + descriptionRes = R.string.patchopts_integrations_desc, + specs = integrations, + enabledCount = integrations.count(isPatchEnabled), + totalCount = integrations.size, + isEnabled = isPatchEnabled, + onToggle = onTogglePatch, + lockState = patchLockState, + variantIndex = variantIndex, + onSelectVariant = onSelectVariant, + optionState = optionState, + modifier = Modifier.padding(top = 4.dp), + ) + } + PatchSelectionAccordion( - specs = specs, - enabledCount = enabledPatchCount, - totalCount = specs.size, + iconRes = R.drawable.ic_healing, + titleRes = R.string.patchopts_patches_title, + descriptionRes = R.string.patchopts_patches_desc, + specs = patches, + enabledCount = patches.count(isPatchEnabled), + totalCount = patches.size, isEnabled = isPatchEnabled, onToggle = onTogglePatch, lockState = patchLockState, variantIndex = variantIndex, onSelectVariant = onSelectVariant, optionState = optionState, - modifier = Modifier.padding(top = 4.dp), + modifier = if (integrations.isEmpty()) Modifier.padding(top = 4.dp) else Modifier, ) if (isDevMode) { diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt index 12bb65e..3467ccf 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt @@ -29,7 +29,16 @@ data class PatchSpec( val variants: List = emptyList(), val defaultVariantIndex: Int = 0, val advancedOptions: List = emptyList(), -) + val category: String = CATEGORY_PATCH, + val pathLocked: Boolean = false, +) { + val isIntegration: Boolean get() = category == CATEGORY_INTEGRATION + + companion object { + const val CATEGORY_PATCH = "patch" + const val CATEGORY_INTEGRATION = "integration" + } +} @Immutable @Serializable @@ -163,6 +172,8 @@ fun builtinPatchSpecs(resolve: (Int) -> String): List = variants = patch.variants.map { VariantSpec(resolve(it.titleRes), it.fileNames, it.extensionFiles) }, defaultVariantIndex = patch.defaultVariantIndex, advancedOptions = patch.advancedOptions.map { it.toSpec(resolve) }, + category = patch.category, + pathLocked = patch.pathLocked, ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt new file mode 100644 index 0000000..3b6afdc --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt @@ -0,0 +1,88 @@ +package com.meowarex.rlmobile.ui.screens.patchopts.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions + +/** + * Confirms unlocking the package-name field, warning that a custom name + * blocks the listed patches/integrations from working. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PackageNameUnlockDialog( + blockedTitles: List, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + BasicAlertDialog(onDismissRequest = onDismiss) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + ) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = stringResource(R.string.patchopts_pkgname_warning_title), + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = AnnotatedString.fromHtml( + stringResource(R.string.patchopts_pkgname_warning_msg, PatchOptions.Default.packageName), + ), + style = MaterialTheme.typography.bodyLarge, + ) + if (blockedTitles.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.padding(start = 4.dp), + ) { + for (title in blockedTitles) { + Text( + text = "• $title", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + ) + } + } + } + Text( + text = stringResource(R.string.patchopts_pkgname_warning_permanent), + style = MaterialTheme.typography.bodyLarge, + ) + Box(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.align(Alignment.CenterEnd)) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_cancel)) + } + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.action_continue)) + } + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt index 5bfa5b5..70ab32c 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt @@ -1,6 +1,8 @@ package com.meowarex.rlmobile.ui.screens.patchopts.components +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background @@ -27,6 +29,7 @@ import com.meowarex.rlmobile.R import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec import com.meowarex.rlmobile.ui.screens.patchopts.effectiveVariants import com.meowarex.rlmobile.ui.screens.patchopts.resolveVariantIndex @@ -35,6 +38,9 @@ private data class LockInfo(val patch: PatchSpec, val lock: PatchLock) @Composable fun PatchSelectionAccordion( + @DrawableRes iconRes: Int, + @StringRes titleRes: Int, + @StringRes descriptionRes: Int, specs: List, enabledCount: Int, totalCount: Int, @@ -69,12 +75,17 @@ fun PatchSelectionAccordion( .clickable(role = Role.Button) { expanded = !expanded } .padding(horizontal = 16.dp, vertical = 14.dp), ) { + Icon( + painter = painterResource(iconRes), + contentDescription = null, + ) + Column( verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.weight(1f), ) { Text( - text = stringResource(R.string.patchopts_patches_title), + text = stringResource(titleRes), style = MaterialTheme.typography.titleMedium, ) Text( @@ -106,7 +117,7 @@ fun PatchSelectionAccordion( .padding(horizontal = 16.dp, vertical = 12.dp), ) { Text( - text = stringResource(R.string.patchopts_patches_desc), + text = stringResource(descriptionRes), style = MaterialTheme.typography.bodySmall, modifier = Modifier .alpha(.7f) @@ -372,6 +383,11 @@ private fun PatchLockDialog( R.string.patch_lock_blocked_msg, lock.by.title, ) + PatchLock.RequiresDefaultPackage -> Triple( + R.string.patch_lock_pkgname_title, + R.string.patch_lock_pkgname_msg, + PatchOptions.Default.packageName, + ) PatchLock.Free -> return } @@ -391,7 +407,7 @@ private fun PatchLockDialog( ) Text( text = AnnotatedString.fromHtml(stringResource(msgRes, blockerTitle)), - style = MaterialTheme.typography.titleMedium, + style = MaterialTheme.typography.bodyLarge, ) Box(modifier = Modifier.fillMaxWidth()) { TextButton( diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt index 46414d5..5b59819 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt @@ -3,8 +3,12 @@ package com.meowarex.rlmobile.ui.screens.patchopts.components.options import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R import com.meowarex.rlmobile.ui.components.Label import com.meowarex.rlmobile.ui.components.ResetToDefaultButton @@ -18,6 +22,8 @@ fun TextPatchOption( onValueChange: (String) -> Unit, onValueReset: () -> Unit, modifier: Modifier = Modifier, + locked: Boolean? = null, + onLockClick: () -> Unit = {}, extra: (@Composable ColumnScope.() -> Unit)? = null, ) { Label( @@ -30,16 +36,41 @@ fun TextPatchOption( onValueChange = onValueChange, isError = valueIsError, singleLine = true, + enabled = locked != true, colors = OutlinedTextFieldDefaults.colors( unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, errorContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, ), trailingIcon = { - ResetToDefaultButton( - enabled = !valueIsDefault, - onClick = onValueReset, - ) + Row(verticalAlignment = Alignment.CenterVertically) { + if (locked != true) { + ResetToDefaultButton( + enabled = !valueIsDefault, + onClick = onValueReset, + ) + } + if (locked != null) { + IconButton( + onClick = onLockClick, + modifier = Modifier.size(32.dp), + ) { + Icon( + painter = painterResource( + if (locked) R.drawable.ic_lock else R.drawable.ic_lock_open, + ), + contentDescription = stringResource( + if (locked) R.string.patchopts_field_unlock else R.string.patchopts_field_lock, + ), + tint = if (locked) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.width(8.dp)) + } + } }, modifier = Modifier .fillMaxWidth() diff --git a/Manager/app/src/main/res/drawable/ic_healing.xml b/Manager/app/src/main/res/drawable/ic_healing.xml new file mode 100644 index 0000000..6bebe3b --- /dev/null +++ b/Manager/app/src/main/res/drawable/ic_healing.xml @@ -0,0 +1,9 @@ + + + diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index f1aa74a..3e92d46 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -252,9 +252,16 @@ Patches Toggle which patches are applied during installation. Unchecked patches are skipped entirely. %1$d of %2$d enabled + Integrations + Connect TIDAL to other systems/apps + Custom Package Name + Installing under a package name other than <b>%s</b> will block the following from working: + The package name is permanent once installed (requires a fresh install) Advanced Options Advanced Options Toggle snapping to steps + Unlock editing + Lock and reset to default Disable Lyrics Cover Removes the mini track cover from the lyrics screen. @@ -333,6 +340,8 @@ Patch Blocked! Patch Required by <b>%s</b>. Patch Blocked by <b>%s</b>. + Original Package Name Invalid! + This only works while the app keeps its original <b>%s</b> package name. Reset the Package Name option to enable it.<br/><br/>An existing install can\'t be renamed (requires a fresh install) Option Locked Requires the <b>%s</b> style. Requires the <b>%s</b> option. diff --git a/patches/manifest.json b/patches/manifest.json index 23bfda2..4f57621 100644 --- a/patches/manifest.json +++ b/patches/manifest.json @@ -215,6 +215,8 @@ { "id": "WazeIntegration", "order": 90, + "category": "integration", + "pathLocked": true, "fileNames": ["waze-media-browser.patch"], "extensionFiles": ["radiant/WazeInitReceiver.smali", "radiant/WazeServiceConnection.smali"], "title": "Waze Integration",