diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java index aea5c227c8..e28b260a19 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java @@ -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; @@ -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. @@ -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. * @@ -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)); } -} +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt index 67a6b017d5..6404a3ebbd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt @@ -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) + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt index 49a5444ab1..a4e4e7189f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt @@ -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 @@ -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")) { @@ -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, resourceNames: Iterable) { + 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("\n\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( + """ + + + """.trimIndent() + ) + } + } + } + + append("") +}.toString() + private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/theme/SeekbarColorPatch;" val seekbarColorPatch = bytecodePatch( @@ -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(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() + reference?.definingClass == "Landroid/widget/ImageView;" + && reference.name == "getDrawable" + } + val checkCastIndex = indexOfFirstInstructionOrThrow(drawableIndex, Opcode.CHECK_CAST) + val drawableRegister = getInstruction(checkCastIndex).registerA + + addInstruction( + checkCastIndex + 1, + "invoke-static { v$drawableRegister }, $EXTENSION_CLASS_DESCRIPTOR->" + + "setSplashAnimationDrawableTheme(Landroid/graphics/drawable/AnimatedVectorDrawable;)V" + ) + } + + // endregion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 72c81db4c9..f1bb0b37a4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -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", ) diff --git a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt index 3faf55a8fa..7a1496afc3 100644 --- a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt @@ -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) { diff --git a/patches/src/main/resources/seekbar/values/attrs.xml b/patches/src/main/resources/seekbar/values/attrs.xml new file mode 100644 index 0000000000..2bf349f0d9 --- /dev/null +++ b/patches/src/main/resources/seekbar/values/attrs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file