Skip to content
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

feat(YouTube - Theme): Apply custom seekbar color to splash screen animation #3978

Merged
Show file tree
Hide file tree
Changes from all 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 @@ -2,9 +2,12 @@

import static app.revanced.extension.shared.StringRef.str;

import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.AnimatedVectorDrawable;

import java.util.Arrays;
import java.util.Locale;

import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
Expand All @@ -16,7 +19,8 @@ public final class SeekbarColorPatch {
private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = Settings.SEEKBAR_CUSTOM_COLOR.get();

/**
* Default color of the seekbar.
* Default color of the litho seekbar.
* Differs slightly from the default custom seekbar color setting.
*/
private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;

Expand Down Expand Up @@ -72,12 +76,76 @@ public static int getSeekbarColor() {
return seekbarColor;
}

/**
* Injection point
*/
public static boolean playerSeekbarGradientEnabled(boolean original) {
if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false;

return original;
}

/**
* Injection point
*/
public static boolean useLotteLaunchSplashScreen(boolean original) {
Logger.printDebug(() -> "useLotteLaunchSplashScreen original: " + original);

if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false;

return original;
}

private static int colorChannelTo3Bits(int channel8Bits) {
final float channel3Bits = channel8Bits * 7 / 255f;

// If a color channel is near zero, then allow rounding up so values between
// 0x12 and 0x23 will show as 0x24. But always round down when the channel is
// near full saturation, otherwise rounding to nearest will cause all values
// between 0xEC and 0xFE to always show as full saturation (0xFF).
return channel3Bits < 6
? Math.round(channel3Bits)
: (int) channel3Bits;
}

private static String get9BitStyleIdentifier(int color24Bit) {
final int r3 = colorChannelTo3Bits(Color.red(color24Bit));
final int g3 = colorChannelTo3Bits(Color.green(color24Bit));
final int b3 = colorChannelTo3Bits(Color.blue(color24Bit));

return String.format(Locale.US, "splash_seekbar_color_style_%d_%d_%d", r3, g3, b3);
}

/**
* Injection point
*/
public static void setSplashAnimationDrawableTheme(AnimatedVectorDrawable vectorDrawable) {
// Alternatively a ColorMatrixColorFilter can be used to change the color of the drawable
// without using any styles, but a color filter cannot selectively change the seekbar
// while keeping the red YT logo untouched.
// Even if the seekbar color xml value is changed to a completely different color (such as green),
// a color filter still cannot be selectively applied when the drawable has more than 1 color.
try {
String seekbarStyle = get9BitStyleIdentifier(seekbarColor);
Logger.printDebug(() -> "Using splash seekbar style: " + seekbarStyle);

final int styleIdentifierDefault = Utils.getResourceIdentifier(
seekbarStyle,
"style"
);
if (styleIdentifierDefault == 0) {
throw new RuntimeException("Seekbar style not found: " + seekbarStyle);
}

Resources.Theme theme = Utils.getContext().getResources().newTheme();
theme.applyStyle(styleIdentifierDefault, true);

vectorDrawable.applyTheme(theme);
} catch (Exception ex) {
Logger.printException(() -> "setSplashAnimationDrawableTheme failure", ex);
}
}

/**
* Injection point.
*
Expand Down Expand Up @@ -189,4 +257,4 @@ private static int clamp(int value, int lower, int upper) {
private static float clamp(float value, float lower, float upper) {
return Math.max(lower, Math.min(value, upper));
}
}
}
1 change: 1 addition & 0 deletions patches/api/patches.api
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,7 @@ public final class app/revanced/patches/youtube/misc/playservice/VersionCheckPat
public static final fun is_19_36_or_greater ()Z
public static final fun is_19_41_or_greater ()Z
public static final fun is_19_43_or_greater ()Z
public static final fun is_19_46_or_greater ()Z
}

public final class app/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatchKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,17 @@ internal val lithoLinearGradientFingerprint = fingerprint {
returns("Landroid/graphics/LinearGradient;")
parameters("F", "F", "F", "F", "[I", "[F")
}

internal const val launchScreenLayoutTypeLotteFeatureFlag = 268507948L

internal val launchScreenLayoutTypeFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
returns("V")
custom { method, _ ->
val firstParameter = method.parameterTypes.firstOrNull()
// 19.25 - 19.45
(firstParameter == "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;"
|| firstParameter == "Landroid/app/Activity;") // 19.46+
&& method.containsLiteralInstruction(launchScreenLayoutTypeLotteFeatureFlag)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,24 @@ import app.revanced.patches.shared.misc.mapping.resourceMappings
import app.revanced.patches.youtube.layout.theme.lithoColorHookPatch
import app.revanced.patches.youtube.layout.theme.lithoColorOverrideHook
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_19_23_or_greater
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.misc.playservice.is_19_46_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint
import app.revanced.util.copyXmlNode
import app.revanced.util.findElementByAttributeValueOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import app.revanced.util.inputStreamFromBundledResource
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import org.w3c.dom.Element
import java.io.ByteArrayInputStream
import kotlin.use

internal var reelTimeBarPlayedColorId = -1L
private set
Expand All @@ -30,6 +39,8 @@ internal var inlineTimeBarColorizedBarPlayedColorDarkId = -1L
internal var inlineTimeBarPlayedNotHighlightedColorId = -1L
private set

internal const val splashSeekbarColorAttributeName = "splash_custom_seekbar_color"

private val seekbarColorResourcePatch = resourcePatch {
dependsOn(
settingsPatch,
Expand All @@ -51,9 +62,8 @@ private val seekbarColorResourcePatch = resourcePatch {
"inline_time_bar_played_not_highlighted_color",
]

// Edit the resume playback drawable and replace the progress bar with a custom drawable
// Modify the resume playback drawable and replace the progress bar with a custom drawable.
document("res/drawable/resume_playback_progressbar_drawable.xml").use { document ->

val layerList = document.getElementsByTagName("layer-list").item(0) as Element
val progressNode = layerList.getElementsByTagName("item").item(1) as Element
if (!progressNode.getAttributeNode("android:id").value.endsWith("progress")) {
Expand All @@ -66,9 +76,102 @@ private val seekbarColorResourcePatch = resourcePatch {
)
scaleNode.replaceChild(replacementNode, shapeNode)
}


if (!is_19_25_or_greater) {
return@execute
}

// Add attribute and styles for splash screen custom color.
// Using a style is the only way to selectively change just the seekbar fill color.
//
// Because the style colors must be hard coded for all color possibilities,
// instead of allowing 24 bit color the style is restricted to 9-bit (3 bits per color channel)
// and the style color closest to the users custom color is used for the splash screen.
arrayOf(
inputStreamFromBundledResource("seekbar/values", "attrs.xml")!! to "res/values/attrs.xml",
ByteArrayInputStream(create9BitSeekbarColorStyles().toByteArray()) to "res/values/styles.xml"
).forEach { (source, destination) ->
"resources".copyXmlNode(
document(source),
document(destination),
).close()
}

fun setSplashDrawablePathFillColor(xmlFileNames: Iterable<String>, vararg resourceNames: String) {
xmlFileNames.forEach { xmlFileName ->
document(xmlFileName).use { document ->
resourceNames.forEach { elementId ->
val element = document.childNodes.findElementByAttributeValueOrThrow(
"android:name",
elementId
)

val attribute = "android:fillColor"
if (!element.hasAttribute(attribute)) {
throw PatchException("Could not find $attribute for $elementId")
}

element.setAttribute(attribute, "?attr/$splashSeekbarColorAttributeName")
}
}
}
}

setSplashDrawablePathFillColor(
listOf(
"res/drawable/\$startup_animation_light__0.xml",
"res/drawable/\$startup_animation_dark__0.xml"
),
"_R_G_L_10_G_D_0_P_0"
)

if (!is_19_46_or_greater) {
// Resources removed in 19.46+
setSplashDrawablePathFillColor(
listOf(
"res/drawable/\$buenos_aires_animation_light__0.xml",
"res/drawable/\$buenos_aires_animation_dark__0.xml"
),
"_R_G_L_8_G_D_0_P_0"
)
}
}
}

/**
* Generate a style xml with all combinations of 9-bit colors.
*/
private fun create9BitSeekbarColorStyles(): String = StringBuilder().apply {
append("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
append("<resources>\n")

for (red in 0..7) {
for (green in 0..7) {
for (blue in 0..7) {
val name = "${red}_${green}_${blue}"

fun roundTo3BitHex(channel8Bits: Int) =
(channel8Bits * 255 / 7).toString(16).padStart(2, '0')
val r = roundTo3BitHex(red)
val g = roundTo3BitHex(green)
val b = roundTo3BitHex(blue)
val color = "#ff$r$g$b"

append(
"""
<style name="splash_seekbar_color_style_$name">
<item name="$splashSeekbarColorAttributeName">$color</item>
</style>
"""
)
}
}
}

append("</resources>")
}.toString()

private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/theme/SeekbarColorPatch;"

val seekbarColorPatch = bytecodePatch(
Expand Down Expand Up @@ -117,27 +220,73 @@ val seekbarColorPatch = bytecodePatch(
}
}

if (is_19_23_or_greater) {
playerSeekbarGradientConfigFingerprint.method.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG)
lithoColorOverrideHook(EXTENSION_CLASS_DESCRIPTOR, "getLithoColor")

if (!is_19_25_or_greater) {
return@execute
}

// 19.25+ changes

playerSeekbarGradientConfigFingerprint.method.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG)
val resultIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT)
val register = getInstruction<OneRegisterInstruction>(resultIndex).registerA

addInstructions(
resultIndex + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->playerSeekbarGradientEnabled(Z)Z
move-result v$register
"""
)
}

lithoLinearGradientFingerprint.method.addInstruction(
0,
"invoke-static/range { p4 .. p5 }, $EXTENSION_CLASS_DESCRIPTOR->setLinearGradient([I[F)V"
)


// region apply seekbar custom color to splash screen animation.

// Don't use the lotte splash screen layout if using custom seekbar.
arrayOf(
launchScreenLayoutTypeFingerprint,
mainActivityOnCreateFingerprint
).forEach { fingerprint ->
fingerprint.method.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(launchScreenLayoutTypeLotteFeatureFlag)
val resultIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT)
val register = getInstruction<OneRegisterInstruction>(resultIndex).registerA

addInstructions(
resultIndex + 1,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->playerSeekbarGradientEnabled(Z)Z
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->useLotteLaunchSplashScreen(Z)Z
move-result v$register
""",
"""
)
}
}

// Hook the splash animation drawable to set the a seekbar color theme.
mainActivityOnCreateFingerprint.method.apply {
val drawableIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == "Landroid/widget/ImageView;"
&& reference.name == "getDrawable"
}
val checkCastIndex = indexOfFirstInstructionOrThrow(drawableIndex, Opcode.CHECK_CAST)
val drawableRegister = getInstruction<OneRegisterInstruction>(checkCastIndex).registerA

lithoLinearGradientFingerprint.method.addInstruction(
0,
"invoke-static/range { p4 .. p5 }, $EXTENSION_CLASS_DESCRIPTOR->setLinearGradient([I[F)V",
addInstruction(
checkCastIndex + 1,
"invoke-static { v$drawableRegister }, $EXTENSION_CLASS_DESCRIPTOR->" +
"setSplashAnimationDrawableTheme(Landroid/graphics/drawable/AnimatedVectorDrawable;)V"
)
}

lithoColorOverrideHook(EXTENSION_CLASS_DESCRIPTOR, "getLithoColor")
// endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ val shortsAutoplayPatch = bytecodePatch(

// Main activity is used to check if app is in pip mode.
mainActivityOnCreateFingerprint.method.addInstructions(
0,
1,
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" +
"setMainActivity(Landroid/app/Activity;)V",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ var is_19_41_or_greater = false
private set
var is_19_43_or_greater = false
private set
var is_19_46_or_greater = false
private set

val versionCheckPatch = resourcePatch(
description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.",
Expand Down Expand Up @@ -68,5 +70,6 @@ val versionCheckPatch = resourcePatch(
is_19_36_or_greater = 243705000 <= playStoreServicesVersion
is_19_41_or_greater = 244305000 <= playStoreServicesVersion
is_19_43_or_greater = 244405000 <= playStoreServicesVersion
is_19_46_or_greater = 244705000 <= playStoreServicesVersion
}
}
1 change: 0 additions & 1 deletion patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ fun String.copyXmlNode(
target: Document,
): AutoCloseable {
val hostNodes = source.getElementsByTagName(this).item(0).childNodes

val destinationNode = target.getElementsByTagName(this).item(0)

for (index in 0 until hostNodes.length) {
Expand Down
Loading
Loading