Skip to content

Commit

Permalink
fix(YouTube - Theme): Use custom seekbar color for cairo startup anim…
Browse files Browse the repository at this point in the history
…ation (#4399)
  • Loading branch information
LisoUseInAIKyrios authored Feb 2, 2025
1 parent 8891f98 commit 1cba294
Show file tree
Hide file tree
Showing 4 changed files with 254 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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;

Expand All @@ -127,13 +124,26 @@ 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
// 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 {
// 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);

Expand All @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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<MethodReference>()
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<MethodReference>()
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 }
}


Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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<MethodReference>()
Expand All @@ -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<MethodReference>()
reference?.definingClass == "Lcom/airbnb/lottie/LottieAnimationView;"
&& reference.name == setAnimationIntMethodName
}.forEach { index ->
val instruction = getInstruction<FiveRegisterInstruction>(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
Expand Down

0 comments on commit 1cba294

Please sign in to comment.