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..e89086cc68 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; @@ -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; @@ -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. * @@ -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)); } -} +} \ No newline at end of file diff --git a/patches/api/patches.api b/patches/api/patches.api index 1bed18e283..88982bc4c5 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -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 { 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..ec9fd440cb 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,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) + } +} 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..a2f0797928 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 @@ -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 @@ -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, @@ -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")) { @@ -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, 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("") + append("\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( + """ + + """ + ) + } + } + } + + append("") +}.toString() + private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/theme/SeekbarColorPatch;" val seekbarColorPatch = bytecodePatch( @@ -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(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(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() + reference?.definingClass == "Landroid/widget/ImageView;" + && reference.name == "getDrawable" + } + val checkCastIndex = indexOfFirstInstructionOrThrow(drawableIndex, Opcode.CHECK_CAST) + val drawableRegister = getInstruction(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 } } 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/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt index 0598a77bd9..f989ce16cf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -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.", @@ -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 } } 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