Skip to content

Commit 9357887

Browse files
LavaDesuNuckyz
andauthored
fix(Spotify - Custom theme): Apply accent color in more places (#5039)
Co-authored-by: Nuckyz <[email protected]>
1 parent e8aa9c3 commit 9357887

File tree

3 files changed

+160
-128
lines changed

3 files changed

+160
-128
lines changed

extensions/spotify/src/main/java/app/revanced/extension/spotify/layout/theme/CustomThemePatch.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,54 @@
88
@SuppressWarnings("unused")
99
public final class CustomThemePatch {
1010

11+
private static final int BACKGROUND_COLOR = getColorFromString("@color/gray_7");
12+
private static final int BACKGROUND_COLOR_SECONDARY = getColorFromString("@color/gray_15");
13+
private static final int ACCENT_COLOR = getColorFromString("@color/spotify_green_157");
14+
private static final int ACCENT_PRESSED_COLOR =
15+
getColorFromString("@color/dark_brightaccent_background_press");
16+
1117
/**
12-
* Injection point.
18+
* Returns an int representation of the color resource or hex code.
1319
*/
14-
public static long getThemeColor(String colorString) {
20+
private static int getColorFromString(String colorString) {
1521
try {
1622
return Utils.getColorFromString(colorString);
1723
} catch (Exception ex) {
18-
Logger.printException(() -> "Invalid custom color: " + colorString, ex);
24+
Logger.printException(() -> "Invalid color string: " + colorString, ex);
1925
return Color.BLACK;
2026
}
2127
}
28+
29+
/**
30+
* Injection point. Returns an int representation of the replaced color from the original color.
31+
*/
32+
public static int replaceColor(int originalColor) {
33+
switch (originalColor) {
34+
// Playlist background color.
35+
case 0xFF121212:
36+
return BACKGROUND_COLOR;
37+
38+
// Share menu background color.
39+
case 0xFF1F1F1F:
40+
// Home category pills background color.
41+
case 0xFF333333:
42+
// Settings header background color.
43+
case 0xFF282828:
44+
// Spotify Connect device list background color.
45+
case 0xFF2A2A2A:
46+
return BACKGROUND_COLOR_SECONDARY;
47+
48+
// Some Lottie animations have a color that's slightly off due to rounding errors.
49+
case 0xFF1ED760: case 0xFF1ED75F:
50+
// Intermediate color used in some animations, same rounding issue.
51+
case 0xFF1DB954: case 0xFF1CB854:
52+
return ACCENT_COLOR;
53+
54+
case 0xFF1ABC54:
55+
return ACCENT_PRESSED_COLOR;
56+
57+
default:
58+
return originalColor;
59+
}
60+
}
2261
}

patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt

Lines changed: 104 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,19 @@ package app.revanced.patches.spotify.layout.theme
22

33
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
44
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
5-
import app.revanced.patcher.fingerprint
65
import app.revanced.patcher.patch.booleanOption
76
import app.revanced.patcher.patch.bytecodePatch
87
import app.revanced.patcher.patch.resourcePatch
98
import app.revanced.patcher.patch.stringOption
10-
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
119
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
1210
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
1311
import app.revanced.util.*
14-
import com.android.tools.smali.dexlib2.AccessFlags
15-
import com.android.tools.smali.dexlib2.Opcode
1612
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
17-
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
13+
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
1814
import org.w3c.dom.Element
1915

2016
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;"
2117

22-
internal val spotifyBackgroundColor = stringOption(
23-
key = "backgroundColor",
24-
default = "@android:color/black",
25-
title = "Primary background color",
26-
description = "The background color. Can be a hex color or a resource reference.",
27-
required = true,
28-
)
29-
30-
internal val overridePlayerGradientColor = booleanOption(
31-
key = "overridePlayerGradientColor",
32-
default = false,
33-
title = "Override player gradient color",
34-
description = "Apply primary background color to the player gradient color, which changes dynamically with the song.",
35-
required = false
36-
)
37-
38-
internal val spotifyBackgroundColorSecondary = stringOption(
39-
key = "backgroundColorSecondary",
40-
default = "#FF121212",
41-
title = "Secondary background color",
42-
description =
43-
"The secondary background color. (e.g. playlist list in home, player artist, song credits). Can be a hex color or a resource reference.",
44-
required = true,
45-
)
46-
47-
internal val spotifyAccentColor = stringOption(
48-
key = "accentColor",
49-
default = "#FF1ED760",
50-
title = "Accent color",
51-
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
52-
required = true,
53-
)
54-
55-
internal val spotifyAccentColorPressed = stringOption(
56-
key = "accentColorPressed",
57-
default = "#FF169C46",
58-
title = "Pressed dark theme accent color",
59-
description =
60-
"The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.",
61-
required = true,
62-
)
63-
6418
private val customThemeBytecodePatch = bytecodePatch {
6519
dependsOn(sharedExtensionPatch)
6620

@@ -71,60 +25,60 @@ private val customThemeBytecodePatch = bytecodePatch {
7125
return@execute
7226
}
7327

74-
fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) {
75-
val index = indexOfFirstLiteralInstructionOrThrow(literal)
76-
val register = getInstruction<OneRegisterInstruction>(index).registerA
28+
val colorSpaceUtilsClassDef = colorSpaceUtilsClassFingerprint.originalClassDef
7729

30+
// Hook a util method that converts ARGB to RGBA in the sRGB color space to replace hardcoded accent colors.
31+
convertArgbToRgbaFingerprint.match(colorSpaceUtilsClassDef).method.apply {
7832
addInstructions(
79-
index + 1,
33+
0,
8034
"""
81-
const-string v$register, "$colorString"
82-
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J
83-
move-result-wide v$register
35+
long-to-int p0, p0
36+
invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
37+
move-result p0
38+
int-to-long p0, p0
8439
"""
8540
)
8641
}
8742

88-
val encoreColorsClassName = with(encoreThemeFingerprint.originalMethod) {
89-
// "Encore" colors are referenced right before the value of POSITIVE_INFINITY is returned.
90-
// Begin the instruction find using the index of where POSITIVE_INFINITY is set into the register.
91-
val positiveInfinityIndex = indexOfFirstLiteralInstructionOrThrow(
92-
Float.POSITIVE_INFINITY
93-
)
94-
val encoreColorsFieldReferenceIndex = indexOfFirstInstructionReversedOrThrow(
95-
positiveInfinityIndex,
96-
Opcode.SGET_OBJECT
97-
)
98-
99-
getInstruction(encoreColorsFieldReferenceIndex)
100-
.getReference<FieldReference>()!!.definingClass
101-
}
102-
103-
val encoreColorsConstructorFingerprint = fingerprint {
104-
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
105-
custom { method, classDef ->
106-
classDef.type == encoreColorsClassName &&
107-
method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL)
43+
// Lottie JSON parser method. It parses the JSON Lottie animation into its own class,
44+
// including the solid color of it.
45+
parseLottieJsonFingerprint.method.apply {
46+
val invokeParseColorIndex = indexOfFirstInstructionOrThrow {
47+
val reference = getReference<MethodReference>()
48+
reference?.definingClass == "Landroid/graphics/Color;"
49+
&& reference.name == "parseColor"
10850
}
109-
}
51+
val parsedColorRegister = getInstruction<OneRegisterInstruction>(invokeParseColorIndex + 1).registerA
11052

111-
val backgroundColor by spotifyBackgroundColor
112-
val backgroundColorSecondary by spotifyBackgroundColorSecondary
53+
val replaceColorDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I"
11354

114-
encoreColorsConstructorFingerprint.method.apply {
115-
addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!)
116-
addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!)
55+
addInstructions(
56+
invokeParseColorIndex + 2,
57+
"""
58+
# Use invoke-static/range because the register number is too large.
59+
invoke-static/range { v$parsedColorRegister .. v$parsedColorRegister }, $replaceColorDescriptor
60+
move-result v$parsedColorRegister
61+
"""
62+
)
11763
}
11864

119-
homeCategoryPillColorsFingerprint.method.addColorChangeInstructions(
120-
HOME_CATEGORY_PILL_COLOR_LITERAL,
121-
backgroundColorSecondary!!
122-
)
65+
// Lottie animated color parser.
66+
parseAnimatedColorFingerprint.method.apply {
67+
val invokeArgbIndex = indexOfFirstInstructionOrThrow {
68+
val reference = getReference<MethodReference>()
69+
reference?.definingClass == "Landroid/graphics/Color;"
70+
&& reference.name == "argb"
71+
}
72+
val argbColorRegister = getInstruction<OneRegisterInstruction>(invokeArgbIndex + 1).registerA
12373

124-
settingsHeaderColorFingerprint.method.addColorChangeInstructions(
125-
SETTINGS_HEADER_COLOR_LITERAL,
126-
backgroundColorSecondary!!
127-
)
74+
addInstructions(
75+
invokeArgbIndex + 2,
76+
"""
77+
invoke-static { v$argbColorRegister }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
78+
move-result v$argbColorRegister
79+
"""
80+
)
81+
}
12882
}
12983
}
13084

@@ -138,11 +92,48 @@ val customThemePatch = resourcePatch(
13892

13993
dependsOn(customThemeBytecodePatch)
14094

141-
val backgroundColor by spotifyBackgroundColor()
142-
val overridePlayerGradientColor by overridePlayerGradientColor()
143-
val backgroundColorSecondary by spotifyBackgroundColorSecondary()
144-
val accentColor by spotifyAccentColor()
145-
val accentColorPressed by spotifyAccentColorPressed()
95+
val backgroundColor by stringOption(
96+
key = "backgroundColor",
97+
default = "@android:color/black",
98+
title = "Primary background color",
99+
description = "The background color. Can be a hex color or a resource reference.",
100+
required = true,
101+
)
102+
103+
val overridePlayerGradientColor by booleanOption(
104+
key = "overridePlayerGradientColor",
105+
default = false,
106+
title = "Override player gradient color",
107+
description =
108+
"Apply primary background color to the player gradient color, which changes dynamically with the song.",
109+
required = false,
110+
)
111+
112+
val backgroundColorSecondary by stringOption(
113+
key = "backgroundColorSecondary",
114+
default = "#FF121212",
115+
title = "Secondary background color",
116+
description = "The secondary background color. (e.g. playlist list in home, player artist, song credits). " +
117+
"Can be a hex color or a resource reference.\",",
118+
required = true,
119+
)
120+
121+
val accentColor by stringOption(
122+
key = "accentColor",
123+
default = "#FF1ED760",
124+
title = "Accent color",
125+
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
126+
required = true,
127+
)
128+
129+
val accentColorPressed by stringOption(
130+
key = "accentColorPressed",
131+
default = "#FF1ABC54",
132+
title = "Pressed dark theme accent color",
133+
description = "The color when accented buttons are pressed, by default slightly darker than accent. " +
134+
"Can be a hex color or a resource reference.",
135+
required = true,
136+
)
146137

147138
execute {
148139
document("res/values/colors.xml").use { document ->
@@ -161,34 +152,41 @@ val customThemePatch = resourcePatch(
161152
}
162153

163154
node.textContent = when (name) {
164-
// Gradient next to user photo and "All" in home page.
165-
"dark_base_background_base",
166-
// Main background.
155+
// Main background color.
167156
"gray_7",
168-
// Left sidebar background in tablet mode.
157+
// Left sidebar background color in tablet mode.
169158
"gray_10",
170-
// "Add account", "Settings and privacy", "View Profile" left sidebar background.
159+
// Gradient next to user photo and "All" in home page.
160+
"dark_base_background_base",
161+
// "Add account", "Settings and privacy", "View Profile" left sidebar background color.
171162
"dark_base_background_elevated_base",
172163
// Song/player gradient start/end color.
173164
"bg_gradient_start_color", "bg_gradient_end_color",
174-
// Login screen background and gradient start.
165+
// Login screen background color and gradient start.
175166
"sthlm_blk", "sthlm_blk_grad_start",
176167
// Misc.
177168
"image_placeholder_color",
178169
-> backgroundColor
179170

180-
// Track credits, merch background in song player.
171+
// "About the artist" background color in song player.
172+
"gray_15",
173+
// Track credits, merch background color in song player.
181174
"track_credits_card_bg", "benefit_list_default_color", "merch_card_background",
182175
// Playlist list background in home page.
183176
"opacity_white_10",
184-
// "About the artist" background in song player.
185-
"gray_15",
186177
// "What's New" pills background.
187178
"dark_base_background_tinted_highlight"
188179
-> backgroundColorSecondary
189180

190-
"dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor
191-
"dark_brightaccent_background_press" -> accentColorPressed
181+
"dark_brightaccent_background_base",
182+
"dark_base_text_brightaccent",
183+
"green_light",
184+
"spotify_green_157"
185+
-> accentColor
186+
187+
"dark_brightaccent_background_press"
188+
-> accentColorPressed
189+
192190
else -> continue
193191
}
194192
}
@@ -198,8 +196,8 @@ val customThemePatch = resourcePatch(
198196
document("res/drawable/start_screen_gradient.xml").use { document ->
199197
val gradientNode = document.getElementsByTagName("gradient").item(0) as Element
200198

201-
gradientNode.setAttribute("android:startColor", backgroundColor)
202-
gradientNode.setAttribute("android:endColor", backgroundColor)
199+
gradientNode.setAttribute("android:startColor", "@color/gray_7")
200+
gradientNode.setAttribute("android:endColor", "@color/gray_7")
203201
}
204202
}
205203
}

patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,25 @@ import app.revanced.patcher.fingerprint
44
import app.revanced.util.containsLiteralInstruction
55
import com.android.tools.smali.dexlib2.AccessFlags
66

7-
internal val encoreThemeFingerprint = fingerprint {
8-
strings("Encore theme was not provided.") // Partial string match.
9-
custom { method, _ ->
10-
method.name == "invoke"
11-
}
7+
internal val colorSpaceUtilsClassFingerprint = fingerprint {
8+
strings("The specified color must be encoded in an RGB color space.") // Partial string match.
129
}
1310

14-
internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212
15-
internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F
16-
internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333
17-
internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828
11+
internal val convertArgbToRgbaFingerprint = fingerprint {
12+
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
13+
returns("J")
14+
parameters("J")
15+
}
1816

19-
internal val homeCategoryPillColorsFingerprint = fingerprint{
20-
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
21-
custom { method, _ ->
22-
method.containsLiteralInstruction(HOME_CATEGORY_PILL_COLOR_LITERAL) &&
23-
method.containsLiteralInstruction(0x33000000)
24-
}
17+
internal val parseLottieJsonFingerprint = fingerprint {
18+
strings("Unsupported matte type: ")
2519
}
2620

27-
internal val settingsHeaderColorFingerprint = fingerprint {
28-
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
21+
internal val parseAnimatedColorFingerprint = fingerprint {
22+
parameters("L", "F")
23+
returns("Ljava/lang/Object;")
2924
custom { method, _ ->
30-
method.containsLiteralInstruction(SETTINGS_HEADER_COLOR_LITERAL) &&
31-
method.containsLiteralInstruction(0)
25+
method.containsLiteralInstruction(255.0) &&
26+
method.containsLiteralInstruction(1.0)
3227
}
3328
}

0 commit comments

Comments
 (0)