From 0158c5b4a071180f854e2de92a76af115207c849 Mon Sep 17 00:00:00 2001
From: ChsBuffer <33744752+chsbuffer@users.noreply.github.com>
Date: Sun, 28 Jul 2024 14:16:34 +0800
Subject: [PATCH] Youtube: - VideoAds - BackgroundPlayback -
RemoveTrackingQueryParameter - HideAds - LithoFilter
---
README.md | 26 +
app/build.gradle.kts | 1 +
.../revanced/integrations/shared/Logger.java | 157 +++++
.../revanced/integrations/shared/Utils.java | 193 ++++++
.../shared/settings/BaseSettings.java | 11 +
.../shared/settings/BooleanSetting.java | 13 +
.../integrations/shared/settings/Setting.java | 18 +
.../integrations/youtube/ByteTrieSearch.java | 45 ++
.../youtube/StringTrieSearch.java | 34 ++
.../integrations/youtube/TrieSearch.java | 412 +++++++++++++
.../youtube/patches/components/AdsFilter.java | 207 +++++++
.../patches/components/LithoFilterPatch.java | 557 ++++++++++++++++++
.../youtube/settings/Settings.java | 27 +
.../github/chsbuffer/revancedxposed/Cache.kt | 109 ++++
.../github/chsbuffer/revancedxposed/Helper.kt | 23 +
.../chsbuffer/revancedxposed/MainHook.kt | 9 +
.../chsbuffer/revancedxposed/MusicHook.kt | 78 +--
.../chsbuffer/revancedxposed/YoutubeHook.kt | 357 +++++++++++
app/src/main/res/values/arrays.xml | 1 +
gradle/libs.versions.toml | 4 +-
20 files changed, 2214 insertions(+), 68 deletions(-)
create mode 100644 README.md
create mode 100644 app/src/main/java/app/revanced/integrations/shared/Logger.java
create mode 100644 app/src/main/java/app/revanced/integrations/shared/Utils.java
create mode 100644 app/src/main/java/app/revanced/integrations/shared/settings/BaseSettings.java
create mode 100644 app/src/main/java/app/revanced/integrations/shared/settings/BooleanSetting.java
create mode 100644 app/src/main/java/app/revanced/integrations/shared/settings/Setting.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/ByteTrieSearch.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/StringTrieSearch.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java
create mode 100644 app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
create mode 100644 app/src/main/java/io/github/chsbuffer/revancedxposed/Cache.kt
create mode 100644 app/src/main/java/io/github/chsbuffer/revancedxposed/Helper.kt
create mode 100644 app/src/main/java/io/github/chsbuffer/revancedxposed/YoutubeHook.kt
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e2e5e2a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# Compatibility & Patches
+
+## Youtube Music
+Youtube Music tested version: 7.11.50
+Patches:
+- HideMusicVideoAds
+- MinimizedPlayback
+- RemoveUpgradeButton
+- HideGetPremium
+- EnableExclusiveAudioPlayback
+
+## Youtube
+Youtube tested version: 19.29.37
+Patches:
+- VideoAds
+- BackgroundPlayback
+- RemoveTrackingQueryParameter
+- HideAds
+- LithoFilter
+
+# Credit
+
+[DexKit](/~https://github.com/LuckyPray/DexKit)
+[ReVanced Integrations](/~https://github.com/ReVanced/revanced-integrations)
+[Revanced Patcher](/~https://github.com/ReVanced/revanced-patcher)
+[Revanced Patches](/~https://github.com/ReVanced/revanced-patches)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 4f2d96b..c7f05ab 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -51,5 +51,6 @@ android {
dependencies {
implementation(libs.dexkit)
+ implementation(libs.annotation)
compileOnly(libs.xposed)
}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/shared/Logger.java b/app/src/main/java/app/revanced/integrations/shared/Logger.java
new file mode 100644
index 0000000..2588505
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/shared/Logger.java
@@ -0,0 +1,157 @@
+package app.revanced.integrations.shared;
+
+import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG;
+import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_STACKTRACE;
+import static app.revanced.integrations.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import app.revanced.integrations.shared.settings.BaseSettings;
+
+public class Logger {
+
+ /**
+ * Log messages using lambdas.
+ */
+ public interface LogMessage {
+ @NonNull
+ String buildMessageString();
+
+ /**
+ * @return For outer classes, this returns {@link Class#getSimpleName()}.
+ * For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
+ *
+ * For example, each of these classes return 'SomethingView':
+ *
+ * com.company.SomethingView
+ * com.company.SomethingView$StaticClass
+ * com.company.SomethingView$1
+ *
+ */
+ private String findOuterClassSimpleName() {
+ var selfClass = this.getClass();
+
+ String fullClassName = selfClass.getName();
+ final int dollarSignIndex = fullClassName.indexOf('$');
+ if (dollarSignIndex < 0) {
+ return selfClass.getSimpleName(); // Already an outer class.
+ }
+
+ // Class is inner, static, or anonymous.
+ // Parse the simple name full name.
+ // A class with no package returns index of -1, but incrementing gives index zero which is correct.
+ final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
+ return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
+ }
+ }
+
+ private static final String REVANCED_LOG_PREFIX = "revanced: ";
+
+ /**
+ * Logs debug messages under the outer class name of the code calling this method.
+ * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
+ * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
+ */
+ public static void printDebug(@NonNull LogMessage message) {
+ if (DEBUG.get()) {
+ var messageString = message.buildMessageString();
+
+ if (DEBUG_STACKTRACE.get()) {
+ var builder = new StringBuilder(messageString);
+ var sw = new StringWriter();
+ new Throwable().printStackTrace(new PrintWriter(sw));
+
+ builder.append('\n').append(sw);
+ messageString = builder.toString();
+ }
+
+ Log.d(REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(), messageString);
+ }
+ }
+
+ /**
+ * Logs information messages using the outer class name of the code calling this method.
+ */
+ public static void printInfo(@NonNull LogMessage message) {
+ printInfo(message, null);
+ }
+
+ /**
+ * Logs information messages using the outer class name of the code calling this method.
+ */
+ public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
+ String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
+ String logMessage = message.buildMessageString();
+ if (ex == null) {
+ Log.i(logTag, logMessage);
+ } else {
+ Log.i(logTag, logMessage, ex);
+ }
+ }
+
+ /**
+ * Logs exceptions under the outer class name of the code calling this method.
+ */
+ public static void printException(@NonNull LogMessage message) {
+ printException(message, null, null);
+ }
+
+ /**
+ * Logs exceptions under the outer class name of the code calling this method.
+ */
+ public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
+ printException(message, ex, null);
+ }
+
+ /**
+ * Logs exceptions under the outer class name of the code calling this method.
+ *
+ * If the calling code is showing it's own error toast,
+ * instead use {@link #printInfo(LogMessage, Exception)}
+ *
+ * @param message log message
+ * @param ex exception (optional)
+ * @param userToastMessage user specific toast message to show instead of the log message (optional)
+ */
+ public static void printException(@NonNull LogMessage message, @Nullable Throwable ex,
+ @Nullable String userToastMessage) {
+ String messageString = message.buildMessageString();
+ String outerClassSimpleName = message.findOuterClassSimpleName();
+ String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
+ if (ex == null) {
+ Log.e(logMessage, messageString);
+ } else {
+ Log.e(logMessage, messageString, ex);
+ }
+ if (DEBUG_TOAST_ON_ERROR.get()) {
+ String toastMessageToDisplay = (userToastMessage != null)
+ ? userToastMessage
+ : outerClassSimpleName + ": " + messageString;
+ Utils.showToastLong(toastMessageToDisplay);
+ }
+ }
+
+ /**
+ * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
+ * Normally this method should not be used.
+ */
+ public static void initializationInfo(@NonNull Class> callingClass, @NonNull String message) {
+ Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
+ }
+
+ /**
+ * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
+ * Normally this method should not be used.
+ */
+ public static void initializationException(@NonNull Class> callingClass, @NonNull String message,
+ @Nullable Exception ex) {
+ Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java
new file mode 100644
index 0000000..281d89c
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java
@@ -0,0 +1,193 @@
+package app.revanced.integrations.shared;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.preference.Preference;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceScreen;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+import android.widget.Toolbar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.text.Bidi;
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class Utils {
+
+ @SuppressLint("StaticFieldLeak")
+ private static Context context;
+
+ public static void setContext(Context appContext) {
+ context = appContext;
+ }
+
+ public static Context getContext() {
+ return context;
+ }
+
+ /**
+ * General purpose pool for network calls and other background tasks.
+ * All tasks run at max thread priority.
+ */
+ private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
+ 3, // 3 threads always ready to go
+ Integer.MAX_VALUE,
+ 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
+ TimeUnit.SECONDS,
+ new SynchronousQueue<>(),
+ r -> { // ThreadFactory
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MAX_PRIORITY); // run at max priority
+ return t;
+ });
+
+ public static void runOnBackgroundThread(@NonNull Runnable task) {
+ backgroundThreadPool.execute(task);
+ }
+
+ @NonNull
+ public static Future submitOnBackgroundThread(@NonNull Callable call) {
+ return backgroundThreadPool.submit(call);
+ }
+
+
+ /**
+ * Safe to call from any thread
+ */
+ public static void showToastShort(@NonNull String messageToToast) {
+ showToast(messageToToast, Toast.LENGTH_SHORT);
+ }
+
+ /**
+ * Safe to call from any thread
+ */
+ public static void showToastLong(@NonNull String messageToToast) {
+ showToast(messageToToast, Toast.LENGTH_LONG);
+ }
+
+ private static void showToast(@NonNull String messageToToast, int toastDuration) {
+ Objects.requireNonNull(messageToToast);
+ runOnMainThreadNowOrLater(() -> {
+ if (context == null) {
+ Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
+ } else {
+ Logger.printDebug(() -> "Showing toast: " + messageToToast);
+ Toast.makeText(context, messageToToast, toastDuration).show();
+ }
+ }
+ );
+ }
+
+ /**
+ * Automatically logs any exceptions the runnable throws.
+ *
+ * @see #runOnMainThreadNowOrLater(Runnable)
+ */
+ public static void runOnMainThread(@NonNull Runnable runnable) {
+ runOnMainThreadDelayed(runnable, 0);
+ }
+
+ /**
+ * Automatically logs any exceptions the runnable throws
+ */
+ public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
+ Runnable loggingRunnable = () -> {
+ try {
+ runnable.run();
+ } catch (Exception ex) {
+ Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex);
+ }
+ };
+ new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
+ }
+
+ /**
+ * If called from the main thread, the code is run immediately.
+ * If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
+ */
+ public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
+ if (isCurrentlyOnMainThread()) {
+ runnable.run();
+ } else {
+ runOnMainThread(runnable);
+ }
+ }
+
+ /**
+ * @return if the calling thread is on the main thread
+ */
+ public static boolean isCurrentlyOnMainThread() {
+ return Looper.getMainLooper().isCurrentThread();
+ }
+
+ /**
+ * @throws IllegalStateException if the calling thread is _off_ the main thread
+ */
+ public static void verifyOnMainThread() throws IllegalStateException {
+ if (!isCurrentlyOnMainThread()) {
+ throw new IllegalStateException("Must call _on_ the main thread");
+ }
+ }
+
+ /**
+ * @throws IllegalStateException if the calling thread is _on_ the main thread
+ */
+ public static void verifyOffMainThread() throws IllegalStateException {
+ if (isCurrentlyOnMainThread()) {
+ throw new IllegalStateException("Must call _off_ the main thread");
+ }
+ }
+
+
+ /**
+ * Hide a view by setting its layout params to 0x0
+ * @param view The view to hide.
+ */
+ public static void hideViewByLayoutParams(View view) {
+ if (view instanceof LinearLayout) {
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
+ view.setLayoutParams(layoutParams);
+ } else if (view instanceof FrameLayout) {
+ FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
+ view.setLayoutParams(layoutParams2);
+ } else if (view instanceof RelativeLayout) {
+ RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0);
+ view.setLayoutParams(layoutParams3);
+ } else if (view instanceof Toolbar) {
+ Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0);
+ view.setLayoutParams(layoutParams4);
+ } else if (view instanceof ViewGroup) {
+ ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0);
+ view.setLayoutParams(layoutParams5);
+ } else {
+ ViewGroup.LayoutParams params = view.getLayoutParams();
+ params.width = 0;
+ params.height = 0;
+ view.setLayoutParams(params);
+ }
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/BaseSettings.java b/app/src/main/java/app/revanced/integrations/shared/settings/BaseSettings.java
new file mode 100644
index 0000000..aa24f15
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/shared/settings/BaseSettings.java
@@ -0,0 +1,11 @@
+package app.revanced.integrations.shared.settings;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+
+public class BaseSettings {
+ public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
+ public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE/*, parent(DEBUG)*/);
+ public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
+ public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE/*, parent(DEBUG)*/);
+}
diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/BooleanSetting.java b/app/src/main/java/app/revanced/integrations/shared/settings/BooleanSetting.java
new file mode 100644
index 0000000..fe2910c
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/shared/settings/BooleanSetting.java
@@ -0,0 +1,13 @@
+package app.revanced.integrations.shared.settings;
+
+public class BooleanSetting extends Setting {
+
+ public BooleanSetting(String key, boolean value) {
+ super(key, value);
+ }
+
+ public BooleanSetting(String key, boolean value, Object drop) {
+ super(key, value);
+ }
+
+}
diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java b/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java
new file mode 100644
index 0000000..b3ddaab
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java
@@ -0,0 +1,18 @@
+package app.revanced.integrations.shared.settings;
+
+public class Setting {
+ T value;
+ public boolean rebootApp = false;
+
+ public Setting(String key, T value) {
+ this.value = value;
+ }
+
+ public T get() {
+ return value;
+ }
+
+ public void save(T newValue){
+ value = newValue;
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/ByteTrieSearch.java b/app/src/main/java/app/revanced/integrations/youtube/ByteTrieSearch.java
new file mode 100644
index 0000000..ef27382
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/ByteTrieSearch.java
@@ -0,0 +1,45 @@
+package app.revanced.integrations.youtube;
+
+import androidx.annotation.NonNull;
+
+import java.nio.charset.StandardCharsets;
+
+public final class ByteTrieSearch extends TrieSearch {
+
+ private static final class ByteTrieNode extends TrieNode {
+ ByteTrieNode() {
+ super();
+ }
+ ByteTrieNode(char nodeCharacterValue) {
+ super(nodeCharacterValue);
+ }
+ @Override
+ TrieNode createNode(char nodeCharacterValue) {
+ return new ByteTrieNode(nodeCharacterValue);
+ }
+ @Override
+ char getCharValue(byte[] text, int index) {
+ return (char) text[index];
+ }
+ @Override
+ int getTextLength(byte[] text) {
+ return text.length;
+ }
+ }
+
+ /**
+ * Helper method for the common usage of converting Strings to raw UTF-8 bytes.
+ */
+ public static byte[][] convertStringsToBytes(String... strings) {
+ final int length = strings.length;
+ byte[][] replacement = new byte[length][];
+ for (int i = 0; i < length; i++) {
+ replacement[i] = strings[i].getBytes(StandardCharsets.UTF_8);
+ }
+ return replacement;
+ }
+
+ public ByteTrieSearch(@NonNull byte[]... patterns) {
+ super(new ByteTrieNode(), patterns);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/StringTrieSearch.java b/app/src/main/java/app/revanced/integrations/youtube/StringTrieSearch.java
new file mode 100644
index 0000000..618d9d6
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/StringTrieSearch.java
@@ -0,0 +1,34 @@
+package app.revanced.integrations.youtube;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Text pattern searching using a prefix tree (trie).
+ */
+public final class StringTrieSearch extends TrieSearch {
+
+ private static final class StringTrieNode extends TrieNode {
+ StringTrieNode() {
+ super();
+ }
+ StringTrieNode(char nodeCharacterValue) {
+ super(nodeCharacterValue);
+ }
+ @Override
+ TrieNode createNode(char nodeValue) {
+ return new StringTrieNode(nodeValue);
+ }
+ @Override
+ char getCharValue(String text, int index) {
+ return text.charAt(index);
+ }
+ @Override
+ int getTextLength(String text) {
+ return text.length();
+ }
+ }
+
+ public StringTrieSearch(@NonNull String... patterns) {
+ super(new StringTrieNode(), patterns);
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java b/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java
new file mode 100644
index 0000000..12b385a
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java
@@ -0,0 +1,412 @@
+package app.revanced.integrations.youtube;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Searches for a group of different patterns using a trie (prefix tree).
+ * Can significantly speed up searching for multiple patterns.
+ */
+public abstract class TrieSearch {
+
+ public interface TriePatternMatchedCallback {
+ /**
+ * Called when a pattern is matched.
+ *
+ * @param textSearched Text that was searched.
+ * @param matchedStartIndex Start index of the search text, where the pattern was matched.
+ * @param matchedLength Length of the match.
+ * @param callbackParameter Optional parameter passed into {@link TrieSearch#matches(Object, Object)}.
+ * @return True, if the search should stop here.
+ * If false, searching will continue to look for other matches.
+ */
+ boolean patternMatched(T textSearched, int matchedStartIndex, int matchedLength, Object callbackParameter);
+ }
+
+ /**
+ * Represents a compressed tree path for a single pattern that shares no sibling nodes.
+ *
+ * For example, if a tree contains the patterns: "foobar", "football", "feet",
+ * it would contain 3 compressed paths of: "bar", "tball", "eet".
+ *
+ * And the tree would contain children arrays only for the first level containing 'f',
+ * the second level containing 'o',
+ * and the third level containing 'o'.
+ *
+ * This is done to reduce memory usage, which can be substantial if many long patterns are used.
+ */
+ private static final class TrieCompressedPath {
+ final T pattern;
+ final int patternStartIndex;
+ final int patternLength;
+ final TriePatternMatchedCallback callback;
+
+ TrieCompressedPath(T pattern, int patternStartIndex, int patternLength, TriePatternMatchedCallback callback) {
+ this.pattern = pattern;
+ this.patternStartIndex = patternStartIndex;
+ this.patternLength = patternLength;
+ this.callback = callback;
+ }
+ boolean matches(TrieNode enclosingNode, // Used only for the get character method.
+ T searchText, int searchTextLength, int searchTextIndex, Object callbackParameter) {
+ if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) {
+ return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match.
+ }
+ for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) {
+ if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) {
+ return false;
+ }
+ }
+ return callback == null || callback.patternMatched(searchText,
+ searchTextIndex - patternStartIndex, patternLength, callbackParameter);
+ }
+ }
+
+ static abstract class TrieNode {
+ /**
+ * Dummy value used for root node. Value can be anything as it's never referenced.
+ */
+ private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character.
+
+ /**
+ * How much to expand the children array when resizing.
+ */
+ private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2;
+
+ /**
+ * Character this node represents.
+ * This field is ignored for the root node (which does not represent any character).
+ */
+ private final char nodeValue;
+
+ /**
+ * A compressed graph path that represents the remaining pattern characters of a single child node.
+ *
+ * If present then child array is always null, although callbacks for other
+ * end of patterns can also exist on this same node.
+ */
+ @Nullable
+ private TrieCompressedPath leaf;
+
+ /**
+ * All child nodes. Only present if no compressed leaf exist.
+ *
+ * Array is dynamically increased in size as needed,
+ * and uses perfect hashing for the elements it contains.
+ *
+ * So if the array contains a given character,
+ * the character will always map to the node with index: (character % arraySize).
+ *
+ * Elements not contained can collide with elements the array does contain,
+ * so must compare the nodes character value.
+ *
+ * Alternatively this array could be a sorted and densely packed array,
+ * and lookup is done using binary search.
+ * That would save a small amount of memory because there's no null children entries,
+ * but would give a worst case search of O(nlog(m)) where n is the number of
+ * characters in the searched text and m is the maximum size of the sorted character arrays.
+ * Using a hash table array always gives O(n) search time.
+ * The memory usage here is very small (all Litho filters use ~10KB of memory),
+ * so the more performant hash implementation is chosen.
+ */
+ @Nullable
+ private TrieNode[] children;
+
+ /**
+ * Callbacks for all patterns that end at this node.
+ */
+ @Nullable
+ private List> endOfPatternCallback;
+
+ TrieNode() {
+ this.nodeValue = ROOT_NODE_CHARACTER_VALUE;
+ }
+ TrieNode(char nodeCharacterValue) {
+ this.nodeValue = nodeCharacterValue;
+ }
+
+ /**
+ * @param pattern Pattern to add.
+ * @param patternIndex Current recursive index of the pattern.
+ * @param patternLength Length of the pattern.
+ * @param callback Callback, where a value of NULL indicates to always accept a pattern match.
+ */
+ private void addPattern(@NonNull T pattern, int patternIndex, int patternLength,
+ @Nullable TriePatternMatchedCallback callback) {
+ if (patternIndex == patternLength) { // Reached the end of the pattern.
+ if (endOfPatternCallback == null) {
+ endOfPatternCallback = new ArrayList<>(1);
+ }
+ endOfPatternCallback.add(callback);
+ return;
+ }
+ if (leaf != null) {
+ // Reached end of the graph and a leaf exist.
+ // Recursively call back into this method and push the existing leaf down 1 level.
+ if (children != null) throw new IllegalStateException();
+ //noinspection unchecked
+ children = new TrieNode[1];
+ TrieCompressedPath temp = leaf;
+ leaf = null;
+ addPattern(temp.pattern, temp.patternStartIndex, temp.patternLength, temp.callback);
+ // Continue onward and add the parameter pattern.
+ } else if (children == null) {
+ leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback);
+ return;
+ }
+ final char character = getCharValue(pattern, patternIndex);
+ final int arrayIndex = hashIndexForTableSize(children.length, character);
+ TrieNode child = children[arrayIndex];
+ if (child == null) {
+ child = createNode(character);
+ children[arrayIndex] = child;
+ } else if (child.nodeValue != character) {
+ // Hash collision. Resize the table until perfect hashing is found.
+ child = createNode(character);
+ expandChildArray(child);
+ }
+ child.addPattern(pattern, patternIndex + 1, patternLength, callback);
+ }
+
+ /**
+ * Resizes the children table until all nodes hash to exactly one array index.
+ */
+ private void expandChildArray(TrieNode child) {
+ int replacementArraySize = Objects.requireNonNull(children).length;
+ while (true) {
+ replacementArraySize += CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT;
+ //noinspection unchecked
+ TrieNode[] replacement = new TrieNode[replacementArraySize];
+ addNodeToArray(replacement, child);
+ boolean collision = false;
+ for (TrieNode existingChild : children) {
+ if (existingChild != null) {
+ if (!addNodeToArray(replacement, existingChild)) {
+ collision = true;
+ break;
+ }
+ }
+ }
+ if (collision) {
+ continue;
+ }
+ children = replacement;
+ return;
+ }
+ }
+
+ private static boolean addNodeToArray(TrieNode[] array, TrieNode childToAdd) {
+ final int insertIndex = hashIndexForTableSize(array.length, childToAdd.nodeValue);
+ if (array[insertIndex] != null ) {
+ return false; // Collision.
+ }
+ array[insertIndex] = childToAdd;
+ return true;
+ }
+
+ private static int hashIndexForTableSize(int arraySize, char nodeValue) {
+ return nodeValue % arraySize;
+ }
+
+ /**
+ * This method is static and uses a loop to avoid all recursion.
+ * This is done for performance since the JVM does not optimize tail recursion.
+ *
+ * @param startNode Node to start the search from.
+ * @param searchText Text to search for patterns in.
+ * @param searchTextIndex Start index, inclusive.
+ * @param searchTextEndIndex End index, exclusive.
+ * @return If any pattern matches, and it's associated callback halted the search.
+ */
+ private static boolean matches(final TrieNode startNode, final T searchText,
+ int searchTextIndex, final int searchTextEndIndex,
+ final Object callbackParameter) {
+ TrieNode node = startNode;
+ int currentMatchLength = 0;
+
+ while (true) {
+ TrieCompressedPath leaf = node.leaf;
+ if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) {
+ return true; // Leaf exists and it matched the search text.
+ }
+ List> endOfPatternCallback = node.endOfPatternCallback;
+ if (endOfPatternCallback != null) {
+ final int matchStartIndex = searchTextIndex - currentMatchLength;
+ for (@Nullable TriePatternMatchedCallback callback : endOfPatternCallback) {
+ if (callback == null) {
+ return true; // No callback and all matches are valid.
+ }
+ if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) {
+ return true; // Callback confirmed the match.
+ }
+ }
+ }
+ TrieNode[] children = node.children;
+ if (children == null) {
+ return false; // Reached a graph end point and there's no further patterns to search.
+ }
+ if (searchTextIndex == searchTextEndIndex) {
+ return false; // Reached end of the search text and found no matches.
+ }
+
+ // Use the start node to reduce VM method lookup, since all nodes are the same class type.
+ final char character = startNode.getCharValue(searchText, searchTextIndex);
+ final int arrayIndex = hashIndexForTableSize(children.length, character);
+ TrieNode child = children[arrayIndex];
+ if (child == null || child.nodeValue != character) {
+ return false;
+ }
+
+ node = child;
+ searchTextIndex++;
+ currentMatchLength++;
+ }
+ }
+
+ /**
+ * Gives an approximate memory usage.
+ *
+ * @return Estimated number of memory pointers used, starting from this node and including all children.
+ */
+ private int estimatedNumberOfPointersUsed() {
+ int numberOfPointers = 4; // Number of fields in this class.
+ if (leaf != null) {
+ numberOfPointers += 4; // Number of fields in leaf node.
+ }
+ if (endOfPatternCallback != null) {
+ numberOfPointers += endOfPatternCallback.size();
+ }
+ if (children != null) {
+ numberOfPointers += children.length;
+ for (TrieNode child : children) {
+ if (child != null) {
+ numberOfPointers += child.estimatedNumberOfPointersUsed();
+ }
+ }
+ }
+ return numberOfPointers;
+ }
+
+ abstract TrieNode createNode(char nodeValue);
+ abstract char getCharValue(T text, int index);
+ abstract int getTextLength(T text);
+ }
+
+ /**
+ * Root node, and it's children represent the first pattern characters.
+ */
+ private final TrieNode root;
+
+ /**
+ * Patterns to match.
+ */
+ private final List patterns = new ArrayList<>();
+
+ @SafeVarargs
+ TrieSearch(@NonNull TrieNode root, @NonNull T... patterns) {
+ this.root = Objects.requireNonNull(root);
+ addPatterns(patterns);
+ }
+
+ @SafeVarargs
+ public final void addPatterns(@NonNull T... patterns) {
+ for (T pattern : patterns) {
+ addPattern(pattern);
+ }
+ }
+
+ /**
+ * Adds a pattern that will always return a positive match if found.
+ *
+ * @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
+ */
+ public void addPattern(@NonNull T pattern) {
+ addPattern(pattern, root.getTextLength(pattern), null);
+ }
+
+ /**
+ * @param pattern Pattern to add. Calling this with a zero length pattern does nothing.
+ * @param callback Callback to determine if searching should halt when a match is found.
+ */
+ public void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback callback) {
+ addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback));
+ }
+
+ void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback callback) {
+ if (patternLength == 0) return; // Nothing to match
+
+ patterns.add(pattern);
+ root.addPattern(pattern, 0, patternLength, callback);
+ }
+
+ public final boolean matches(@NonNull T textToSearch) {
+ return matches(textToSearch, 0);
+ }
+
+ public boolean matches(@NonNull T textToSearch, @NonNull Object callbackParameter) {
+ return matches(textToSearch, 0, root.getTextLength(textToSearch),
+ Objects.requireNonNull(callbackParameter));
+ }
+
+ public boolean matches(@NonNull T textToSearch, int startIndex) {
+ return matches(textToSearch, startIndex, root.getTextLength(textToSearch));
+ }
+
+ public final boolean matches(@NonNull T textToSearch, int startIndex, int endIndex) {
+ return matches(textToSearch, startIndex, endIndex, null);
+ }
+
+ /**
+ * Searches through text, looking for any substring that matches any pattern in this tree.
+ *
+ * @param textToSearch Text to search through.
+ * @param startIndex Index to start searching, inclusive value.
+ * @param endIndex Index to stop matching, exclusive value.
+ * @param callbackParameter Optional parameter passed to the callbacks.
+ * @return If any pattern matched, and it's callback halted searching.
+ */
+ public boolean matches(@NonNull T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) {
+ return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter);
+ }
+
+ private boolean matches(@NonNull T textToSearch, int textToSearchLength, int startIndex, int endIndex,
+ @Nullable Object callbackParameter) {
+ if (endIndex > textToSearchLength) {
+ throw new IllegalArgumentException("endIndex: " + endIndex
+ + " is greater than texToSearchLength: " + textToSearchLength);
+ }
+ if (patterns.isEmpty()) {
+ return false; // No patterns were added.
+ }
+ for (int i = startIndex; i < endIndex; i++) {
+ if (TrieNode.matches(root, textToSearch, i, endIndex, callbackParameter)) return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return Estimated memory size (in kilobytes) of this instance.
+ */
+ public int getEstimatedMemorySize() {
+ if (patterns.isEmpty()) {
+ return 0;
+ }
+ // Assume the device has less than 32GB of ram (and can use pointer compression),
+ // or the device is 32-bit.
+ final int numberOfBytesPerPointer = 4;
+ return (int) Math.ceil((numberOfBytesPerPointer * root.estimatedNumberOfPointersUsed()) / 1024.0);
+ }
+
+ public int numberOfPatterns() {
+ return patterns.size();
+ }
+
+ public List getPatterns() {
+ return Collections.unmodifiableList(patterns);
+ }
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java
new file mode 100644
index 0000000..2e4a0fb
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java
@@ -0,0 +1,207 @@
+package app.revanced.integrations.youtube.patches.components;
+
+import android.app.Instrumentation;
+import android.view.KeyEvent;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import app.revanced.integrations.youtube.settings.Settings;
+import app.revanced.integrations.shared.Logger;
+import app.revanced.integrations.shared.Utils;
+import app.revanced.integrations.youtube.StringTrieSearch;
+
+@SuppressWarnings("unused")
+public final class AdsFilter extends Filter {
+ // region Fullscreen ad
+ private static volatile long lastTimeClosedFullscreenAd;
+ private static final Instrumentation instrumentation = new Instrumentation();
+ private final StringFilterGroup fullscreenAd;
+
+ // endregion
+
+ private final StringTrieSearch exceptions = new StringTrieSearch();
+
+ private final StringFilterGroup channelProfile;
+ private final ByteArrayFilterGroup visitStoreButton;
+
+ private final StringFilterGroup shoppingLinks;
+
+ public AdsFilter() {
+ exceptions.addPatterns(
+ "home_video_with_context", // Don't filter anything in the home page video component.
+ "related_video_with_context", // Don't filter anything in the related video component.
+ "comment_thread", // Don't filter anything in the comments.
+ "|comment.", // Don't filter anything in the comments replies.
+ "library_recent_shelf"
+ );
+
+ // Identifiers.
+
+
+ final var carouselAd = new StringFilterGroup(
+ Settings.HIDE_GENERAL_ADS,
+ "carousel_ad"
+ );
+ addIdentifierCallbacks(carouselAd);
+
+ // Paths.
+
+ fullscreenAd = new StringFilterGroup(
+ Settings.HIDE_FULLSCREEN_ADS,
+ "_interstitial"
+ );
+
+ final var buttonedAd = new StringFilterGroup(
+ Settings.HIDE_BUTTONED_ADS,
+ "_ad_with",
+ "_buttoned_layout",
+ // text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout
+ "image_button_group_layout",
+ "full_width_square_image_layout",
+ "video_display_button_group_layout",
+ "landscape_image_wide_button_layout",
+ "video_display_carousel_button_group_layout"
+ );
+
+ final var generalAds = new StringFilterGroup(
+ Settings.HIDE_GENERAL_ADS,
+ "ads_video_with_context",
+ "banner_text_icon",
+ "square_image_layout",
+ "watch_metadata_app_promo",
+ "video_display_full_layout",
+ "hero_promo_image",
+ "statement_banner",
+ "carousel_footered_layout",
+ "text_image_button_layout",
+ "primetime_promo",
+ "product_details",
+ "carousel_headered_layout",
+ "full_width_portrait_image_layout",
+ "brand_video_shelf"
+ );
+
+// final var movieAds = new StringFilterGroup(
+// Settings.HIDE_MOVIES_SECTION,
+// "browsy_bar",
+// "compact_movie",
+// "horizontal_movie_shelf",
+// "movie_and_show_upsell_card",
+// "compact_tvfilm_item",
+// "offer_module_root"
+// );
+
+ final var viewProducts = new StringFilterGroup(
+ Settings.HIDE_PRODUCTS_BANNER,
+ "product_item",
+ "products_in_video"
+ );
+
+ shoppingLinks = new StringFilterGroup(
+ Settings.HIDE_SHOPPING_LINKS,
+ "expandable_list"
+ );
+
+ channelProfile = new StringFilterGroup(
+ null,
+ "channel_profile.eml"
+ );
+
+ visitStoreButton = new ByteArrayFilterGroup(
+ Settings.HIDE_VISIT_STORE_BUTTON,
+ "header_store_button"
+ );
+
+ final var webLinkPanel = new StringFilterGroup(
+ Settings.HIDE_WEB_SEARCH_RESULTS,
+ "web_link_panel"
+ );
+
+ final var merchandise = new StringFilterGroup(
+ Settings.HIDE_MERCHANDISE_BANNERS,
+ "product_carousel"
+ );
+
+ final var selfSponsor = new StringFilterGroup(
+ Settings.HIDE_SELF_SPONSOR,
+ "cta_shelf_card"
+ );
+
+ addPathCallbacks(
+ generalAds,
+ buttonedAd,
+ merchandise,
+ viewProducts,
+ selfSponsor,
+ fullscreenAd,
+ channelProfile,
+ webLinkPanel,
+ shoppingLinks
+// movieAds
+ );
+ }
+
+ @Override
+ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
+ StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+ if (exceptions.matches(path))
+ return false;
+
+ if (matchedGroup == fullscreenAd) {
+ if (path.contains("|ImageType|")) closeFullscreenAd();
+
+ return false; // Do not actually filter the fullscreen ad otherwise it will leave a dimmed screen.
+ }
+
+ if (matchedGroup == channelProfile) {
+ if (visitStoreButton.check(protobufBufferArray).isFiltered()) {
+ return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ }
+ return false;
+ }
+
+ // Check for the index because of likelihood of false positives.
+ if (matchedGroup == shoppingLinks && contentIndex != 0)
+ return false;
+
+ return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+ }
+
+ /**
+ * Close the fullscreen ad.
+ *
+ * The strategy is to send a back button event to the app to close the fullscreen ad using the back button event.
+ */
+ private static void closeFullscreenAd() {
+ final var currentTime = System.currentTimeMillis();
+
+ // Prevent spamming the back button.
+ if (currentTime - lastTimeClosedFullscreenAd < 10000) return;
+ lastTimeClosedFullscreenAd = currentTime;
+
+ Logger.printDebug(() -> "Closing fullscreen ad");
+
+ Utils.runOnMainThreadDelayed(() -> {
+ // Must run off main thread (Odd, but whatever).
+ Utils.runOnBackgroundThread(() -> {
+ try {
+ instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
+ } catch (Exception ex) {
+ // Injecting user events on Android 10+ requires the manifest to include
+ // INJECT_EVENTS, and it's usage is heavily restricted
+ // and requires the user to manually approve the permission in the device settings.
+ //
+ // And no matter what, permissions cannot be added for root installations
+ // as manifest changes are ignored for mount installations.
+ //
+ // Instead, catch the SecurityException and turn off hide full screen ads
+ // since this functionality does not work for these devices.
+ Logger.printInfo(() -> "Could not inject back button event", ex);
+ Settings.HIDE_FULLSCREEN_ADS.save(false);
+ Utils.showToastLong("revanced_hide_fullscreen_ads_feature_not_available_toast");
+ }
+ });
+ }, 1000);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java
new file mode 100644
index 0000000..3d66aeb
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java
@@ -0,0 +1,557 @@
+package app.revanced.integrations.youtube.patches.components;
+
+import android.os.Build;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+
+import app.revanced.integrations.shared.Logger;
+import app.revanced.integrations.shared.settings.BooleanSetting;
+import app.revanced.integrations.shared.settings.BaseSettings;
+import app.revanced.integrations.youtube.ByteTrieSearch;
+import app.revanced.integrations.youtube.StringTrieSearch;
+import app.revanced.integrations.youtube.TrieSearch;
+import app.revanced.integrations.youtube.settings.Settings;
+
+abstract class FilterGroup {
+ final static class FilterGroupResult {
+ private BooleanSetting setting;
+ private int matchedIndex;
+ private int matchedLength;
+ // In the future it might be useful to include which pattern matched,
+ // but for now that is not needed.
+
+ FilterGroupResult() {
+ this(null, -1, 0);
+ }
+
+ FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
+ setValues(setting, matchedIndex, matchedLength);
+ }
+
+ public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
+ this.setting = setting;
+ this.matchedIndex = matchedIndex;
+ this.matchedLength = matchedLength;
+ }
+
+ /**
+ * A null value if the group has no setting,
+ * or if no match is returned from {@link FilterGroupList#check(Object)}.
+ */
+ public BooleanSetting getSetting() {
+ return setting;
+ }
+
+ public boolean isFiltered() {
+ return matchedIndex >= 0;
+ }
+
+ /**
+ * Matched index of first pattern that matched, or -1 if nothing matched.
+ */
+ public int getMatchedIndex() {
+ return matchedIndex;
+ }
+
+ /**
+ * Length of the matched filter pattern.
+ */
+ public int getMatchedLength() {
+ return matchedLength;
+ }
+ }
+
+ protected final BooleanSetting setting;
+ protected final T[] filters;
+
+ /**
+ * Initialize a new filter group.
+ *
+ * @param setting The associated setting.
+ * @param filters The filters.
+ */
+ @SafeVarargs
+ public FilterGroup(final BooleanSetting setting, final T... filters) {
+ this.setting = setting;
+ this.filters = filters;
+ if (filters.length == 0) {
+ throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
+ }
+ }
+
+ public boolean isEnabled() {
+ return setting == null || setting.get();
+ }
+
+ /**
+ * @return If {@link FilterGroupList} should include this group when searching.
+ * By default, all filters are included except non enabled settings that require reboot.
+ */
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ public boolean includeInSearch() {
+ return isEnabled() || !setting.rebootApp;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
+ }
+
+ public abstract FilterGroupResult check(final T stack);
+}
+
+class StringFilterGroup extends FilterGroup {
+
+ public StringFilterGroup(final BooleanSetting setting, final String... filters) {
+ super(setting, filters);
+ }
+
+ @Override
+ public FilterGroupResult check(final String string) {
+ int matchedIndex = -1;
+ int matchedLength = 0;
+ if (isEnabled()) {
+ for (String pattern : filters) {
+ if (!string.isEmpty()) {
+ final int indexOf = string.indexOf(pattern);
+ if (indexOf >= 0) {
+ matchedIndex = indexOf;
+ matchedLength = pattern.length();
+ break;
+ }
+ }
+ }
+ }
+ return new FilterGroupResult(setting, matchedIndex, matchedLength);
+ }
+}
+
+/**
+ * If you have more than 1 filter patterns, then all instances of
+ * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
+ * which uses a prefix tree to give better performance.
+ */
+class ByteArrayFilterGroup extends FilterGroup {
+
+ private volatile int[][] failurePatterns;
+
+ // Modified implementation from https://stackoverflow.com/a/1507813
+ private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
+ // Finds the first occurrence of the pattern in the byte array using
+ // KMP matching algorithm.
+ int patternLength = pattern.length;
+ for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
+ while (j > 0 && pattern[j] != data[i]) {
+ j = failure[j - 1];
+ }
+ if (pattern[j] == data[i]) {
+ j++;
+ }
+ if (j == patternLength) {
+ return i - patternLength + 1;
+ }
+ }
+ return -1;
+ }
+
+ private static int[] createFailurePattern(byte[] pattern) {
+ // Computes the failure function using a boot-strapping process,
+ // where the pattern is matched against itself.
+ final int patternLength = pattern.length;
+ final int[] failure = new int[patternLength];
+
+ for (int i = 1, j = 0; i < patternLength; i++) {
+ while (j > 0 && pattern[j] != pattern[i]) {
+ j = failure[j - 1];
+ }
+ if (pattern[j] == pattern[i]) {
+ j++;
+ }
+ failure[i] = j;
+ }
+ return failure;
+ }
+
+ public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
+ super(setting, filters);
+ }
+
+ /**
+ * Converts the Strings into byte arrays. Used to search for text in binary data.
+ */
+ public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
+ super(setting, ByteTrieSearch.convertStringsToBytes(filters));
+ }
+
+ private synchronized void buildFailurePatterns() {
+ if (failurePatterns != null)
+ return; // Thread race and another thread already initialized the search.
+ Logger.printDebug(() -> "Building failure array for: " + this);
+ int[][] failurePatterns = new int[filters.length][];
+ int i = 0;
+ for (byte[] pattern : filters) {
+ failurePatterns[i++] = createFailurePattern(pattern);
+ }
+ this.failurePatterns = failurePatterns; // Must set after initialization finishes.
+ }
+
+ @Override
+ public FilterGroupResult check(final byte[] bytes) {
+ int matchedLength = 0;
+ int matchedIndex = -1;
+ if (isEnabled()) {
+ int[][] failures = failurePatterns;
+ if (failures == null) {
+ buildFailurePatterns(); // Lazy load.
+ failures = failurePatterns;
+ }
+ for (int i = 0, length = filters.length; i < length; i++) {
+ byte[] filter = filters[i];
+ matchedIndex = indexOf(bytes, filter, failures[i]);
+ if (matchedIndex >= 0) {
+ matchedLength = filter.length;
+ break;
+ }
+ }
+ }
+ return new FilterGroupResult(setting, matchedIndex, matchedLength);
+ }
+}
+
+
+abstract class FilterGroupList> implements Iterable {
+
+ private final List filterGroups = new ArrayList<>();
+ private final TrieSearch search = createSearchGraph();
+
+ @SafeVarargs
+ protected final void addAll(final T... groups) {
+ filterGroups.addAll(Arrays.asList(groups));
+
+ for (T group : groups) {
+ if (!group.includeInSearch()) {
+ continue;
+ }
+ for (V pattern : group.filters) {
+ search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+ if (group.isEnabled()) {
+ FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
+ result.setValues(group.setting, matchedStartIndex, matchedLength);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+ }
+
+ @NonNull
+ @Override
+ public Iterator iterator() {
+ return filterGroups.iterator();
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @Override
+ public void forEach(@NonNull Consumer super T> action) {
+ filterGroups.forEach(action);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ @NonNull
+ @Override
+ public Spliterator spliterator() {
+ return filterGroups.spliterator();
+ }
+
+ protected FilterGroup.FilterGroupResult check(V stack) {
+ FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
+ search.matches(stack, result);
+ return result;
+
+ }
+
+ protected abstract TrieSearch createSearchGraph();
+}
+
+final class StringFilterGroupList extends FilterGroupList {
+ protected StringTrieSearch createSearchGraph() {
+ return new StringTrieSearch();
+ }
+}
+
+/**
+ * If searching for a single byte pattern, then it is slightly better to use
+ * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
+ * than a prefix tree to search for only 1 pattern.
+ */
+final class ByteArrayFilterGroupList extends FilterGroupList {
+ protected ByteTrieSearch createSearchGraph() {
+ return new ByteTrieSearch();
+ }
+}
+
+/**
+ * Filters litho based components.
+ *
+ * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
+ * and {@link #addPathCallbacks(StringFilterGroup...)}.
+ *
+ * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
+ * either an identifier or a path.
+ * Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
+ * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
+ * or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
+ *
+ * All callbacks must be registered before the constructor completes.
+ */
+abstract class Filter {
+
+ public enum FilterContentType {
+ IDENTIFIER,
+ PATH,
+ PROTOBUFFER
+ }
+
+ /**
+ * Identifier callbacks. Do not add to this instance,
+ * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
+ */
+ protected final List identifierCallbacks = new ArrayList<>();
+ /**
+ * Path callbacks. Do not add to this instance,
+ * and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
+ */
+ protected final List pathCallbacks = new ArrayList<>();
+
+ /**
+ * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
+ * if any of the groups are found.
+ */
+ protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
+ identifierCallbacks.addAll(Arrays.asList(groups));
+ }
+
+ /**
+ * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
+ * if any of the groups are found.
+ */
+ protected final void addPathCallbacks(StringFilterGroup... groups) {
+ pathCallbacks.addAll(Arrays.asList(groups));
+ }
+
+ /**
+ * Called after an enabled filter has been matched.
+ * Default implementation is to always filter the matched component and log the action.
+ * Subclasses can perform additional or different checks if needed.
+ *
+ * If the content is to be filtered, subclasses should always
+ * call this method (and never return a plain 'true').
+ * That way the logs will always show when a component was filtered and which filter hide it.
+ *
+ * Method is called off the main thread.
+ *
+ * @param matchedGroup The actual filter that matched.
+ * @param contentType The type of content matched.
+ * @param contentIndex Matched index of the identifier or path.
+ * @return True if the litho component should be filtered out.
+ */
+ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
+ StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+ if (BaseSettings.DEBUG.get()) {
+ String filterSimpleName = getClass().getSimpleName();
+ if (contentType == FilterContentType.IDENTIFIER) {
+ Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
+ } else {
+ Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
+ }
+ }
+ return true;
+ }
+}
+
+/**
+ * Placeholder for actual filters.
+ */
+final class DummyFilter extends Filter {
+}
+
+@SuppressWarnings("unused")
+public final class LithoFilterPatch {
+ /**
+ * Simple wrapper to pass the litho parameters through the prefix search.
+ */
+ private static final class LithoFilterParameters {
+ @Nullable
+ final String identifier;
+ final String path;
+ final byte[] protoBuffer;
+
+ LithoFilterParameters(@Nullable String lithoIdentifier, String lithoPath, byte[] protoBuffer) {
+ this.identifier = lithoIdentifier;
+ this.path = lithoPath;
+ this.protoBuffer = protoBuffer;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ // Estimate the percentage of the buffer that are Strings.
+ StringBuilder builder = new StringBuilder(Math.max(100, protoBuffer.length / 2));
+ builder.append("ID: ");
+ builder.append(identifier);
+ builder.append(" Path: ");
+ builder.append(path);
+ if (BaseSettings.DEBUG_PROTOBUFFER.get()) {
+ builder.append(" BufferStrings: ");
+ findAsciiStrings(builder, protoBuffer);
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * Search through a byte array for all ASCII strings.
+ */
+ private static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
+ // Valid ASCII values (ignore control characters).
+ final int minimumAscii = 32; // 32 = space character
+ final int maximumAscii = 126; // 127 = delete character
+ final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
+ String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering.
+
+ final int length = buffer.length;
+ int start = 0;
+ int end = 0;
+ while (end < length) {
+ int value = buffer[end];
+ if (value < minimumAscii || value > maximumAscii || end == length - 1) {
+ if (end - start >= minimumAsciiStringLength) {
+ for (int i = start; i < end; i++) {
+ builder.append((char) buffer[i]);
+ }
+ builder.append(delimitingCharacter);
+ }
+ start = end + 1;
+ }
+ end++;
+ }
+ }
+ }
+
+ private static final Filter[] filters = new Filter[]{
+ new AdsFilter() // Replaced by patch.
+ };
+
+ private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
+ private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
+
+ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+ /**
+ * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
+ * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
+ */
+ private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>();
+
+ static {
+ for (Filter filter : filters) {
+ filterUsingCallbacks(identifierSearchTree, filter,
+ filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
+ filterUsingCallbacks(pathSearchTree, filter,
+ filter.pathCallbacks, Filter.FilterContentType.PATH);
+ }
+
+ Logger.printDebug(() -> "Using: "
+ + identifierSearchTree.numberOfPatterns() + " identifier filters"
+ + " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
+ + pathSearchTree.numberOfPatterns() + " path filters"
+ + " (" + pathSearchTree.getEstimatedMemorySize() + " KB)");
+ }
+
+ private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
+ Filter filter, List groups,
+ Filter.FilterContentType type) {
+ for (StringFilterGroup group : groups) {
+ if (!group.includeInSearch()) {
+ continue;
+ }
+ for (String pattern : group.filters) {
+ pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
+ if (!group.isEnabled()) return false;
+ LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
+ return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer,
+ group, type, matchedStartIndex);
+ }
+ );
+ }
+ }
+ }
+
+ /**
+ * Injection point. Called off the main thread.
+ */
+ @SuppressWarnings("unused")
+ public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) {
+ // Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
+ // This is intentional, as it appears the buffer can be set once and then filtered multiple times.
+ // The buffer will be cleared from memory after a new buffer is set by the same thread,
+ // or when the calling thread eventually dies.
+ if (protobufBuffer == null) {
+ // It appears the buffer can be cleared out just before the call to #filter()
+ // Ignore this null value and retain the last buffer that was set.
+ Logger.printDebug(() -> "Ignoring null protobuffer");
+ } else {
+ bufferThreadLocal.set(protobufBuffer);
+ }
+ }
+
+ /**
+ * Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
+ */
+ @SuppressWarnings("unused")
+ public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
+ try {
+ // It is assumed that protobufBuffer is empty as well in this case.
+ if (pathBuilder.length() == 0)
+ return false;
+
+ ByteBuffer protobufBuffer = bufferThreadLocal.get();
+ final byte[] bufferArray;
+ // Potentially the buffer may have been null or never set up until now.
+ // Use an empty buffer so the litho id/path filters still work correctly.
+ if (protobufBuffer == null) {
+ Logger.printDebug(() -> "Proto buffer is null, using an empty buffer array");
+ bufferArray = EMPTY_BYTE_ARRAY;
+ } else if (!protobufBuffer.hasArray()) {
+ Logger.printDebug(() -> "Proto buffer does not have an array, using an empty buffer array");
+ bufferArray = EMPTY_BYTE_ARRAY;
+ } else {
+ bufferArray = protobufBuffer.array();
+ }
+
+ LithoFilterParameters parameter = new LithoFilterParameters(lithoIdentifier,
+ pathBuilder.toString(), bufferArray);
+ Logger.printDebug(() -> "Searching " + parameter);
+
+ if (parameter.identifier != null) {
+ if (identifierSearchTree.matches(parameter.identifier, parameter)) return true;
+ }
+ if (pathSearchTree.matches(parameter.path, parameter)) return true;
+ } catch (Exception ex) {
+ Logger.printException(() -> "Litho filter failure", ex);
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
new file mode 100644
index 0000000..2db92f5
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -0,0 +1,27 @@
+package app.revanced.integrations.youtube.settings;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import app.revanced.integrations.shared.Logger;
+import app.revanced.integrations.shared.settings.*;
+@SuppressWarnings("deprecation")
+public class Settings extends BaseSettings {
+ // Ads
+ public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE);
+ public static final BooleanSetting HIDE_BUTTONED_ADS = new BooleanSetting("revanced_hide_buttoned_ads", TRUE);
+ public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE);
+ public static final BooleanSetting HIDE_GET_PREMIUM = new BooleanSetting("revanced_hide_get_premium", TRUE);
+ public static final BooleanSetting HIDE_HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts_ads", TRUE);
+ public static final BooleanSetting HIDE_MERCHANDISE_BANNERS = new BooleanSetting("revanced_hide_merchandise_banners", TRUE);
+ public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE);
+ public static final BooleanSetting HIDE_PRODUCTS_BANNER = new BooleanSetting("revanced_hide_products_banner", TRUE);
+ public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE);
+ public static final BooleanSetting HIDE_SELF_SPONSOR = new BooleanSetting("revanced_hide_self_sponsor_ads", TRUE);
+ public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true);
+ public static final BooleanSetting HIDE_VISIT_STORE_BUTTON = new BooleanSetting("revanced_hide_visit_store_button", TRUE);
+ public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE);
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/chsbuffer/revancedxposed/Cache.kt b/app/src/main/java/io/github/chsbuffer/revancedxposed/Cache.kt
new file mode 100644
index 0000000..c9bd860
--- /dev/null
+++ b/app/src/main/java/io/github/chsbuffer/revancedxposed/Cache.kt
@@ -0,0 +1,109 @@
+package io.github.chsbuffer.revancedxposed
+
+import android.annotation.SuppressLint
+import android.app.Application
+import android.content.Context
+import android.content.SharedPreferences
+import de.robv.android.xposed.XposedBridge
+import org.luckypray.dexkit.result.ClassData
+import org.luckypray.dexkit.result.FieldData
+import org.luckypray.dexkit.result.MethodData
+import org.luckypray.dexkit.wrap.DexClass
+import org.luckypray.dexkit.wrap.DexField
+import org.luckypray.dexkit.wrap.DexMethod
+
+@SuppressLint("CommitPrefEdits")
+open class Cache(val app: Application) {
+ lateinit var pref: SharedPreferences
+ lateinit var map: MutableMap
+
+ @Suppress("UNCHECKED_CAST")
+ fun loadCache(): Boolean {
+ pref = app.getSharedPreferences("xprevanced", Context.MODE_PRIVATE)
+ val packageInfo = app.packageManager.getPackageInfo(app.packageName, 0)
+
+ val id = packageInfo.lastUpdateTime.toString()
+ val cachedId = pref.getString("id", null)
+
+ XposedBridge.log("cache ID: $id")
+ XposedBridge.log("cached ID: ${cachedId ?: ""}")
+
+ if (!cachedId.equals(id)) {
+ map = mutableMapOf("id" to id)
+ return false
+ } else {
+ map = pref.all.toMutableMap() as MutableMap
+ return true
+ }
+ }
+
+ fun saveCache() {
+ val edit = pref.edit()
+ map.forEach { k, v ->
+ edit.putString(k, v)
+ }
+ edit.commit()
+ }
+
+ fun getDexClass(key: String, f: () -> ClassData): DexClass {
+ return map[key]?.let { DexClass(it) } ?: f().apply {
+ map[key] = this.descriptor
+ XposedBridge.log("$key Matches: ${this.toDexType()}")
+ }.toDexType()
+ }
+
+ fun getDexMethod(key: String, f: () -> MethodData): DexMethod {
+ return map[key]?.let { DexMethod(it) } ?: f().apply {
+ map[key] = this.descriptor
+ XposedBridge.log("$key Matches: ${this.toDexMethod()}")
+ }.toDexMethod()
+ }
+
+ fun getDexField(key: String, f: () -> FieldData): DexField {
+ return map[key]?.let { DexField(it) } ?: f().apply {
+ map[key] = this.descriptor
+ XposedBridge.log("$key Matches: ${this.toDexField()}")
+ }.toDexField()
+ }
+
+ fun getString(key: String, f: () -> String): String {
+ return map[key] ?: f().also {
+ map[key] = it
+ XposedBridge.log("$key Matches: ${it}")
+ }
+ }
+
+ fun getNumber(key: String, f: () -> Number): Number {
+ return map[key]?.let { return Integer.parseInt(it) } ?: f().also {
+ map[key] = it.toString()
+ XposedBridge.log("$key Matches: ${it}")
+ }
+ }
+
+ fun getDexMethod(key: String): DexMethod {
+ return DexMethod(map[key]!!)
+ }
+
+ fun getDexClass(key: String): DexClass {
+ return DexClass(map[key]!!)
+ }
+
+ fun getDexField(key: String): DexField {
+ return DexField(map[key]!!)
+ }
+
+ fun getString(key: String): String {
+ return map[key]!!
+ }
+
+ fun setString(key: String, value: Any) {
+ val str = when (value) {
+ is ClassData -> value.toDexType()
+ is MethodData -> value.toDexMethod()
+ is FieldData -> value.toDexField()
+ else -> value
+ }
+ XposedBridge.log("$key Matches: $str")
+ map[key] = str.toString()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/github/chsbuffer/revancedxposed/Helper.kt b/app/src/main/java/io/github/chsbuffer/revancedxposed/Helper.kt
new file mode 100644
index 0000000..e020776
--- /dev/null
+++ b/app/src/main/java/io/github/chsbuffer/revancedxposed/Helper.kt
@@ -0,0 +1,23 @@
+package io.github.chsbuffer.revancedxposed
+
+import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
+import org.luckypray.dexkit.DexKitBridge
+import org.luckypray.dexkit.util.OpCodeUtil
+
+fun createDexKit(lpparam: LoadPackageParam): DexKitBridge {
+ System.loadLibrary("dexkit")
+ return DexKitBridge.create(lpparam.classLoader, true)
+}
+
+
+fun opCodesOf(
+ vararg opNames: String?,
+): Collection {
+ return opNames.map {
+ if (it == null) {
+ -1
+ } else {
+ OpCodeUtil.getOpCode(it)
+ }
+ }
+}
diff --git a/app/src/main/java/io/github/chsbuffer/revancedxposed/MainHook.kt b/app/src/main/java/io/github/chsbuffer/revancedxposed/MainHook.kt
index 73a28b3..f2a150c 100644
--- a/app/src/main/java/io/github/chsbuffer/revancedxposed/MainHook.kt
+++ b/app/src/main/java/io/github/chsbuffer/revancedxposed/MainHook.kt
@@ -19,6 +19,15 @@ class MainHook : IXposedHookLoadPackage {
XposedBridge.log("Youtube Music handleLoadPackage: ${t}ms")
}
}
+
+ "com.google.android.youtube" -> {
+ inContext(lpparam) { app ->
+ val t = measureTimeMillis {
+ YoutubeHook(app, lpparam).Hook()
+ }
+ XposedBridge.log("Youtube handleLoadPackage: ${t}ms")
+ }
+ }
}
}
}
diff --git a/app/src/main/java/io/github/chsbuffer/revancedxposed/MusicHook.kt b/app/src/main/java/io/github/chsbuffer/revancedxposed/MusicHook.kt
index 4e1cd3b..1d6e92b 100644
--- a/app/src/main/java/io/github/chsbuffer/revancedxposed/MusicHook.kt
+++ b/app/src/main/java/io/github/chsbuffer/revancedxposed/MusicHook.kt
@@ -1,9 +1,6 @@
package io.github.chsbuffer.revancedxposed
-import android.annotation.SuppressLint
import android.app.Application
-import android.content.Context
-import android.content.SharedPreferences
import android.view.View
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement
@@ -12,15 +9,11 @@ import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
import org.luckypray.dexkit.DexKitBridge
import org.luckypray.dexkit.query.enums.StringMatchType
-import org.luckypray.dexkit.result.MethodData
-import org.luckypray.dexkit.wrap.DexMethod
import java.lang.reflect.Modifier
-@SuppressLint("CommitPrefEdits")
-class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
+class MusicHook(app: Application, val lpparam: LoadPackageParam) : Cache(app) {
lateinit var dexkit: DexKitBridge
- lateinit var pref: SharedPreferences
- lateinit var map: MutableMap
+
var cached: Boolean = false
fun Hook() {
@@ -28,8 +21,7 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
XposedBridge.log("Using cached keys: $cached")
if (!cached) {
- System.loadLibrary("dexkit")
- dexkit = DexKitBridge.create(lpparam.classLoader, true)
+ dexkit = createDexKit(lpparam)
}
try {
@@ -48,7 +40,7 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
}
private fun EnableExclusiveAudioPlayback() {
- getMethodData("AllowExclusiveAudioPlaybackFingerprint") {
+ getDexMethod("AllowExclusiveAudioPlaybackFingerprint") {
dexkit.findMethod {
matcher {
returnType = "boolean"
@@ -78,57 +70,9 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
}
}
- @Suppress("UNCHECKED_CAST")
- fun loadCache(): Boolean {
- pref = app.getSharedPreferences("xprevanced", Context.MODE_PRIVATE)
- val packageInfo = app.packageManager.getPackageInfo(app.packageName, 0)
-
- val id = packageInfo.lastUpdateTime.toString()
- val cachedId = pref.getString("id", null)
-
- XposedBridge.log("cache ID: $id")
- XposedBridge.log("cached ID: ${cachedId ?: ""}")
-
- if (!cachedId.equals(id)) {
- map = mutableMapOf("id" to id)
- return false
- } else {
- map = pref.all.toMutableMap() as MutableMap
- return true
- }
- }
-
- fun saveCache() {
-// pref.edit(true) {
-// map.forEach { k, v ->
-// putString(k, v)
-// }
-// }
-
- val edit = pref.edit()
- map.forEach { k, v ->
- edit.putString(k, v)
- }
- edit.commit()
- }
-
- fun getMethodData(key: String, f: () -> MethodData): DexMethod {
- return map[key]?.let { DexMethod(it) } ?: f().apply {
- map[key] = this.descriptor
- XposedBridge.log("$key Matches: ${this.toDexMethod()}")
- }.toDexMethod()
- }
-
- fun getData(key: String, f: () -> String): String {
- return map[key] ?: f().also {
- map[key] = it
- XposedBridge.log("$key Matches: ${it}")
- }
- }
-
fun HideGetPremium() {
// HideGetPremiumFingerprint
- getMethodData("HideGetPremiumFingerprint") {
+ getDexMethod("HideGetPremiumFingerprint") {
dexkit.findMethod {
matcher {
modifiers = Modifier.FINAL or Modifier.PUBLIC
@@ -164,7 +108,7 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
private fun RemoveUpgradeButton() {
// PivotBarConstructorFingerprint
- getMethodData("PivotBarConstructorFingerprint") {
+ getDexMethod("PivotBarConstructorFingerprint") {
val result = dexkit.findMethod {
matcher {
name = ""
@@ -180,13 +124,13 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
)
}
}.single()
- getData("pivotBarElementField") {
+ getString("pivotBarElementField") {
result.declaredClass!!.fields.single { f -> f.typeName == "java.util.List" }.fieldName
}
result
}.let {
val pivotBarElementField =
- getData("pivotBarElementField") { throw Exception("WTF, Shouldn't i already searched?") }
+ getString("pivotBarElementField") { throw Exception("WTF, Shouldn't i already searched?") }
XposedBridge.hookMethod(it.getConstructorInstance(lpparam.classLoader),
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
@@ -200,7 +144,7 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
}
private fun MinimizedPlayback() {
- getMethodData("BackgroundPlaybackDisableFingerprint") {
+ getDexMethod("BackgroundPlaybackDisableFingerprint") {
dexkit.findMethod {
matcher {
returnType = "boolean"
@@ -226,7 +170,7 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
}
// KidsMinimizedPlaybackPolicyControllerFingerprint
- getMethodData("KidsMinimizedPlaybackPolicyControllerFingerprint") {
+ getDexMethod("KidsMinimizedPlaybackPolicyControllerFingerprint") {
dexkit.findMethod {
matcher {
@@ -258,7 +202,7 @@ class MusicHook(val app: Application, val lpparam: LoadPackageParam) {
private fun HideMusicVideoAds() {
// ShowMusicVideoAdsParentFingerprint
- getMethodData("ShowMusicVideoAdsMethod") {
+ getDexMethod("ShowMusicVideoAdsMethod") {
dexkit.findMethod {
matcher {
usingStrings(
diff --git a/app/src/main/java/io/github/chsbuffer/revancedxposed/YoutubeHook.kt b/app/src/main/java/io/github/chsbuffer/revancedxposed/YoutubeHook.kt
new file mode 100644
index 0000000..bd55c61
--- /dev/null
+++ b/app/src/main/java/io/github/chsbuffer/revancedxposed/YoutubeHook.kt
@@ -0,0 +1,357 @@
+package io.github.chsbuffer.revancedxposed
+
+import android.app.Application
+import android.content.ClipData
+import android.content.Intent
+import android.view.View
+import app.revanced.integrations.shared.Utils
+import app.revanced.integrations.youtube.patches.components.LithoFilterPatch
+import de.robv.android.xposed.XC_MethodHook
+import de.robv.android.xposed.XC_MethodReplacement
+import de.robv.android.xposed.XposedBridge
+import de.robv.android.xposed.XposedHelpers
+import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
+import org.luckypray.dexkit.DexKitBridge
+import org.luckypray.dexkit.result.FieldUsingType
+import java.lang.reflect.Member
+import java.lang.reflect.Modifier
+import java.nio.ByteBuffer
+
+class YoutubeHook(app: Application, val lpparam: LoadPackageParam) : Cache(app) {
+ lateinit var dexkit: DexKitBridge
+ val classLoader: ClassLoader = lpparam.classLoader
+
+ fun Hook() {
+ val cached = loadCache()
+ if (!cached)
+ dexkit = createDexKit(lpparam)
+
+ try {
+ VideoAds()
+ BackgroundPlayback()
+ RemoveTrackingQueryParameter()
+ HideAds()
+ LithoFilter()
+ saveCache()
+ } catch (err: Exception) {
+ pref.edit().clear().apply()
+ throw err
+ } finally {
+ if (!cached) dexkit.close()
+ }
+ }
+
+ fun VideoAds() {
+
+ val LoadVideoAds = getDexMethod("LoadVideoAds") {
+ dexkit.findMethod {
+ matcher {
+ usingEqStrings(
+ listOf(
+ "TriggerBundle doesn't have the required metadata specified by the trigger ",
+ "Tried to enter slot with no assigned slotAdapter",
+ "Trying to enter a slot when a slot of same type and physical position is already active. Its status: ",
+ )
+ )
+ }
+ }.single()
+ }
+
+ XposedBridge.hookMethod(
+ LoadVideoAds.getMethodInstance(classLoader), XC_MethodReplacement.DO_NOTHING
+ )
+ }
+
+ fun BackgroundPlayback() {
+
+ val prefBackgroundAndOfflineCategoryId = getNumber("prefBackgroundAndOfflineCategoryId") {
+ app.resources.getIdentifier(
+ "pref_background_and_offline_category", "string", app.packageName
+ )
+ }
+
+ val BackgroundPlaybackManagerFingerprint =
+ getDexMethod("BackgroundPlaybackManagerFingerprint") {
+ dexkit.findMethod {
+ matcher {
+ returnType = "boolean"
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ paramTypes = listOf(null)
+ opNames = listOf(
+ "const/4",
+ "if-eqz",
+ "iget",
+ "and-int/lit16",
+ "if-eqz",
+ "iget-object",
+ "if-nez",
+ "sget-object",
+ "iget",
+ "const",
+ "if-ne",
+ "iget-object",
+ "if-nez",
+ "sget-object",
+ "iget",
+ "if-ne",
+ "iget-object",
+ "check-cast",
+ "goto",
+ "sget-object",
+ "goto",
+ "const/4",
+ "if-eqz",
+ "iget-boolean",
+ "if-eqz"
+ )
+ }
+ }.single()
+ }
+
+ val BackgroundPlaybackSettingsFingerprint =
+ getDexMethod("BackgroundPlaybackSettingsBoolean") {
+ dexkit.findMethod {
+ matcher {
+ returnType = "java.lang.String"
+ modifiers = Modifier.PUBLIC or Modifier.FINAL
+ paramCount = 0
+ opNames = listOf(
+ "invoke-virtual",
+ "move-result",
+ "invoke-virtual",
+ "move-result",
+ "if-eqz",
+ "if-nez",
+ "goto"
+ )
+ usingNumbers(prefBackgroundAndOfflineCategoryId)
+ }
+ }.single().invokes.filter { it.returnTypeName == "boolean" }[1]
+ }
+
+ XposedBridge.hookMethod(
+ BackgroundPlaybackManagerFingerprint.getMethodInstance(classLoader),
+ XC_MethodReplacement.returnConstant(true)
+ )
+ XposedBridge.hookMethod(
+ BackgroundPlaybackSettingsFingerprint.getMethodInstance(classLoader),
+ XC_MethodReplacement.returnConstant(true)
+ )
+ }
+
+ fun RemoveTrackingQueryParameter() {
+ val CopyTextFingerprint = getDexMethod("CopyTextFingerprint") {
+ dexkit.findMethod {
+ matcher {
+ returnType = "void"
+ paramTypes = listOf(null, "java.util.Map")
+ opNames = listOf(
+ "iget-object",
+ "const-string",
+ "invoke-static",
+ "move-result-object",
+ "invoke-virtual",
+ "iget-object",
+ "iget-object",
+ "invoke-interface",
+ "return-void",
+ )
+ }
+ }.single()
+ }
+
+ val sanitizeArg1 = object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam) {
+ val url = param.args[1] as String
+ param.args[1] =
+ url.replace(".si=.+".toRegex(), "").replace(".feature=.+".toRegex(), "")
+ }
+ }
+
+ XposedBridge.hookMethod(
+ CopyTextFingerprint.getMethodInstance(classLoader), ScopedHook(
+ XposedHelpers.findMethodExact(
+ ClipData::class.java.name,
+ lpparam.classLoader,
+ "newPlainText",
+ CharSequence::class.java,
+ CharSequence::class.java
+ ), sanitizeArg1
+ )
+ )
+
+ val intentSanitizeHook = ScopedHook(
+ XposedHelpers.findMethodExact(
+ Intent::class.java.name,
+ lpparam.classLoader,
+ "putExtra",
+ String::class.java,
+ String::class.java
+ ), sanitizeArg1
+ )
+
+ val YouTubeShareSheetFingerprint = getDexMethod("YouTubeShareSheetFingerprint") {
+ dexkit.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.FINAL
+ returnType = "void"
+ paramTypes = listOf(null, "java.util.Map")
+ opNames = listOf(
+ "check-cast",
+ "goto",
+ "move-object",
+ "invoke-virtual",
+ )
+ addInvoke("Landroid/content/Intent;->putExtra(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;")
+ }
+ }.single()
+ }
+
+ XposedBridge.hookMethod(
+ YouTubeShareSheetFingerprint.getMethodInstance(classLoader), intentSanitizeHook
+ )
+
+ val SystemShareSheetFingerprint = getDexMethod("SystemShareSheetFingerprint") {
+ dexkit.findMethod {
+ matcher {
+ returnType = "void"
+ paramTypes = listOf(null, "java.util.Map")
+ opNames = listOf(
+ "check-cast",
+ "goto",
+ )
+ addEqString("YTShare_Logging_Share_Intent_Endpoint_Byte_Array")
+ }
+ }.single()
+ }
+
+ XposedBridge.hookMethod(
+ SystemShareSheetFingerprint.getMethodInstance(classLoader), intentSanitizeHook
+ )
+ }
+
+ fun LithoFilter() {
+ val ComponentContextParserFingerprint = getDexMethod("ComponentContextParserFingerprint") {
+ dexkit.findMethod {
+ matcher {
+ addEqString("Component was not found %s because it was removed due to duplicate converter bindings.")
+ }
+ }.single()
+ }
+
+ val ProtobufBufferReferenceFingerprint =
+ getDexMethod("ProtobufBufferReferenceFingerprint") {
+ dexkit.findMethod {
+ matcher {
+ returnType = "void"
+ modifiers = Modifier.PUBLIC or Modifier.FINAL
+ paramTypes = listOf("int", "java.nio.ByteBuffer")
+ opNames = listOf(
+ "iput", "invoke-virtual", "move-result", "sub-int/2addr"
+ )
+ }
+ }.single()
+ }
+
+ // Pass the buffer into Integrations.
+ XposedBridge.hookMethod(ProtobufBufferReferenceFingerprint.getMethodInstance(classLoader),
+ object : XC_MethodHook() {
+ override fun beforeHookedMethod(param: MethodHookParam) {
+ LithoFilterPatch.setProtoBuffer(param.args[1] as ByteBuffer)
+ }
+ })
+
+ // Hook the method that parses bytes into a ComponentContext.
+ val emptyComponentClass = getDexClass("emptyComponentClass") {
+ dexkit.findClass {
+ matcher {
+ addMethod {
+ name = ""
+ addEqString("EmptyComponent")
+ }
+ }
+ }.single()
+ }
+
+ val parseBytesToConversionContext = getDexMethod("parseBytesToConversionContext") {
+ val method = dexkit.findMethod {
+ matcher {
+ usingEqStrings(
+ "Failed to parse Element proto.",
+ "Cannot read theme key from model.",
+ "Number of bits must be positive",
+ "Failed to parse LoggingProperties",
+ "Found an Element with missing debugger id."
+ )
+ }
+ }.single()
+ val conversionContextClass = method.returnType!!
+ setString("conversionContextClass", conversionContextClass)
+ setString("identifierFieldData",
+ conversionContextClass.methods.single { it.methodName == "toString" }.usingFields.filter { it.usingType == FieldUsingType.Read && it.field.typeSign == "Ljava/lang/String;" }[1].field
+ )
+ setString("pathBuilderFieldData",
+ conversionContextClass.fields.single { it.typeSign == "Ljava/lang/StringBuilder;" })
+ method
+ }
+
+ val conversionContextClass = getDexClass("conversionContextClass")
+
+ val identifierField = getDexField("identifierFieldData").getFieldInstance(classLoader)
+ val pathBuilderField = getDexField("pathBuilderFieldData").getFieldInstance(classLoader)
+
+ val filtered = ThreadLocal()
+
+ XposedBridge.hookMethod(parseBytesToConversionContext.getMethodInstance(classLoader),
+ object : XC_MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam) {
+ val conversion = param.result
+
+ val identifier = identifierField.get(conversion) as String
+ val pathBuilder = pathBuilderField.get(conversion) as StringBuilder
+ filtered.set(LithoFilterPatch.filter(identifier, pathBuilder))
+ }
+ })
+
+ XposedBridge.hookMethod(ComponentContextParserFingerprint.getMethodInstance(classLoader),
+ object : XC_MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam) {
+ if (filtered.get() == true) {
+ param.result =
+ emptyComponentClass.getInstance(classLoader).declaredConstructors.single()
+ .newInstance()
+ }
+ }
+ })
+ }
+
+ fun HideAds() {
+ val adAttributionId = getNumber("adAttributionId") {
+ app.resources.getIdentifier("ad_attribution", "id", app.packageName)
+ }
+
+ XposedHelpers.findAndHookMethod(View::class.java.name,
+ lpparam.classLoader,
+ "findViewById",
+ Int::class.java.name,
+ object : XC_MethodHook() {
+ override fun afterHookedMethod(param: MethodHookParam) {
+ if (param.args[0].equals(adAttributionId)) {
+ XposedBridge.log("Hide Ad Attribution View")
+ Utils.hideViewByLayoutParams(param.result as View)
+ }
+ }
+ })
+ }
+}
+
+class ScopedHook(val hookMethod: Member, val callback: XC_MethodHook) : XC_MethodHook() {
+ lateinit var Unhook: XC_MethodHook.Unhook
+ override fun beforeHookedMethod(param: MethodHookParam?) {
+ Unhook = XposedBridge.hookMethod(hookMethod, callback)
+ }
+
+ override fun afterHookedMethod(param: MethodHookParam?) {
+ Unhook.unhook()
+ }
+}
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 4874473..0552e6f 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -2,5 +2,6 @@
- com.google.android.apps.youtube.music
+ - com.google.android.youtube
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9ca1bea..61c10fe 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,12 +1,14 @@
[versions]
-agp = "8.5.0"
+agp = "8.5.1"
kotlin = "1.9.0"
dexkit = "2.0.2"
xposed = "82"
+annotation = "1.8.1"
[libraries]
dexkit = { group = "org.luckypray", name = "dexkit", version.ref = "dexkit" }
xposed = { group = "de.robv.android.xposed", name = "api", version.ref = "xposed" }
+annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }