Skip to content

Commit

Permalink
feat(YouTube - Theme): Apply custom seekbar color to splash screen an…
Browse files Browse the repository at this point in the history
…imation
  • Loading branch information
LisoUseInAIKyrios committed Nov 24, 2024
1 parent fac5484 commit cca8eaa
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 5 deletions.
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 @@ -18,7 +21,7 @@ public final class SeekbarColorPatch {
/**
* Default color of the seekbar.
*/
private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;
private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0033;

/**
* Default colors of the gradient seekbar.
Expand Down Expand Up @@ -72,12 +75,65 @@ 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);

// The lotte animation is the latest cairo style,
// and use that if not using a custom color
return seekbarColor == ORIGINAL_SEEKBAR_COLOR;
}

private static String get8BitStyleIdentifier(int color24Bit) {
// Convert to nearest 3-3-2 bit depth.
final int r3 = Math.round(Color.red(color24Bit) * 7 / 255f);
final int g3 = Math.round(Color.green(color24Bit) * 7 / 255f);
final int b2 = Math.round(Color.blue(color24Bit) * 3 / 255f);

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

/**
* 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 = get8BitStyleIdentifier(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 +245,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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,33 @@ 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")
parameters("Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;", "L", "L", "L", "L", "L", "L", "L")
literal { launchScreenLayoutTypeLotteFeatureFlag }
}

internal val launchScreenOptimizedFeatureFlagFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("Z")
parameters("L")
custom { method, _ ->
method.containsLiteralInstruction(268639016)
&& method.containsLiteralInstruction(4)
}
}

internal val launchScreenBuenosAiresFeatureFlagFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("Z")
parameters("L")
custom { method, _ ->
method.containsLiteralInstruction(268639016)
&& method.containsLiteralInstruction(1)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,24 @@ 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.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 app.revanced.util.returnEarly
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 Down Expand Up @@ -53,7 +63,6 @@ private val seekbarColorResourcePatch = resourcePatch {

// Edit 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 +75,97 @@ private val seekbarColorResourcePatch = resourcePatch {
)
scaleNode.replaceChild(replacementNode, shapeNode)
}


if (is_19_25_or_greater) {
// 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 8-bit (3-3-2 color depth)
// 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(create8BitSeekbarColorStyles().toByteArray())
to "res/values/styles.xml"
).forEach { (source, destination) ->
"resources".copyXmlNode(
document(source),
document(destination),
).close()
}

fun setSplashDrawablePathFillColor(xmlFileNames: Iterable<String>, resourceNames: Iterable<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/splash_custom_seekbar_color")
}
}
}
}

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

setSplashDrawablePathFillColor(
listOf(
"res/drawable/\$buenos_aires_animation_light__0.xml",
"res/drawable/\$buenos_aires_animation_dark__0.xml"
),
listOf("_R_G_L_8_G_D_0_P_0")
)
}
}
}

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

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

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

append(
"""
<style name="splash_seekbar_color_style_$name">
<item name="splash_custom_seekbar_color">$color</item>
</style>
""".trimIndent()
)
}
}
}

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

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

val seekbarColorPatch = bytecodePatch(
Expand Down Expand Up @@ -139,5 +236,55 @@ val seekbarColorPatch = bytecodePatch(
}

lithoColorOverrideHook(EXTENSION_CLASS_DESCRIPTOR, "getLithoColor")


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

// Development changes to force the different launch screens to be used.
// All appear to be nearly identical and there is no strong reason to force any particular one.
if (false) {
// 3 combinations of each flag individually on and with both flags off.
launchScreenBuenosAiresFeatureFlagFingerprint.method.returnEarly(false)
launchScreenOptimizedFeatureFlagFingerprint.method.returnEarly(true)
}

// 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->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

addInstruction(
checkCastIndex + 1,
"invoke-static { v$drawableRegister }, $EXTENSION_CLASS_DESCRIPTOR->" +
"setSplashAnimationDrawableTheme(Landroid/graphics/drawable/AnimatedVectorDrawable;)V"
)
}

// 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
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
4 changes: 4 additions & 0 deletions patches/src/main/resources/seekbar/values/attrs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr format="reference|color" name="splash_custom_seekbar_color"/>
</resources>

0 comments on commit cca8eaa

Please sign in to comment.