diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java index b7ceb2b986..31153a4d73 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java @@ -6,11 +6,19 @@ import android.graphics.Color; import android.graphics.drawable.AnimatedVectorDrawable; +import com.airbnb.lottie.LottieAnimationView; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Locale; +import java.util.Scanner; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -93,17 +101,6 @@ public static int getSeekbarColor() { return customSeekbarColor; } - /** - * 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; @@ -127,6 +124,17 @@ private static String get9BitStyleIdentifier(int color24Bit) { /** * Injection point */ + public static boolean useLotteLaunchSplashScreen(boolean original) { + // This method is only used for development purposes to force the old style launch screen. + // Forcing this off on some devices can cause unexplained startup crashes, + // where the lottie animation is still used even though this condition appears to bypass it. + return original; // false = drawable style, true = lottie style. + } + + /** + * Injection point. + * Old drawable style launch screen. + */ 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 @@ -134,6 +142,8 @@ public static void setSplashAnimationDrawableTheme(AnimatedVectorDrawable vector // 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 { + // Must set the color even if custom seekbar is off, + // because the xml color was replaced with a themed value. String seekbarStyle = get9BitStyleIdentifier(customSeekbarColor); Logger.printDebug(() -> "Using splash seekbar style: " + seekbarStyle); @@ -154,6 +164,77 @@ public static void setSplashAnimationDrawableTheme(AnimatedVectorDrawable vector } } + /** + * Injection point. + * Modern Lottie style animation. + */ + public static void setSplashAnimationLottie(LottieAnimationView view, int resourceId) { + try { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED) { + view.patch_setAnimation(resourceId); + return; + } + + //noinspection ConstantConditions + if (false) { // Set true to force slow animation for development. + final int longAnimation = Utils.getResourceIdentifier( + Utils.isDarkModeEnabled(Utils.getContext()) + ? "startup_animation_5s_30fps_dark" + : "startup_animation_5s_30fps_light", + "raw"); + if (longAnimation != 0) { + resourceId = longAnimation; + } + } + + // Must specify primary key name otherwise the morphing YT logo color is also changed. + String originalKey = "\"k\":"; + String originalPrimary = originalKey + "[1,0,0.2,1]"; + String originalAccent = originalKey + "[1,0.152941176471,0.56862745098,1]"; + + String replacementPrimary = originalKey + getColorStringArray(customSeekbarColor); + String replacementAccent = originalKey + getColorStringArray(customSeekbarColorGradient[1]); + + String json = loadRawResourceAsString(resourceId); + if (json == null) { + return; // Should never happen. + } + + if (BaseSettings.DEBUG.get() && (!json.contains(originalPrimary) || !json.contains(originalAccent))) { + String jsonFinal = json; + Logger.printException(() -> "Could not replace launch animation colors: " + jsonFinal); + } + + Logger.printDebug(() -> "Replacing Lottie animation JSON"); + json = json.replace(originalPrimary, replacementPrimary); + json = json.replace(originalAccent, replacementAccent); + + // cacheKey is not needed since the animation will not be reused. + view.patch_setAnimation(new ByteArrayInputStream(json.getBytes()), null); + } catch (Exception ex) { + Logger.printException(() -> "setSplashAnimationLottie failure", ex); + } + } + + private static String getColorStringArray(int color) { + return Arrays.toString(new double[]{ + Color.red(color) / 255.0, + Color.green(color) / 255.0, + Color.blue(color) / 255.0, + Color.alpha(color) / 255.0 + }); + } + + private static String loadRawResourceAsString(int resourceId) { + try (InputStream inputStream = Utils.getContext().getResources().openRawResource(resourceId); + Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name()).useDelimiter("\\A")) { + return scanner.next(); + } catch (IOException e) { + Logger.printException(() -> "Could not load resource: " + resourceId); + return null; + } + } + /** * Injection point. */ diff --git a/extensions/youtube/stub/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/extensions/youtube/stub/src/main/java/com/airbnb/lottie/LottieAnimationView.java new file mode 100644 index 0000000000..f9c148fc4a --- /dev/null +++ b/extensions/youtube/stub/src/main/java/com/airbnb/lottie/LottieAnimationView.java @@ -0,0 +1,15 @@ +package com.airbnb.lottie; + +import java.io.InputStream; + +@SuppressWarnings("unused") +public class LottieAnimationView { + + public void patch_setAnimation(InputStream stream, String cacheKey) { + throw new RuntimeException("stub"); + } + + public final void patch_setAnimation(int rawResInt) { + throw new RuntimeException("stub"); + } +} 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 5735f57d58..4f7c8eaba3 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 @@ -2,9 +2,12 @@ package app.revanced.patches.youtube.layout.seekbar import app.revanced.patcher.fingerprint import app.revanced.util.containsLiteralInstruction +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction import app.revanced.util.literal import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal val fullscreenSeekbarThumbnailsFingerprint = fingerprint { returns("Z") @@ -109,3 +112,52 @@ internal val launchScreenLayoutTypeFingerprint = fingerprint { && method.containsLiteralInstruction(launchScreenLayoutTypeLotteFeatureFlag) } } + +internal const val LOTTIE_ANIMATION_VIEW_CLASS_TYPE = "Lcom/airbnb/lottie/LottieAnimationView;" + +internal val lottieAnimationViewSetAnimationIntFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters("I") + returns("V") + custom { methodDef, classDef -> + classDef.type == LOTTIE_ANIMATION_VIEW_CLASS_TYPE && methodDef.indexOfFirstInstruction { + val reference = getReference() + reference?.definingClass == "Lcom/airbnb/lottie/LottieAnimationView;" + && reference.name == "isInEditMode" + } >= 0 + } +} + +internal val lottieAnimationViewSetAnimationStreamFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters("L") + returns("V") + custom { methodDef, classDef -> + classDef.type == LOTTIE_ANIMATION_VIEW_CLASS_TYPE && methodDef.indexOfFirstInstruction { + val reference = getReference() + reference?.definingClass == "Ljava/util/Set;" + && reference.name == "add" + } >= 0 && methodDef.containsLiteralInstruction(0) + } +} + +internal val lottieCompositionFactoryZipFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + parameters("Landroid/content/Context;", "Ljava/lang/String;", "Ljava/lang/String;") + returns("L") + strings(".zip", ".lottie") +} + +/** + * Resolves using class found in [lottieCompositionFactoryZipFingerprint]. + * + * [Original method](https://github.com/airbnb/lottie-android/blob/26ad8bab274eac3f93dccccfa0cafc39f7408d13/lottie/src/main/java/com/airbnb/lottie/LottieCompositionFactory.java#L386) + */ +internal val lottieCompositionFactoryFromJsonInputStreamFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + parameters("Ljava/io/InputStream;", "Ljava/lang/String;") + returns("L") + literal { 2 } +} + + 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 19a2b5397f..35567b829d 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 @@ -3,10 +3,12 @@ package app.revanced.patches.youtube.layout.seekbar import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patches.shared.misc.mapping.get import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.mapping.resourceMappings @@ -22,15 +24,21 @@ 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.findInstructionIndicesReversedOrThrow import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import app.revanced.util.inputStreamFromBundledResource import app.revanced.util.insertFeatureFlagBooleanOverride +import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction 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 com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter import org.w3c.dom.Element import java.io.ByteArrayInputStream import kotlin.use @@ -307,7 +315,11 @@ val seekbarColorPatch = bytecodePatch( // region apply seekbar custom color to splash screen animation. - // Don't use the lotte splash screen layout if using custom seekbar. + if (!is_19_34_or_greater) { + return@execute // 19.25 does not have a cairo launch animation. + } + + // Add development hook to force old drawable splash animation. arrayOf( launchScreenLayoutTypeFingerprint, mainActivityOnCreateFingerprint @@ -318,7 +330,7 @@ val seekbarColorPatch = bytecodePatch( ) } - // Hook the splash animation drawable to set the a seekbar color theme. + // Hook the splash animation to set the a seekbar color. mainActivityOnCreateFingerprint.method.apply { val drawableIndex = indexOfFirstInstructionOrThrow { val reference = getReference() @@ -333,6 +345,87 @@ val seekbarColorPatch = bytecodePatch( "invoke-static { v$drawableRegister }, $EXTENSION_CLASS_DESCRIPTOR->" + "setSplashAnimationDrawableTheme(Landroid/graphics/drawable/AnimatedVectorDrawable;)V" ) + + // Replace the Lottie animation view setAnimation(int) call. + val setAnimationIntMethodName = lottieAnimationViewSetAnimationIntFingerprint.originalMethod.name + + findInstructionIndicesReversedOrThrow { + val reference = getReference() + reference?.definingClass == "Lcom/airbnb/lottie/LottieAnimationView;" + && reference.name == setAnimationIntMethodName + }.forEach { index -> + val instruction = getInstruction(index) + + replaceInstruction( + index, + "invoke-static { v${instruction.registerC}, v${instruction.registerD} }, " + + "$EXTENSION_CLASS_DESCRIPTOR->setSplashAnimationLottie" + + "(Lcom/airbnb/lottie/LottieAnimationView;I)V" + ) + } + } + + + // Add non obfuscated method aliases for `setAnimation(int)` + // and `setAnimation(InputStream, String)` so extension code can call them. + lottieAnimationViewSetAnimationIntFingerprint.classDef.methods.apply { + val addedMethodName = "patch_setAnimation" + val setAnimationIntName = lottieAnimationViewSetAnimationIntFingerprint.originalMethod.name + + add(ImmutableMethod( + LOTTIE_ANIMATION_VIEW_CLASS_TYPE, + addedMethodName, + listOf(ImmutableMethodParameter("I", null, null)), + "V", + AccessFlags.PUBLIC.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + addInstructions( + """ + invoke-virtual { p0, p1 }, Lcom/airbnb/lottie/LottieAnimationView;->$setAnimationIntName(I)V + return-void + """ + ) + }) + + val factoryStreamClass : CharSequence + val factoryStreamName : CharSequence + val factoryStreamReturnType : CharSequence + lottieCompositionFactoryFromJsonInputStreamFingerprint.match( + lottieCompositionFactoryZipFingerprint.originalClassDef + ).originalMethod.apply { + factoryStreamClass = definingClass + factoryStreamName = name + factoryStreamReturnType = returnType + } + + val setAnimationStreamName = lottieAnimationViewSetAnimationStreamFingerprint + .originalMethod.name + + add(ImmutableMethod( + LOTTIE_ANIMATION_VIEW_CLASS_TYPE, + addedMethodName, + listOf( + ImmutableMethodParameter("Ljava/io/InputStream;", null, null), + ImmutableMethodParameter("Ljava/lang/String;", null, null) + ), + "V", + AccessFlags.PUBLIC.value, + null, + null, + MutableMethodImplementation(4), + ).toMutable().apply { + addInstructions( + """ + invoke-static { p1, p2 }, $factoryStreamClass->$factoryStreamName(Ljava/io/InputStream;Ljava/lang/String;)$factoryStreamReturnType + move-result-object v0 + invoke-virtual { p0, v0}, Lcom/airbnb/lottie/LottieAnimationView;->$setAnimationStreamName($factoryStreamReturnType)V + return-void + """ + ) + }) } // endregion