From d7a7a0b982dbafa181b04f984a5f7618fb067c2a Mon Sep 17 00:00:00 2001 From: Nuckyz <61953774+Nuckyz@users.noreply.github.com> Date: Wed, 2 Apr 2025 14:14:17 -0300 Subject: [PATCH] fix(Spotify - Custom theme): Override more color resources (#4690) --- .../app/revanced/extension/shared/Utils.java | 11 +++ .../layout/theme/CustomThemePatch.java | 22 +++++ .../extension/youtube/ThemeHelper.java | 24 +++--- .../layout/theme/CustomThemeBytecodePatch.kt | 75 ++++++++++++++++ .../spotify/layout/theme/CustomThemePatch.kt | 86 ++++++++----------- .../spotify/layout/theme/Fingerprints.kt | 30 +++++++ .../patches/spotify/layout/theme/Options.kt | 36 ++++++++ 7 files changed, 222 insertions(+), 62 deletions(-) create mode 100644 extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/theme/CustomThemePatch.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index 47806567c..8343d595c 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -9,6 +9,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.net.ConnectivityManager; import android.os.Build; import android.os.Bundle; @@ -799,4 +800,14 @@ public class Utils { builder.getContext().setTheme(editTextDialogStyle); } } + + /** + * Parse a color resource or hex code to an int representation of the color. + */ + public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException { + if (colorString.startsWith("#")) { + return Color.parseColor(colorString); + } + return getResourceColor(colorString); + } } diff --git a/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/theme/CustomThemePatch.java b/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/theme/CustomThemePatch.java new file mode 100644 index 000000000..8949b9b2d --- /dev/null +++ b/extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/theme/CustomThemePatch.java @@ -0,0 +1,22 @@ +package app.revanced.extension.spotify.layout.theme; + +import android.graphics.Color; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +@SuppressWarnings("unused") +public final class CustomThemePatch { + + /** + * Injection point. + */ + public static long getThemeColor(String colorString) { + try { + return Utils.getColorFromString(colorString); + } catch (Exception ex) { + Logger.printException(() -> "Invalid custom color: " + colorString, ex); + return Color.BLACK; + } + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java index c4887b8e3..80b9b583c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java @@ -45,13 +45,24 @@ public class ThemeHelper { return "@color/yt_black3"; } + private static int getThemeColor(String resourceName, int defaultColor) { + try { + return Utils.getColorFromString(resourceName); + } catch (Exception ex) { + // User entered an invalid custom theme color. + // Normally this should never be reached, and no localized strings are needed. + Utils.showToastLong("Invalid custom theme color: " + resourceName); + return defaultColor; + } + } + /** * @return The dark theme color as specified by the Theme patch (if included), * or the dark mode background color unpatched YT uses. */ public static int getDarkThemeColor() { if (darkThemeColor == null) { - darkThemeColor = getColorInt(darkThemeResourceName()); + darkThemeColor = getThemeColor(darkThemeResourceName(), Color.BLACK); } return darkThemeColor; } @@ -71,18 +82,11 @@ public class ThemeHelper { */ public static int getLightThemeColor() { if (lightThemeColor == null) { - lightThemeColor = getColorInt(lightThemeResourceName()); + lightThemeColor = getThemeColor(lightThemeResourceName(), Color.WHITE); } return lightThemeColor; } - private static int getColorInt(String colorString) { - if (colorString.startsWith("#")) { - return Color.parseColor(colorString); - } - return Utils.getResourceColor(colorString); - } - public static int getBackgroundColor() { return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor(); } @@ -96,6 +100,6 @@ public class ThemeHelper { ? "yt_black3" : "yt_white1"; - return getColorInt(colorName); + return Utils.getColorFromString(colorName); } } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt new file mode 100644 index 000000000..cdedbfd6a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt @@ -0,0 +1,75 @@ +package app.revanced.patches.spotify.layout.theme + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch +import app.revanced.util.* +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;" + +internal val customThemeByteCodePatch = bytecodePatch { + dependsOn(sharedExtensionPatch) + + val backgroundColor by spotifyBackgroundColor + val backgroundColorSecondary by spotifyBackgroundColorSecondary + + execute { + fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) { + val index = indexOfFirstLiteralInstructionOrThrow(literal) + val register = getInstruction(index).registerA + + addInstructions( + index + 1, + """ + const-string v$register, "$colorString" + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J + move-result-wide v$register + """ + ) + } + + val encoreColorsClassName = with(encoreThemeFingerprint) { + // Find index of the first static get found after the string constant. + val encoreColorsFieldReferenceIndex = originalMethod.indexOfFirstInstructionOrThrow( + stringMatches!!.first().index, + Opcode.SGET_OBJECT + ) + + originalMethod.getInstruction(encoreColorsFieldReferenceIndex) + .getReference()!!.definingClass + } + + val encoreColorsConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + custom { method, classDef -> + classDef.type == encoreColorsClassName && + method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL) + } + } + + encoreColorsConstructorFingerprint.method.apply { + // Playlist song list background color. + addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!) + + // Share menu background color. + addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!) + } + + homeCategoryPillColorsFingerprint.method.apply { + // Home category pills background color. + addColorChangeInstructions(HOME_CATEGORY_PILL_COLOR_LITERAL, backgroundColorSecondary!!) + } + + settingsHeaderColorFingerprint.method.apply { + // Settings header background color. + addColorChangeInstructions(SETTINGS_HEADER_COLOR_LITERAL, backgroundColorSecondary!!) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt index 8af9d65fb..5f2f09435 100644 --- a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt @@ -1,59 +1,24 @@ -@file:Suppress("NAME_SHADOWING") - package app.revanced.patches.spotify.layout.theme import app.revanced.patcher.patch.resourcePatch -import app.revanced.patcher.patch.stringOption import org.w3c.dom.Element @Suppress("unused") val customThemePatch = resourcePatch( name = "Custom theme", - description = "Applies a custom theme.", + description = "Applies a custom theme (defaults to amoled black)", use = false, ) { compatibleWith("com.spotify.music") - val backgroundColor by stringOption( - key = "backgroundColor", - default = "@android:color/black", - title = "Primary background color", - description = "The background color. Can be a hex color or a resource reference.", - required = true, - ) + dependsOn(customThemeByteCodePatch) - val backgroundColorSecondary by stringOption( - key = "backgroundColorSecondary", - default = "#ff282828", - title = "Secondary background color", - description = "The secondary background color. (e.g. search box, artist & podcast). Can be a hex color or a resource reference.", - required = true, - ) - - val accentColor by stringOption( - key = "accentColor", - default = "#ff1ed760", - title = "Accent color", - description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.", - required = true, - ) - - val accentColorPressed by stringOption( - key = "accentColorPressed", - default = "#ff169c46", - title = "Pressed dark theme accent color", - description = - "The color when accented buttons are pressed, by default slightly darker than accent. " + - "Can be a hex color or a resource reference.", - required = true, - ) + val backgroundColor by spotifyBackgroundColor() + val backgroundColorSecondary by spotifyBackgroundColorSecondary() + val accentColor by spotifyAccentColor() + val accentColorPressed by spotifyAccentColorPressed() execute { - val backgroundColor = backgroundColor!! - val backgroundColorSecondary = backgroundColorSecondary!! - val accentColor = accentColor!! - val accentColorPressed = accentColorPressed!! - document("res/values/colors.xml").use { document -> val resourcesNode = document.getElementsByTagName("resources").item(0) as Element @@ -61,20 +26,37 @@ val customThemePatch = resourcePatch( for (i in 0 until childNodes.length) { val node = childNodes.item(i) as? Element ?: continue - node.textContent = - when (node.getAttribute("name")) { - "dark_base_background_elevated_base", "design_dark_default_color_background", - "design_dark_default_color_surface", "gray_7", "gray_background", "gray_layer", - "sthlm_blk", + node.textContent = when (node.getAttribute("name")) { + // Gradient next to user photo and "All" in home page + "dark_base_background_base", + // Main background + "gray_7", + // Left sidebar background in tablet mode + "gray_10", + // Add account, Settings and privacy, View Profile left sidebar background + "dark_base_background_elevated_base", + // Song/player background + "bg_gradient_start_color", "bg_gradient_end_color", + // Login screen + "sthlm_blk", "sthlm_blk_grad_start", "stockholm_black", + // Misc + "image_placeholder_color", -> backgroundColor - "gray_15" -> backgroundColorSecondary + // Track credits, merch in song player + "track_credits_card_bg", "benefit_list_default_color", "merch_card_background", + // Playlist list background in home page + "opacity_white_10", + // About artist background in song player + "gray_15", + // What's New pills background + "dark_base_background_tinted_highlight" + -> backgroundColorSecondary - "dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor - - "dark_brightaccent_background_press" -> accentColorPressed - else -> continue - } + "dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor + "dark_brightaccent_background_press" -> accentColorPressed + else -> continue + } } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt new file mode 100644 index 000000000..28943e040 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.spotify.layout.theme + +import app.revanced.patcher.fingerprint +import app.revanced.util.containsLiteralInstruction +import com.android.tools.smali.dexlib2.AccessFlags + +internal val encoreThemeFingerprint = fingerprint { + strings("Encore theme was not provided.") // Partial string match. +} + +internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828 +internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333 +internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212 +internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F + +internal val homeCategoryPillColorsFingerprint = fingerprint{ + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + custom { method, _ -> + method.containsLiteralInstruction(HOME_CATEGORY_PILL_COLOR_LITERAL) && + method.containsLiteralInstruction(0x33000000) + } +} + +internal val settingsHeaderColorFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + custom { method, _ -> + method.containsLiteralInstruction(SETTINGS_HEADER_COLOR_LITERAL) && + method.containsLiteralInstruction(0) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt new file mode 100644 index 000000000..e71c97912 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.spotify.layout.theme + +import app.revanced.patcher.patch.stringOption + +internal val spotifyBackgroundColor = stringOption( + key = "backgroundColor", + default = "@android:color/black", + title = "Primary background color", + description = "The background color. Can be a hex color or a resource reference.", + required = true, +) + +internal val spotifyBackgroundColorSecondary = stringOption( + key = "backgroundColorSecondary", + default = "#FF121212", + title = "Secondary background color", + description = "The secondary background color. (e.g. playlist list, player arist, credits). Can be a hex color or a resource reference.", + required = true, +) + +internal val spotifyAccentColor = stringOption( + key = "accentColor", + default = "#FF1ED760", + title = "Accent color", + description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.", + required = true, +) + +internal val spotifyAccentColorPressed = stringOption( + key = "accentColorPressed", + default = "#FF169C46", + title = "Pressed dark theme accent color", + description = + "The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.", + required = true, +)