From 9357887b6fca7aaf34dfb0163645b6a998e1db76 Mon Sep 17 00:00:00 2001 From: Cilly Leang Date: Tue, 3 Jun 2025 18:02:15 +1000 Subject: [PATCH] fix(Spotify - Custom theme): Apply accent color in more places (#5039) Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com> --- .../layout/theme/CustomThemePatch.java | 45 +++- .../spotify/layout/theme/CustomThemePatch.kt | 218 +++++++++--------- .../spotify/layout/theme/Fingerprints.kt | 35 ++- 3 files changed, 165 insertions(+), 133 deletions(-) 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 index 8949b9b2d..ef3932501 100644 --- 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 @@ -8,15 +8,54 @@ import app.revanced.extension.shared.Utils; @SuppressWarnings("unused") public final class CustomThemePatch { + private static final int BACKGROUND_COLOR = getColorFromString("@color/gray_7"); + private static final int BACKGROUND_COLOR_SECONDARY = getColorFromString("@color/gray_15"); + private static final int ACCENT_COLOR = getColorFromString("@color/spotify_green_157"); + private static final int ACCENT_PRESSED_COLOR = + getColorFromString("@color/dark_brightaccent_background_press"); + /** - * Injection point. + * Returns an int representation of the color resource or hex code. */ - public static long getThemeColor(String colorString) { + private static int getColorFromString(String colorString) { try { return Utils.getColorFromString(colorString); } catch (Exception ex) { - Logger.printException(() -> "Invalid custom color: " + colorString, ex); + Logger.printException(() -> "Invalid color string: " + colorString, ex); return Color.BLACK; } } + + /** + * Injection point. Returns an int representation of the replaced color from the original color. + */ + public static int replaceColor(int originalColor) { + switch (originalColor) { + // Playlist background color. + case 0xFF121212: + return BACKGROUND_COLOR; + + // Share menu background color. + case 0xFF1F1F1F: + // Home category pills background color. + case 0xFF333333: + // Settings header background color. + case 0xFF282828: + // Spotify Connect device list background color. + case 0xFF2A2A2A: + return BACKGROUND_COLOR_SECONDARY; + + // Some Lottie animations have a color that's slightly off due to rounding errors. + case 0xFF1ED760: case 0xFF1ED75F: + // Intermediate color used in some animations, same rounding issue. + case 0xFF1DB954: case 0xFF1CB854: + return ACCENT_COLOR; + + case 0xFF1ABC54: + return ACCENT_PRESSED_COLOR; + + default: + return originalColor; + } + } } 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 67a5d65a7..738db4dec 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 @@ -2,65 +2,19 @@ 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.booleanOption import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.patch.stringOption -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET 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 +import com.android.tools.smali.dexlib2.iface.reference.MethodReference import org.w3c.dom.Element private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;" -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 overridePlayerGradientColor = booleanOption( - key = "overridePlayerGradientColor", - default = false, - title = "Override player gradient color", - description = "Apply primary background color to the player gradient color, which changes dynamically with the song.", - required = false -) - -internal val spotifyBackgroundColorSecondary = stringOption( - key = "backgroundColorSecondary", - default = "#FF121212", - title = "Secondary background color", - description = - "The secondary background color. (e.g. playlist list in home, player artist, song 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, -) - private val customThemeBytecodePatch = bytecodePatch { dependsOn(sharedExtensionPatch) @@ -71,60 +25,60 @@ private val customThemeBytecodePatch = bytecodePatch { return@execute } - fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) { - val index = indexOfFirstLiteralInstructionOrThrow(literal) - val register = getInstruction(index).registerA + val colorSpaceUtilsClassDef = colorSpaceUtilsClassFingerprint.originalClassDef + + // Hook a util method that converts ARGB to RGBA in the sRGB color space to replace hardcoded accent colors. + convertArgbToRgbaFingerprint.match(colorSpaceUtilsClassDef).method.apply { + addInstructions( + 0, + """ + long-to-int p0, p0 + invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I + move-result p0 + int-to-long p0, p0 + """ + ) + } + + // Lottie JSON parser method. It parses the JSON Lottie animation into its own class, + // including the solid color of it. + parseLottieJsonFingerprint.method.apply { + val invokeParseColorIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.definingClass == "Landroid/graphics/Color;" + && reference.name == "parseColor" + } + val parsedColorRegister = getInstruction(invokeParseColorIndex + 1).registerA + + val replaceColorDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I" addInstructions( - index + 1, + invokeParseColorIndex + 2, """ - const-string v$register, "$colorString" - invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J - move-result-wide v$register + # Use invoke-static/range because the register number is too large. + invoke-static/range { v$parsedColorRegister .. v$parsedColorRegister }, $replaceColorDescriptor + move-result v$parsedColorRegister """ ) } - val encoreColorsClassName = with(encoreThemeFingerprint.originalMethod) { - // "Encore" colors are referenced right before the value of POSITIVE_INFINITY is returned. - // Begin the instruction find using the index of where POSITIVE_INFINITY is set into the register. - val positiveInfinityIndex = indexOfFirstLiteralInstructionOrThrow( - Float.POSITIVE_INFINITY - ) - val encoreColorsFieldReferenceIndex = indexOfFirstInstructionReversedOrThrow( - positiveInfinityIndex, - Opcode.SGET_OBJECT - ) - - getInstruction(encoreColorsFieldReferenceIndex) - .getReference()!!.definingClass - } - - val encoreColorsConstructorFingerprint = fingerprint { - accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) - custom { method, classDef -> - classDef.type == encoreColorsClassName && - method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL) + // Lottie animated color parser. + parseAnimatedColorFingerprint.method.apply { + val invokeArgbIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.definingClass == "Landroid/graphics/Color;" + && reference.name == "argb" } + val argbColorRegister = getInstruction(invokeArgbIndex + 1).registerA + + addInstructions( + invokeArgbIndex + 2, + """ + invoke-static { v$argbColorRegister }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I + move-result v$argbColorRegister + """ + ) } - - val backgroundColor by spotifyBackgroundColor - val backgroundColorSecondary by spotifyBackgroundColorSecondary - - encoreColorsConstructorFingerprint.method.apply { - addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!) - addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!) - } - - homeCategoryPillColorsFingerprint.method.addColorChangeInstructions( - HOME_CATEGORY_PILL_COLOR_LITERAL, - backgroundColorSecondary!! - ) - - settingsHeaderColorFingerprint.method.addColorChangeInstructions( - SETTINGS_HEADER_COLOR_LITERAL, - backgroundColorSecondary!! - ) } } @@ -138,11 +92,48 @@ val customThemePatch = resourcePatch( dependsOn(customThemeBytecodePatch) - val backgroundColor by spotifyBackgroundColor() - val overridePlayerGradientColor by overridePlayerGradientColor() - val backgroundColorSecondary by spotifyBackgroundColorSecondary() - val accentColor by spotifyAccentColor() - val accentColorPressed by spotifyAccentColorPressed() + 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, + ) + + val overridePlayerGradientColor by booleanOption( + key = "overridePlayerGradientColor", + default = false, + title = "Override player gradient color", + description = + "Apply primary background color to the player gradient color, which changes dynamically with the song.", + required = false, + ) + + val backgroundColorSecondary by stringOption( + key = "backgroundColorSecondary", + default = "#FF121212", + title = "Secondary background color", + description = "The secondary background color. (e.g. playlist list in home, player artist, song credits). " + + "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 = "#FF1ABC54", + 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, + ) execute { document("res/values/colors.xml").use { document -> @@ -161,34 +152,41 @@ val customThemePatch = resourcePatch( } node.textContent = when (name) { + // Main background color. + "gray_7", + // Left sidebar background color in tablet mode. + "gray_10", // 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. + // "Add account", "Settings and privacy", "View Profile" left sidebar background color. "dark_base_background_elevated_base", // Song/player gradient start/end color. "bg_gradient_start_color", "bg_gradient_end_color", - // Login screen background and gradient start. + // Login screen background color and gradient start. "sthlm_blk", "sthlm_blk_grad_start", // Misc. "image_placeholder_color", -> backgroundColor - // Track credits, merch background in song player. + // "About the artist" background color in song player. + "gray_15", + // Track credits, merch background color in song player. "track_credits_card_bg", "benefit_list_default_color", "merch_card_background", // Playlist list background in home page. "opacity_white_10", - // "About the 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 + "dark_brightaccent_background_base", + "dark_base_text_brightaccent", + "green_light", + "spotify_green_157" + -> accentColor + + "dark_brightaccent_background_press" + -> accentColorPressed + else -> continue } } @@ -198,8 +196,8 @@ val customThemePatch = resourcePatch( document("res/drawable/start_screen_gradient.xml").use { document -> val gradientNode = document.getElementsByTagName("gradient").item(0) as Element - gradientNode.setAttribute("android:startColor", backgroundColor) - gradientNode.setAttribute("android:endColor", backgroundColor) + gradientNode.setAttribute("android:startColor", "@color/gray_7") + gradientNode.setAttribute("android:endColor", "@color/gray_7") } } } 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 index 355422477..eb3ab7839 100644 --- 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 @@ -4,30 +4,25 @@ 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. - custom { method, _ -> - method.name == "invoke" - } +internal val colorSpaceUtilsClassFingerprint = fingerprint { + strings("The specified color must be encoded in an RGB color space.") // Partial string match. } -internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212 -internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F -internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333 -internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828 - -internal val homeCategoryPillColorsFingerprint = fingerprint{ - accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) - custom { method, _ -> - method.containsLiteralInstruction(HOME_CATEGORY_PILL_COLOR_LITERAL) && - method.containsLiteralInstruction(0x33000000) - } +internal val convertArgbToRgbaFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL) + returns("J") + parameters("J") } -internal val settingsHeaderColorFingerprint = fingerprint { - accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) +internal val parseLottieJsonFingerprint = fingerprint { + strings("Unsupported matte type: ") +} + +internal val parseAnimatedColorFingerprint = fingerprint { + parameters("L", "F") + returns("Ljava/lang/Object;") custom { method, _ -> - method.containsLiteralInstruction(SETTINGS_HEADER_COLOR_LITERAL) && - method.containsLiteralInstruction(0) + method.containsLiteralInstruction(255.0) && + method.containsLiteralInstruction(1.0) } }