Skip to content

Commit 4c8b56f

Browse files
feat(YouTube Music): Add Custom branding patch (#6007)
Co-authored-by: LisoUseInAIKyrios <[email protected]>
1 parent 1754023 commit 4c8b56f

File tree

61 files changed

+281
-122
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+281
-122
lines changed

patches/api/patches.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ public final class app/revanced/patches/music/interaction/permanentshuffle/Perma
372372
public static final fun getPermanentShufflePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
373373
}
374374

375+
public final class app/revanced/patches/music/layout/branding/CustomBrandingPatchKt {
376+
public static final fun getCustomBrandingPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
377+
}
378+
375379
public final class app/revanced/patches/music/layout/castbutton/HideCastButtonKt {
376380
public static final fun getHideCastButton ()Lapp/revanced/patcher/patch/BytecodePatch;
377381
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package app.revanced.patches.music.layout.branding
2+
3+
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
4+
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
5+
import app.revanced.patcher.patch.bytecodePatch
6+
import app.revanced.patcher.util.smali.ExternalLabel
7+
import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch
8+
import app.revanced.patches.shared.misc.mapping.get
9+
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
10+
import app.revanced.patches.shared.misc.mapping.resourceMappings
11+
import app.revanced.util.getReference
12+
import app.revanced.util.indexOfFirstInstructionOrThrow
13+
import app.revanced.util.indexOfFirstInstructionReversed
14+
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
15+
import com.android.tools.smali.dexlib2.Opcode
16+
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
17+
18+
private val disableSplashAnimationPatch = bytecodePatch {
19+
20+
dependsOn(resourceMappingPatch)
21+
22+
execute {
23+
// The existing YT animation usually only shows for a fraction of a second,
24+
// and the existing animation does not match the new splash screen
25+
// causing the original YT Music logo to momentarily flash on screen as the animation starts.
26+
//
27+
// Could replace the lottie animation file with our own custom animation (app_launch.json),
28+
// but the animation is not always the same size as the launch screen and it's still
29+
// barely shown. Instead turn off the animation entirely (app will also launch a little faster).
30+
cairoSplashAnimationConfigFingerprint.method.apply {
31+
val mainActivityLaunchAnimation = resourceMappings["layout", "main_activity_launch_animation"]
32+
val literalIndex = indexOfFirstLiteralInstructionOrThrow(
33+
mainActivityLaunchAnimation
34+
)
35+
val insertIndex = indexOfFirstInstructionReversed(literalIndex) {
36+
this.opcode == Opcode.INVOKE_VIRTUAL &&
37+
getReference<MethodReference>()?.name == "setContentView"
38+
} + 1
39+
val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
40+
opcode == Opcode.INVOKE_VIRTUAL &&
41+
getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
42+
} + 1
43+
44+
addInstructionsWithLabels(
45+
insertIndex,
46+
"goto :skip_animation",
47+
ExternalLabel("skip_animation", getInstruction(jumpIndex))
48+
)
49+
}
50+
}
51+
}
52+
53+
private const val APP_NAME = "YT Music ReVanced"
54+
55+
@Suppress("unused")
56+
val customBrandingPatch = baseCustomBrandingPatch(
57+
defaultAppName = APP_NAME,
58+
appNameValues = mapOf(
59+
"YT Music ReVanced" to APP_NAME,
60+
"Music ReVanced" to "Music ReVanced",
61+
"Music" to "Music",
62+
"YT Music" to "YT Music",
63+
),
64+
resourceFolder = "custom-branding/music",
65+
iconResourceFileNames = arrayOf(
66+
"adaptiveproduct_youtube_music_2024_q4_background_color_108",
67+
"adaptiveproduct_youtube_music_2024_q4_foreground_color_108",
68+
"ic_launcher_release",
69+
),
70+
71+
block = {
72+
dependsOn(disableSplashAnimationPatch)
73+
74+
compatibleWith(
75+
"com.google.android.apps.youtube.music"(
76+
"7.29.52",
77+
"8.10.52"
78+
)
79+
)
80+
}
81+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package app.revanced.patches.music.layout.branding
2+
3+
import app.revanced.patcher.fingerprint
4+
import app.revanced.patches.music.shared.YOUTUBE_MUSIC_MAIN_ACTIVITY_CLASS_TYPE
5+
6+
internal val cairoSplashAnimationConfigFingerprint = fingerprint {
7+
returns("V")
8+
parameters("Landroid/os/Bundle;")
9+
custom { method, classDef ->
10+
method.name == "onCreate" && method.definingClass == YOUTUBE_MUSIC_MAIN_ACTIVITY_CLASS_TYPE
11+
}
12+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package app.revanced.patches.shared.layout.branding
2+
3+
import app.revanced.patcher.patch.ResourcePatch
4+
import app.revanced.patcher.patch.ResourcePatchBuilder
5+
import app.revanced.patcher.patch.ResourcePatchContext
6+
import app.revanced.patcher.patch.resourcePatch
7+
import app.revanced.patcher.patch.stringOption
8+
import app.revanced.util.ResourceGroup
9+
import app.revanced.util.Utils.trimIndentMultiline
10+
import app.revanced.util.copyResources
11+
import java.io.File
12+
import java.nio.file.Files
13+
import java.util.logging.Logger
14+
15+
private const val REVANCED_ICON = "ReVanced*Logo" // Can never be a valid path.
16+
17+
internal val mipmapDirectories = arrayOf(
18+
"xxxhdpi",
19+
"xxhdpi",
20+
"xhdpi",
21+
"hdpi",
22+
"mdpi",
23+
).map { "mipmap-$it" }.toTypedArray()
24+
25+
private fun formatResourceFileList(resourceNames: Array<String>) = resourceNames.joinToString("\n") { "- $it" }
26+
27+
/**
28+
* Attempts to fix unescaped and invalid characters not allowed for an Android app name.
29+
*/
30+
private fun escapeAppName(name: String): String? {
31+
// Remove ASCII control characters.
32+
val cleanedName = name.filter { it.code >= 32 }
33+
34+
// Replace invalid XML characters with escaped equivalents.
35+
val escapedName = cleanedName
36+
.replace("&", "&amp;") // Must be first to avoid double-escaping.
37+
.replace("<", "&lt;")
38+
.replace(">", "&gt;")
39+
.replace(Regex("(?<!&)\""), "&quot;")
40+
41+
// Trim empty spacing.
42+
val trimmed = escapedName.trim()
43+
44+
return trimmed.ifBlank { null }
45+
}
46+
47+
/**
48+
* Shared custom branding patch for YouTube and YT Music.
49+
*/
50+
internal fun baseCustomBrandingPatch(
51+
defaultAppName: String,
52+
appNameValues: Map<String, String>,
53+
resourceFolder: String,
54+
iconResourceFileNames: Array<String>,
55+
block: ResourcePatchBuilder.() -> Unit = {},
56+
executeBlock: ResourcePatchContext.() -> Unit = {}
57+
): ResourcePatch = resourcePatch(
58+
name = "Custom branding",
59+
description = "Applies a custom app name and icon. Defaults to \"$defaultAppName\" and the ReVanced logo.",
60+
use = false,
61+
) {
62+
val iconResourceFileNamesPng = iconResourceFileNames.map { "$it.png" }.toTypedArray<String>()
63+
64+
val appName by stringOption(
65+
key = "appName",
66+
default = defaultAppName,
67+
values = appNameValues,
68+
title = "App name",
69+
description = "The name of the app.",
70+
)
71+
72+
val iconPath by stringOption(
73+
key = "iconPath",
74+
default = REVANCED_ICON,
75+
values = mapOf("ReVanced Logo" to REVANCED_ICON),
76+
title = "App icon",
77+
description = """
78+
The icon to apply to the app.
79+
80+
If a path to a folder is provided, the folder must contain the following folders:
81+
82+
${formatResourceFileList(mipmapDirectories)}
83+
84+
Each of these folders must contain the following files:
85+
86+
${formatResourceFileList(iconResourceFileNamesPng)}
87+
""".trimIndentMultiline(),
88+
)
89+
90+
block()
91+
92+
execute {
93+
// Change the app icon and launch screen.
94+
val iconResourceGroups = mipmapDirectories.map { directory ->
95+
ResourceGroup(
96+
directory,
97+
*iconResourceFileNamesPng,
98+
)
99+
}
100+
101+
val iconPathTrimmed = iconPath!!.trim()
102+
if (iconPathTrimmed == REVANCED_ICON) {
103+
iconResourceGroups.forEach {
104+
copyResources(resourceFolder, it)
105+
}
106+
} else {
107+
val filePath = File(iconPathTrimmed)
108+
val resourceDirectory = get("res")
109+
110+
iconResourceGroups.forEach { group ->
111+
val fromDirectory = filePath.resolve(group.resourceDirectoryName)
112+
val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName)
113+
114+
group.resources.forEach { iconFileName ->
115+
Files.write(
116+
toDirectory.resolve(iconFileName).toPath(),
117+
fromDirectory.resolve(iconFileName).readBytes(),
118+
)
119+
}
120+
}
121+
}
122+
123+
// Change the app name.
124+
escapeAppName(appName!!)?.let { escapedAppName ->
125+
val newValue = "android:label=\"$escapedAppName\""
126+
127+
val manifest = get("AndroidManifest.xml")
128+
val original = manifest.readText()
129+
val replacement = original
130+
// YouTube
131+
.replace("android:label=\"@string/application_name\"", newValue)
132+
// YT Music
133+
.replace("android:label=\"@string/app_launcher_name\"", newValue)
134+
135+
if (original == replacement) {
136+
Logger.getLogger(this::class.java.name).warning(
137+
"Could not replace manifest app name"
138+
)
139+
}
140+
141+
manifest.writeText(replacement)
142+
}
143+
144+
executeBlock() // Must be after the main code to rename the new icons for YouTube 19.34+.
145+
}
146+
}

0 commit comments

Comments
 (0)