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/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/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/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 212fe07..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, @@ -130,6 +132,41 @@ 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, + category = PatchSpec.CATEGORY_INTEGRATION, + pathLocked = true, + 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", + ), + PatchOption.Color( + key = "accent_color", + titleRes = R.string.patch_waze_accent_color_title, + default = -16747037, // Waze default #ff0075e3 + token = "RL_WAZE_THEME_COLOR", + ), + ), + ), LyricsProgressPill( order = 40, fileNames = listOf( 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..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 @@ -59,12 +59,20 @@ 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, 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..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 } @@ -153,11 +171,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,11 +201,14 @@ class PatchOptionsModel( setSlider = ::setSliderValue, choice = ::choiceValue, setChoice = ::setChoiceValue, + color = ::colorValue, + setColor = ::setColorValue, isModified = ::isAdvancedModified, reset = ::resetAdvanced, ) fun lockState(spec: PatchSpec): PatchLock { + if (isBlockedByPackageName(spec)) return PatchLock.RequiresDefaultPackage if (spec.variants.isNotEmpty()) return PatchLock.Free val byId = specs.associateBy { it.id } @@ -206,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) @@ -323,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, @@ -385,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 22aa3d7..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 @@ -106,6 +115,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 @@ -151,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, ) } @@ -187,13 +210,21 @@ 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 ?: "" }, 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 +289,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 +303,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/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/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt index 6c1b8cf..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) }, + ) } } @@ -378,6 +390,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( @@ -400,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, @@ -432,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/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 2aaa020..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. @@ -286,6 +293,19 @@ 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 + Accent Color + Waze Default + TIDAL Cyan + Material You + Auto Menu + Home + Recents + Custom color + Hex color + Use #RRGGBB or #AARRGGBB. Enable Legacy UI 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/data.json b/patches/data.json index de8de32..42050d6 100644 --- a/patches/data.json +++ b/patches/data.json @@ -1,5 +1,5 @@ { "tidalVersionCode": 9090, "tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk", - "patchesVersion": "0.9.14" + "patchesVersion": "0.9.15" } diff --git a/patches/extension/radiant/WazeInitReceiver.smali b/patches/extension/radiant/WazeInitReceiver.smali new file mode 100644 index 0000000..806e17b --- /dev/null +++ b/patches/extension/radiant/WazeInitReceiver.smali @@ -0,0 +1,87 @@ +.class public final Lradiant/WazeInitReceiver; +.super Landroid/content/BroadcastReceiver; +.source "WazeInitReceiver.smali" + + +# direct methods +.method public constructor ()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..97c4bbb --- /dev/null +++ b/patches/extension/radiant/WazeServiceConnection.smali @@ -0,0 +1,228 @@ +.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 + + const-string v1, "themeColor" + + const v2, __RL_WAZE_THEME_COLOR__ # Waze SDK themeColor + + 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..4f57621 100644 --- a/patches/manifest.json +++ b/patches/manifest.json @@ -212,6 +212,39 @@ "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, + "category": "integration", + "pathLocked": true, + "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, + "advancedOptions": [ + { + "type": "choice", + "key": "browse_root", + "title": "Media Menu", + "entries": ["Auto Menu", "Home", "Recents"], + "values": [ + "ROOT_AUTO", + "HOME_V2::home_page_v2_id", + "RECENTLY_PLAYED::home/pages/CONTINUE_LISTEN_TO/view-all" + ], + "defaultIndex": 0, + "token": "RL_WAZE_ROOT_ID" + }, + { + "type": "color", + "key": "accent_color", + "title": "Accent Color", + "default": -16747037, + "token": "RL_WAZE_THEME_COLOR" + } + ] + }, { "id": "DebugMenuUnlock", "order": 100, diff --git a/patches/waze-media-browser.patch b/patches/waze-media-browser.patch new file mode 100644 index 0000000..3943849 --- /dev/null +++ b/patches/waze-media-browser.patch @@ -0,0 +1,59 @@ +--- a/pb/c.smali ++++ b/pb/c.smali +@@ -801,6 +801,26 @@ +- ++ + .line 236 + :goto_8 ++ const-string/jumbo v5, "com.waze" ++ ++ invoke-static {v3, v5}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # package match ++ ++ move-result v5 ++ ++ if-eqz v5, :rl_waze_done ++ ++ 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 ++ ++ invoke-static {v2, v5}, Lkotlin/jvm/internal/n;->b(Ljava/lang/Object;Ljava/lang/Object;)Z # cert match ++ ++ move-result v5 ++ ++ 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;