feat(YouTube Music): Add Custom branding patch (#6007)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
MarcaD
2025-09-28 14:26:12 +03:00
committed by GitHub
parent 1754023dd6
commit 4c8b56f546
61 changed files with 285 additions and 126 deletions

View File

@@ -372,6 +372,10 @@ public final class app/revanced/patches/music/interaction/permanentshuffle/Perma
public static final fun getPermanentShufflePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/music/layout/branding/CustomBrandingPatchKt {
public static final fun getCustomBrandingPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/music/layout/castbutton/HideCastButtonKt {
public static final fun getHideCastButton ()Lapp/revanced/patcher/patch/BytecodePatch;
}

View File

@@ -0,0 +1,81 @@
package app.revanced.patches.music.layout.branding
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch
import app.revanced.patches.shared.misc.mapping.get
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.mapping.resourceMappings
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversed
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private val disableSplashAnimationPatch = bytecodePatch {
dependsOn(resourceMappingPatch)
execute {
// The existing YT animation usually only shows for a fraction of a second,
// and the existing animation does not match the new splash screen
// causing the original YT Music logo to momentarily flash on screen as the animation starts.
//
// Could replace the lottie animation file with our own custom animation (app_launch.json),
// but the animation is not always the same size as the launch screen and it's still
// barely shown. Instead turn off the animation entirely (app will also launch a little faster).
cairoSplashAnimationConfigFingerprint.method.apply {
val mainActivityLaunchAnimation = resourceMappings["layout", "main_activity_launch_animation"]
val literalIndex = indexOfFirstLiteralInstructionOrThrow(
mainActivityLaunchAnimation
)
val insertIndex = indexOfFirstInstructionReversed(literalIndex) {
this.opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "setContentView"
} + 1
val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
} + 1
addInstructionsWithLabels(
insertIndex,
"goto :skip_animation",
ExternalLabel("skip_animation", getInstruction(jumpIndex))
)
}
}
}
private const val APP_NAME = "YT Music ReVanced"
@Suppress("unused")
val customBrandingPatch = baseCustomBrandingPatch(
defaultAppName = APP_NAME,
appNameValues = mapOf(
"YT Music ReVanced" to APP_NAME,
"Music ReVanced" to "Music ReVanced",
"Music" to "Music",
"YT Music" to "YT Music",
),
resourceFolder = "custom-branding/music",
iconResourceFileNames = arrayOf(
"adaptiveproduct_youtube_music_2024_q4_background_color_108",
"adaptiveproduct_youtube_music_2024_q4_foreground_color_108",
"ic_launcher_release",
),
block = {
dependsOn(disableSplashAnimationPatch)
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52",
"8.10.52"
)
)
}
)

View File

@@ -0,0 +1,12 @@
package app.revanced.patches.music.layout.branding
import app.revanced.patcher.fingerprint
import app.revanced.patches.music.shared.YOUTUBE_MUSIC_MAIN_ACTIVITY_CLASS_TYPE
internal val cairoSplashAnimationConfigFingerprint = fingerprint {
returns("V")
parameters("Landroid/os/Bundle;")
custom { method, classDef ->
method.name == "onCreate" && method.definingClass == YOUTUBE_MUSIC_MAIN_ACTIVITY_CLASS_TYPE
}
}

View File

@@ -0,0 +1,146 @@
package app.revanced.patches.shared.layout.branding
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.ResourcePatchBuilder
import app.revanced.patcher.patch.ResourcePatchContext
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.util.ResourceGroup
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyResources
import java.io.File
import java.nio.file.Files
import java.util.logging.Logger
private const val REVANCED_ICON = "ReVanced*Logo" // Can never be a valid path.
internal val mipmapDirectories = arrayOf(
"xxxhdpi",
"xxhdpi",
"xhdpi",
"hdpi",
"mdpi",
).map { "mipmap-$it" }.toTypedArray()
private fun formatResourceFileList(resourceNames: Array<String>) = resourceNames.joinToString("\n") { "- $it" }
/**
* Attempts to fix unescaped and invalid characters not allowed for an Android app name.
*/
private fun escapeAppName(name: String): String? {
// Remove ASCII control characters.
val cleanedName = name.filter { it.code >= 32 }
// Replace invalid XML characters with escaped equivalents.
val escapedName = cleanedName
.replace("&", "&amp;") // Must be first to avoid double-escaping.
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace(Regex("(?<!&)\""), "&quot;")
// Trim empty spacing.
val trimmed = escapedName.trim()
return trimmed.ifBlank { null }
}
/**
* Shared custom branding patch for YouTube and YT Music.
*/
internal fun baseCustomBrandingPatch(
defaultAppName: String,
appNameValues: Map<String, String>,
resourceFolder: String,
iconResourceFileNames: Array<String>,
block: ResourcePatchBuilder.() -> Unit = {},
executeBlock: ResourcePatchContext.() -> Unit = {}
): ResourcePatch = resourcePatch(
name = "Custom branding",
description = "Applies a custom app name and icon. Defaults to \"$defaultAppName\" and the ReVanced logo.",
use = false,
) {
val iconResourceFileNamesPng = iconResourceFileNames.map { "$it.png" }.toTypedArray<String>()
val appName by stringOption(
key = "appName",
default = defaultAppName,
values = appNameValues,
title = "App name",
description = "The name of the app.",
)
val iconPath by stringOption(
key = "iconPath",
default = REVANCED_ICON,
values = mapOf("ReVanced Logo" to REVANCED_ICON),
title = "App icon",
description = """
The icon to apply to the app.
If a path to a folder is provided, the folder must contain the following folders:
${formatResourceFileList(mipmapDirectories)}
Each of these folders must contain the following files:
${formatResourceFileList(iconResourceFileNamesPng)}
""".trimIndentMultiline(),
)
block()
execute {
// Change the app icon and launch screen.
val iconResourceGroups = mipmapDirectories.map { directory ->
ResourceGroup(
directory,
*iconResourceFileNamesPng,
)
}
val iconPathTrimmed = iconPath!!.trim()
if (iconPathTrimmed == REVANCED_ICON) {
iconResourceGroups.forEach {
copyResources(resourceFolder, it)
}
} else {
val filePath = File(iconPathTrimmed)
val resourceDirectory = get("res")
iconResourceGroups.forEach { group ->
val fromDirectory = filePath.resolve(group.resourceDirectoryName)
val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName)
group.resources.forEach { iconFileName ->
Files.write(
toDirectory.resolve(iconFileName).toPath(),
fromDirectory.resolve(iconFileName).readBytes(),
)
}
}
}
// Change the app name.
escapeAppName(appName!!)?.let { escapedAppName ->
val newValue = "android:label=\"$escapedAppName\""
val manifest = get("AndroidManifest.xml")
val original = manifest.readText()
val replacement = original
// YouTube
.replace("android:label=\"@string/application_name\"", newValue)
// YT Music
.replace("android:label=\"@string/app_launcher_name\"", newValue)
if (original == replacement) {
Logger.getLogger(this::class.java.name).warning(
"Could not replace manifest app name"
)
}
manifest.writeText(replacement)
}
executeBlock() // Must be after the main code to rename the new icons for YouTube 19.34+.
}
}

View File

@@ -1,46 +1,34 @@
package app.revanced.patches.youtube.layout.branding
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.util.ResourceGroup
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyResources
import java.io.File
import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch
import app.revanced.patches.shared.layout.branding.mipmapDirectories
import java.nio.file.Files
private const val REVANCED_ICON = "ReVanced*Logo" // Can never be a valid path.
private const val APP_NAME = "YouTube ReVanced"
private val iconResourceFileNames = arrayOf(
"adaptiveproduct_youtube_background_color_108",
"adaptiveproduct_youtube_foreground_color_108",
"ic_launcher",
"ic_launcher_round",
).map { "$it.png" }.toTypedArray()
private val iconResourceFileNamesNew = mapOf(
private val youtubeIconResourceFileNames_19_34 = mapOf(
"adaptiveproduct_youtube_foreground_color_108" to "adaptiveproduct_youtube_2024_q4_foreground_color_108",
"adaptiveproduct_youtube_background_color_108" to "adaptiveproduct_youtube_2024_q4_background_color_108",
)
private val mipmapDirectories = arrayOf(
"xxxhdpi",
"xxhdpi",
"xhdpi",
"hdpi",
"mdpi",
).map { "mipmap-$it" }
@Suppress("unused")
val customBrandingPatch = resourcePatch(
name = "Custom branding",
description = "Applies a custom app name and icon. Defaults to \"YouTube ReVanced\" and the ReVanced logo.",
use = false,
) {
dependsOn(versionCheckPatch)
val customBrandingPatch = baseCustomBrandingPatch(
defaultAppName = APP_NAME,
appNameValues = mapOf(
"YouTube ReVanced" to APP_NAME,
"YT ReVanced" to "YT ReVanced",
"YT" to "YT",
"YouTube" to "YouTube",
),
resourceFolder = "custom-branding/youtube",
iconResourceFileNames = arrayOf(
"adaptiveproduct_youtube_background_color_108",
"adaptiveproduct_youtube_foreground_color_108",
"ic_launcher",
"ic_launcher_round",
),
block = {
compatibleWith(
"com.google.android.youtube"(
"19.34.42",
@@ -49,74 +37,15 @@ val customBrandingPatch = resourcePatch(
"20.14.43",
)
)
},
val appName by stringOption(
key = "appName",
default = APP_NAME,
values = mapOf(
"YouTube ReVanced" to APP_NAME,
"YT ReVanced" to "YT ReVanced",
"YT" to "YT",
"YouTube" to "YouTube",
),
title = "App name",
description = "The name of the app.",
)
val icon by stringOption(
key = "iconPath",
default = REVANCED_ICON,
values = mapOf("ReVanced Logo" to REVANCED_ICON),
title = "App icon",
description = """
The icon to apply to the app.
If a path to a folder is provided, the folder must contain the following folders:
${mipmapDirectories.joinToString("\n") { "- $it" }}
Each of these folders must contain the following files:
${iconResourceFileNames.joinToString("\n") { "- $it" }}
""".trimIndentMultiline(),
)
execute {
icon?.let { icon ->
// Change the app icon.
mipmapDirectories.map { directory ->
ResourceGroup(
directory,
*iconResourceFileNames,
)
}.let { resourceGroups ->
if (icon != REVANCED_ICON) {
val path = File(icon)
val resourceDirectory = get("res")
resourceGroups.forEach { group ->
val fromDirectory = path.resolve(group.resourceDirectoryName)
val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName)
group.resources.forEach { iconFileName ->
Files.write(
toDirectory.resolve(iconFileName).toPath(),
fromDirectory.resolve(iconFileName).readBytes(),
)
}
}
} else {
resourceGroups.forEach { copyResources("custom-branding", it) }
}
}
if (is_19_34_or_greater) {
executeBlock = {
val resourceDirectory = get("res")
mipmapDirectories.forEach { directory ->
val targetDirectory = resourceDirectory.resolve(directory)
iconResourceFileNamesNew.forEach { (old, new) ->
youtubeIconResourceFileNames_19_34.forEach { (old, new) ->
val oldFile = targetDirectory.resolve("$old.png")
val newFile = targetDirectory.resolve("$new.png")
@@ -124,18 +53,4 @@ val customBrandingPatch = resourcePatch(
}
}
}
}
appName?.let { name ->
// Change the app name.
val manifest = get("AndroidManifest.xml")
manifest.writeText(
manifest.readText()
.replace(
"android:label=\"@string/application_name",
"android:label=\"$name",
),
)
}
}
}

View File

@@ -38,6 +38,7 @@ var is_19_32_or_greater = false
@Deprecated("19.34.42 is the lowest supported version")
var is_19_33_or_greater = false
private set
@Deprecated("19.34.42 is the lowest supported version")
var is_19_34_or_greater = false
private set
var is_19_35_or_greater = false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB