From 523f0a342b0552e1af456b54ef47a042398c7ff9 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Thu, 2 Jul 2026 15:49:18 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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" } ] },