Skip to content

fix(Spotify - Custom theme): Apply accent color in more places #5039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
@SuppressWarnings("unused")
public final class CustomThemePatch {

private static final int ACCENT = (int)getThemeColor("@color/spotify_green_157");
private static final int ACCENT_PRESSED = (int)getThemeColor("@color/dark_brightaccent_background_press");

/**
* Injection point.
*/
Expand All @@ -19,4 +22,22 @@ public static long getThemeColor(String colorString) {
return Color.BLACK;
}
}

public static long replaceColor(long color) {
return replaceColor((int)color);
}

public static int replaceColor(int color) {
switch (color) {
case 0xff1ed760: case 0xff1ed75f: // Some lottie animations have a color that's slightly off due to rounding errors
case 0xff1db954: case 0xff1cb854: // Intermediate color used in some animations, same rounding issue
return ACCENT;

case 0xff1abc54:
return ACCENT_PRESSED;

default:
return color;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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;"
Expand Down Expand Up @@ -54,7 +55,7 @@ internal val spotifyAccentColor = stringOption(

internal val spotifyAccentColorPressed = stringOption(
key = "accentColorPressed",
default = "#FF169C46",
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.",
Expand Down Expand Up @@ -125,6 +126,38 @@ private val customThemeBytecodePatch = bytecodePatch {
SETTINGS_HEADER_COLOR_LITERAL,
backgroundColorSecondary!!
)

// Hijacks a util method that removes alpha to replace hardcoded accent colors
removeAlphaFingerprint.match(miscUtilsFingerprint.classDef).method.apply {
addInstructions(0, """
invoke-static { p0, p1 }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(J)J
move-result-wide p0
""")
}

// Lottie JSON parser method
// It's a gigantic method that parses each value, including the solid color
parseLottieJsonFingerprint.method.apply {
val invokeIdx = indexOfFirstInstructionOrThrow {
val ref = this.getReference<MethodReference>()
ref?.definingClass == "Landroid/graphics/Color;" && ref.name == "parseColor"
}
val resultRegister = getInstruction<OneRegisterInstruction>(invokeIdx + 1).registerA
addInstructions(invokeIdx + 2, """
invoke-static/range { v$resultRegister .. v$resultRegister }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
move-result v$resultRegister
""")
}

// Lottie animated color parser
parseAnimatedColorFingerprint.method.apply {
val idx = indexOfFirstInstructionReversedOrThrow(Opcode.MOVE_RESULT)
val resultRegister = getInstruction<OneRegisterInstruction>(idx).registerA
addInstructions(idx + 1, """
invoke-static { v$resultRegister }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
move-result v$resultRegister
""")
}
}
}

Expand Down Expand Up @@ -187,8 +220,15 @@ val customThemePatch = resourcePatch(
"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
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,27 @@ internal val settingsHeaderColorFingerprint = fingerprint {
method.containsLiteralInstruction(0)
}
}

internal val miscUtilsFingerprint = fingerprint {
strings("The specified color must be encoded in an RGB color space.")
}

// Requires matching against miscUtilsFingerprint
internal val removeAlphaFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
returns("J")
parameters("J")
}

internal val parseLottieJsonFingerprint = fingerprint {
strings("Unsupported matte type")
}

internal val parseAnimatedColorFingerprint = fingerprint {
parameters("L", "F")
returns("Ljava/lang/Object;")
custom { method, _ ->
method.containsLiteralInstruction(255.0) &&
method.containsLiteralInstruction(1.0)
}
}