feat: Update YouTube & YouTube Music patches (#6571)

This commit is a squash of multiple commits, authored by the individuals referenced below. To see the exact commits by each author, see the unsquashed tree at https://github.com/ReVanced/revanced-patches/pull/6571 or with commit 03940665d27a42ed08992757dfe4534bd8243356.

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
Co-authored-by: hoodles <207470673+hoo-dles@users.noreply.github.com>
Co-authored-by: ILoveOpenSourceApplications <117499019+iloveopensourceapplications@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+lisouseinaikyrios@users.noreply.github.com>
Co-authored-by: inotia00 <108592928+inotia00@users.noreply.github.com>
Co-authored-by: OxrxL <108184954+oxrxl@users.noreply.github.com>
This commit is contained in:
oSumAtrIX 2026-02-07 23:45:08 +01:00
parent db5e0fe587
commit 88d33b847d
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
300 changed files with 8226 additions and 3643 deletions

View file

@ -16,8 +16,8 @@ import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.preference.LogBufferManager;
/**
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
* and additionally accessible thru {@link LogBufferManager}.
* ReVanced specific logger. Logging is done to standard device log (accessible through ADB),
* and additionally accessible through {@link LogBufferManager}.
*
* All methods are thread safe, and are safe to call even
* if {@link Utils#getContext()} is not available.
@ -202,7 +202,7 @@ public class Logger {
/**
* Logs exceptions under the outer class name of the code calling this method.
* <p>
* If the calling code is showing it's own error toast,
* If the calling code is showing its own error toast,
* instead use {@link #printInfo(LogMessage, Exception)}
*
* @param message log message

View file

@ -70,7 +70,7 @@ public class StringRef {
}
/**
* Creates a StringRef object that'll not change it's value
* Creates a StringRef object that'll not change its value
*
* @param value value which toString() method returns when invoked on returned object
* @return Unique StringRef instance, its value will never change
@ -102,7 +102,7 @@ public class StringRef {
public String toString() {
if (!resolved) {
if (resources == null || packageName == null) {
Context context = Utils.getContext();
var context = Utils.getContext();
resources = context.getResources();
packageName = context.getPackageName();
}

View file

@ -106,14 +106,18 @@ public abstract class TrieSearch<T> {
* 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.
/*
* Alternatively, this could be implemented as a sorted, densely packed array
* with lookups performed via binary search.
* This approach would save a small amount of memory by eliminating null
* child entries. However, it would result in a worst-case lookup time of
* O(n log m), where:
* - n is the number of characters in the input text, and
* - m is the maximum size of the sorted character arrays.
* In contrast, using a hash-based array guarantees O(n) lookup time.
* Given that the total memory usage is already very small (all Litho filters
* together use approximately 10KB), the hash-based implementation is preferred
* for its superior performance.
*/
@Nullable
private TrieNode<T>[] children;

View file

@ -206,7 +206,7 @@ public class Utils {
}
/**
* Hide a view by setting its visibility to GONE.
* Hide a view by setting its visibility as GONE.
*
* @param setting The setting to check for hiding the view.
* @param view The view to hide.
@ -218,7 +218,7 @@ public class Utils {
}
/**
* Hide a view by setting its visibility to GONE.
* Hide a view by setting its visibility as GONE.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
@ -288,7 +288,7 @@ public class Utils {
// Could do a thread sleep, but that will trigger an exception if the thread is interrupted.
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
}
// Return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
// Return the value, otherwise the compiler or VM might optimize and remove the meaningless time-wasting work,
// leaving an empty loop that hammers on the System.currentTimeMillis native call.
return meaninglessValue;
}
@ -298,10 +298,12 @@ public class Utils {
}
public static int indexOfFirstFound(String value, String... targets) {
for (String string : targets) {
if (!string.isEmpty()) {
final int indexOf = value.indexOf(string);
if (indexOf >= 0) return indexOf;
if (isNotEmpty(value)) {
for (String string : targets) {
if (!string.isEmpty()) {
final int indexOf = value.indexOf(string);
if (indexOf >= 0) return indexOf;
}
}
}
return -1;
@ -473,6 +475,10 @@ public class Utils {
clipboard.setPrimaryClip(clip);
}
public static boolean isNotEmpty(@Nullable String str) {
return str != null && !str.isEmpty();
}
public static boolean isTablet() {
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
}
@ -481,7 +487,7 @@ public class Utils {
private static Boolean isRightToLeftTextLayout;
/**
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc).
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc.).
* If this should match any ReVanced language override then instead use
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
* This is the default locale of the device, which may differ if
@ -495,7 +501,7 @@ public class Utils {
}
/**
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc).
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc.).
*/
public static boolean isRightToLeftLocale(Locale locale) {
String displayLanguage = locale.getDisplayLanguage();
@ -524,7 +530,7 @@ public class Utils {
/**
* @return if the text contains at least 1 number character,
* including any unicode numbers such as Arabic.
* including any Unicode numbers such as Arabic.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean containsNumber(CharSequence text) {
@ -1156,7 +1162,7 @@ public class Utils {
}
/**
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
* Uses {@link #adjustColorBrightness(int, float)} depending on if light or dark mode is active.
*/
@ColorInt
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {

View file

@ -288,8 +288,8 @@ public final class CheckEnvironmentPatch {
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
Boolean timeCheckPassed = nearPatchTime.check();
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Allow installing recently patched apks,
// even if the install source is not Manager or ADB.
// Allow installing recently patched APKs,
// even if the installation source is not Manager or ADB.
Check.disableForever();
return;
} else {

View file

@ -7,6 +7,8 @@ import android.content.pm.PackageManager;
import android.graphics.Color;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@ -53,24 +55,35 @@ public class CustomBrandingPatch {
}
}
private static final int notificationSmallIcon;
@Nullable
private static Integer notificationSmallIcon;
static {
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
if (branding == BrandingTheme.ORIGINAL) {
notificationSmallIcon = 0;
} else {
// Original icon is quantum_ic_video_youtube_white_24
String iconName = "revanced_notification_icon";
if (branding == BrandingTheme.CUSTOM) {
iconName += "_custom";
private static int getNotificationSmallIcon() {
// Cannot use static initialization block otherwise cyclic references exist
// between Settings initialization and this class.
if (notificationSmallIcon == null) {
if (GmsCoreSupport.isPackageNameOriginal()) {
Logger.printDebug(() -> "App is root mounted. Not overriding small notification icon");
return notificationSmallIcon = 0;
}
notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName);
if (notificationSmallIcon == 0) {
Logger.printException(() -> "Could not load notification small icon");
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
if (branding == BrandingTheme.ORIGINAL) {
notificationSmallIcon = 0;
} else {
// Original icon is quantum_ic_video_youtube_white_24
String iconName = "revanced_notification_icon";
if (branding == BrandingTheme.CUSTOM) {
iconName += "_custom";
}
notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName);
if (notificationSmallIcon == 0) {
Logger.printException(() -> "Could not load notification small icon");
}
}
}
return notificationSmallIcon;
}
/**
@ -89,8 +102,9 @@ public class CustomBrandingPatch {
*/
public static void setNotificationIcon(Notification.Builder builder) {
try {
if (notificationSmallIcon != 0) {
builder.setSmallIcon(notificationSmallIcon)
final int smallIcon = getNotificationSmallIcon();
if (smallIcon != 0) {
builder.setSmallIcon(smallIcon)
.setColor(Color.TRANSPARENT); // Remove YT red tint.
}
} catch (Exception ex) {
@ -104,8 +118,41 @@ public class CustomBrandingPatch {
* The total number of app name aliases, including dummy aliases.
*/
private static int numberOfPresetAppNames() {
// Modified during patching.
throw new IllegalStateException();
// Modified during patching, but requires a default if custom branding is excluded.
return 1;
}
/**
* Injection point.
* <p>
* If a custom icon was provided during patching.
*/
private static boolean userProvidedCustomIcon() {
// Modified during patching, but requires a default if custom branding is excluded.
return false;
}
/**
* Injection point.
* <p>
* If a custom name was provided during patching.
*/
private static boolean userProvidedCustomName() {
// Modified during patching, but requires a default if custom branding is excluded..
return false;
}
public static int getDefaultAppNameIndex() {
return userProvidedCustomName()
? numberOfPresetAppNames()
: 1;
}
public static BrandingTheme getDefaultIconStyle() {
return userProvidedCustomIcon()
? BrandingTheme.CUSTOM
: BrandingTheme.ORIGINAL;
}
/**

View file

@ -41,12 +41,13 @@ public final class EnableDebuggingPatch {
/**
* Injection point.
*/
public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) {
public static boolean isBooleanFeatureFlagEnabled(boolean value, long flag) {
if (LOG_FEATURE_FLAGS && value) {
if (DISABLED_FEATURE_FLAGS.contains(flag)) {
Long flagObj = flag;
if (DISABLED_FEATURE_FLAGS.contains(flagObj)) {
return false;
}
if (featureFlags.putIfAbsent(flag, TRUE) == null) {
if (featureFlags.putIfAbsent(flagObj, TRUE) == null) {
Logger.printDebug(() -> "boolean feature is enabled: " + flag);
}
}
@ -59,6 +60,8 @@ public final class EnableDebuggingPatch {
*/
public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) {
if (LOG_FEATURE_FLAGS && defaultValue != value) {
if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
if (featureFlags.putIfAbsent(flag, true) == null) {
// Align the log outputs to make post processing easier.
Logger.printDebug(() -> " double feature is enabled: " + flag
@ -74,6 +77,8 @@ public final class EnableDebuggingPatch {
*/
public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) {
if (LOG_FEATURE_FLAGS && defaultValue != value) {
if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
if (featureFlags.putIfAbsent(flag, true) == null) {
Logger.printDebug(() -> " long feature is enabled: " + flag
+ " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue));

View file

@ -18,8 +18,8 @@ public final class SanitizeSharingLinksPatch {
* Injection point.
*/
public static String sanitize(String url) {
if (BaseSettings.SANITIZE_SHARED_LINKS.get()) {
url = sanitizer.sanitizeUrlString(url);
if (BaseSettings.SANITIZE_SHARING_LINKS.get()) {
url = sanitizer.sanitizeURLString(url);
}
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {

View file

@ -13,6 +13,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
@ -24,7 +25,7 @@ import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
@SuppressWarnings("unused")
public final class CustomFilter extends Filter {
private static void showInvalidSyntaxToast(@NonNull String expression) {
private static void showInvalidSyntaxToast(String expression) {
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
}
@ -36,7 +37,12 @@ public final class CustomFilter extends Filter {
public static final String SYNTAX_STARTS_WITH = "^";
/**
* Optional character that separates the path from a proto buffer string pattern.
* Optional character that separates the path from an accessibility string pattern.
*/
public static final String SYNTAX_ACCESSIBILITY_SYMBOL = "#";
/**
* Optional character that separates the path/accessibility from a proto buffer string pattern.
*/
public static final String SYNTAX_BUFFER_SYMBOL = "$";
@ -51,15 +57,21 @@ public final class CustomFilter extends Filter {
return Collections.emptyList();
}
// Map key is the path including optional special characters (^ and/or $)
// Map key is the full path including optional special characters (^, #, $),
// and any accessibility pattern, but does not contain any buffer patterns.
Map<String, CustomFilterGroup> result = new HashMap<>();
Pattern pattern = Pattern.compile(
"(" // map key group
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with
+ "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path
+ "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol
+ ")" // end map key group
+ "(.*)"); // optional buffer string
"(" // Map key group.
// Optional starts with.
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)"
// Path string.
+ "([^\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + SYNTAX_BUFFER_SYMBOL + "\\E]*)"
// Optional accessibility string.
+ "(?:\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + "\\E([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*))?"
// Optional buffer string.
+ "(?:\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E(.*))?"
+ ")"); // end map key group
for (String expression : rawCustomFilterText.split("\n")) {
if (expression.isBlank()) continue;
@ -73,10 +85,12 @@ public final class CustomFilter extends Filter {
final String mapKey = matcher.group(1);
final boolean pathStartsWith = !matcher.group(2).isEmpty();
final String path = matcher.group(3);
final boolean hasBufferSymbol = !matcher.group(4).isEmpty();
final String bufferString = matcher.group(5);
final String accessibility = matcher.group(4); // null if not present
final String buffer = matcher.group(5); // null if not present
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
if (path.isBlank()
|| (accessibility != null && accessibility.isEmpty())
|| (buffer != null && buffer.isEmpty())) {
showInvalidSyntaxToast(expression);
continue;
}
@ -89,8 +103,13 @@ public final class CustomFilter extends Filter {
group = new CustomFilterGroup(pathStartsWith, path);
result.put(mapKey, group);
}
if (hasBufferSymbol) {
group.addBufferString(bufferString);
if (accessibility != null) {
group.addAccessibilityString(accessibility);
}
if (buffer != null) {
group.addBufferString(buffer);
}
}
@ -98,14 +117,22 @@ public final class CustomFilter extends Filter {
}
final boolean startsWith;
StringTrieSearch accessibilitySearch;
ByteTrieSearch bufferSearch;
CustomFilterGroup(boolean startsWith, @NonNull String path) {
CustomFilterGroup(boolean startsWith, String path) {
super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
this.startsWith = startsWith;
}
void addBufferString(@NonNull String bufferString) {
void addAccessibilityString(String accessibilityString) {
if (accessibilitySearch == null) {
accessibilitySearch = new StringTrieSearch();
}
accessibilitySearch.addPattern(accessibilityString);
}
void addBufferString(String bufferString) {
if (bufferSearch == null) {
bufferSearch = new ByteTrieSearch();
}
@ -117,6 +144,11 @@ public final class CustomFilter extends Filter {
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("CustomFilterGroup{");
if (accessibilitySearch != null) {
builder.append(", accessibility=");
builder.append(accessibilitySearch.getPatterns());
}
builder.append("path=");
if (startsWith) builder.append(SYNTAX_STARTS_WITH);
builder.append(filters[0]);
@ -146,18 +178,26 @@ public final class CustomFilter extends Filter {
}
@Override
public boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// All callbacks are custom filter groups.
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
// Check path start requirement.
if (custom.startsWith && contentIndex != 0) {
return false;
}
if (custom.bufferSearch == null) {
return true; // No buffer filter, only path filtering.
// Check accessibility string if specified.
if (custom.accessibilitySearch != null && !custom.accessibilitySearch.matches(accessibility)) {
return false;
}
return custom.bufferSearch.matches(buffer);
// Check buffer if specified.
if (custom.bufferSearch != null && !custom.bufferSearch.matches(buffer)) {
return false;
}
return true; // All custom filter conditions passed.
}
}

View file

@ -13,9 +13,9 @@ import java.util.List;
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
* and {@link #addPathCallbacks(StringFilterGroup...)}.
*
* To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
* To filter {@link FilterContentType#PROTOBUFFER} or {@link FilterContentType#ACCESSIBILITY}, first add a callback to
* either an identifier or a path.
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
* or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
*
@ -26,6 +26,7 @@ public abstract class Filter {
public enum FilterContentType {
IDENTIFIER,
PATH,
ACCESSIBILITY,
PROTOBUFFER
}
@ -41,7 +42,7 @@ public abstract class Filter {
public final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
/**
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
*/
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
@ -49,7 +50,7 @@ public abstract class Filter {
}
/**
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
*/
protected final void addPathCallbacks(StringFilterGroup... groups) {
@ -63,12 +64,15 @@ public abstract class Filter {
* <p>
* Method is called off the main thread.
*
* @param identifier Litho identifier.
* @param accessibility Accessibility string, or an empty string if not present for the component.
* @param buffer Protocol buffer.
* @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.
*/
public boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
return true;
}

View file

@ -122,7 +122,7 @@ public abstract class FilterGroup<T> {
/**
* If you have more than 1 filter patterns, then all instances of
* this class should filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
* this class should be filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
* which uses a prefix tree to give better performance.
*/
public static class ByteArrayFilterGroup extends FilterGroup<byte[]> {
@ -149,7 +149,7 @@ public abstract class FilterGroup<T> {
}
private static int[] createFailurePattern(byte[] pattern) {
// Computes the failure function using a boot-strapping process,
// Computes the failure function using a bootstrapping process,
// where the pattern is matched against itself.
final int patternLength = pattern.length;
final int[] failure = new int[patternLength];

View file

@ -24,11 +24,14 @@ public final class LithoFilterPatch {
private static final class LithoFilterParameters {
final String identifier;
final String path;
final String accessibility;
final byte[] buffer;
LithoFilterParameters(String lithoIdentifier, String lithoPath, byte[] buffer) {
LithoFilterParameters(String lithoIdentifier, String lithoPath,
String accessibility, byte[] buffer) {
this.identifier = lithoIdentifier;
this.path = lithoPath;
this.accessibility = accessibility;
this.buffer = buffer;
}
@ -39,6 +42,11 @@ public final class LithoFilterPatch {
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
builder.append("ID: ");
builder.append(identifier);
if (!accessibility.isEmpty()) {
// AccessibilityId and AccessibilityText are pieces of BufferStrings.
builder.append(" Accessibility: ");
builder.append(accessibility);
}
builder.append(" Path: ");
builder.append(path);
if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) {
@ -122,7 +130,7 @@ public final class LithoFilterPatch {
/**
* String suffix for components.
* Can be any of: ".eml", ".e-b", ".eml-js", "e-js-b"
* Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b"
*/
private static final byte[] LITHO_COMPONENT_EXTENSION_BYTES = ".e".getBytes(StandardCharsets.US_ASCII);
@ -132,7 +140,7 @@ public final class LithoFilterPatch {
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,
* Because litho filtering is multithreaded 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.
* Used for 20.21 and lower.
*/
@ -140,7 +148,7 @@ public final class LithoFilterPatch {
/**
* Identifier to protocol buffer mapping. Only used for 20.22+.
* Thread local is needed because filtering is multi-threaded and each thread can load
* Thread local is needed because filtering is multithreaded and each thread can load
* a different component with the same identifier.
*/
private static final ThreadLocal<Map<String, byte[]>> identifierToBufferThread = new ThreadLocal<>();
@ -155,6 +163,7 @@ public final class LithoFilterPatch {
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
static {
for (Filter filter : filters) {
filterUsingCallbacks(identifierSearchTree, filter,
filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
@ -186,16 +195,13 @@ public final class LithoFilterPatch {
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
final boolean isFiltered = filter.isFiltered(parameters.identifier,
parameters.path, parameters.buffer, group, type, matchedStartIndex);
parameters.accessibility, parameters.path, parameters.buffer,
group, type, matchedStartIndex);
if (isFiltered && BaseSettings.DEBUG.get()) {
if (type == Filter.FilterContentType.IDENTIFIER) {
Logger.printDebug(() -> "Filtered " + filterSimpleName
+ " identifier: " + parameters.identifier);
} else {
Logger.printDebug(() -> "Filtered " + filterSimpleName
+ " path: " + parameters.path);
}
Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER
? filterSimpleName + " filtered identifier: " + parameters.identifier
: filterSimpleName + " filtered path: " + parameters.path);
}
return isFiltered;
@ -212,7 +218,7 @@ public final class LithoFilterPatch {
/**
* Helper function that differs from {@link Character#isDigit(char)}
* as this only matches ascii and not unicode numbers.
* as this only matches ascii and not Unicode numbers.
*/
private static boolean isAsciiNumber(byte character) {
return '0' <= character && character <= '9';
@ -233,12 +239,19 @@ public final class LithoFilterPatch {
Logger.printDebug(() -> "New buffer: " + builder);
}
// The identifier always seems to start very close to the buffer start.
// Highest identifier start index ever observed is 50, with most around 30 to 40.
// The buffer can be very large with up to 200kb has been observed,
// so the search is restricted to only the start.
final int maxBufferStartIndex = 500; // 10x expected upper bound.
// Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of
// unique characters, but it seems to be slower since the extra overhead of checking the
// bad character array negates any performance gain of skipping a few extra subsearches.
int emlIndex = -1;
final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length;
for (int i = 0, lastStartIndex = buffer.length - emlStringLength; i <= lastStartIndex; i++) {
final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength);
for (int i = 0; i < lastBufferIndexToCheckFrom; i++) {
boolean match = true;
for (int j = 0; j < emlStringLength; j++) {
if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) {
@ -254,6 +267,9 @@ public final class LithoFilterPatch {
if (emlIndex < 0) {
// Buffer is not used for creating a new litho component.
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
Logger.printDebug(() -> "Could not find eml index");
}
return;
}
@ -290,7 +306,9 @@ public final class LithoFilterPatch {
}
}
if (endIndex < 0) {
Logger.printException(() -> "Could not find buffer identifier");
if (BaseSettings.DEBUG.get()) {
Logger.printException(() -> "Debug: Could not find buffer identifier");
}
return;
}
@ -329,7 +347,8 @@ public final class LithoFilterPatch {
/**
* Injection point.
*/
public static boolean isFiltered(String identifier, StringBuilder pathBuilder) {
public static boolean isFiltered(String identifier, @Nullable String accessibilityId,
@Nullable String accessibilityText, StringBuilder pathBuilder) {
try {
if (identifier.isEmpty() || pathBuilder.length() == 0) {
return false;
@ -340,7 +359,7 @@ public final class LithoFilterPatch {
final int pipeIndex = identifier.indexOf('|');
if (pipeIndex >= 0) {
// If the identifier contains no pipe, then it's not an ".eml" identifier
// and the buffer is not uniquely identified. Typically this only happens
// and the buffer is not uniquely identified. Typically, this only happens
// for subcomponents where buffer filtering is not used.
String identifierKey = identifier.substring(0, pipeIndex);
@ -357,7 +376,9 @@ public final class LithoFilterPatch {
// No buffer is found for some components, such as
// shorts_lockup_cell.eml on channel profiles.
// For now, just ignore this and filter without a buffer.
Logger.printException(() -> "Could not find global buffer for identifier: " + identifier);
if (BaseSettings.DEBUG.get()) {
Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier);
}
}
}
}
@ -372,7 +393,15 @@ public final class LithoFilterPatch {
}
String path = pathBuilder.toString();
LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, buffer);
String accessibility = "";
if (accessibilityId != null && !accessibilityId.isBlank()) {
accessibility = accessibilityId;
}
if (accessibilityText != null && !accessibilityText.isBlank()) {
accessibility = accessibilityId + '|' + accessibilityText;
}
LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, accessibility, buffer);
Logger.printDebug(() -> "Searching " + parameter);
return identifierSearchTree.matches(identifier, parameter)

View file

@ -24,23 +24,23 @@ public class LinkSanitizer {
: List.of(parametersToRemove);
}
public String sanitizeUrlString(String url) {
public String sanitizeURLString(String url) {
try {
return sanitizeUri(Uri.parse(url)).toString();
return sanitizeURI(Uri.parse(url)).toString();
} catch (Exception ex) {
Logger.printException(() -> "sanitizeUrlString failure: " + url, ex);
Logger.printException(() -> "sanitizeURLString failure: " + url, ex);
return url;
}
}
public Uri sanitizeUri(Uri uri) {
public Uri sanitizeURI(Uri uri) {
try {
String scheme = uri.getScheme();
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
// Opening YouTube share sheet 'other' option passes the video title as a URI.
// Checking !uri.isHierarchical() works for all cases, except if the
// video title starts with / and then it's hierarchical but still an invalid URI.
Logger.printDebug(() -> "Ignoring uri: " + uri);
Logger.printDebug(() -> "Ignoring URI: " + uri);
return uri;
}
@ -56,12 +56,12 @@ public class LinkSanitizer {
}
}
Uri sanitizedUrl = builder.build();
Logger.printInfo(() -> "Sanitized url: " + uri + " to: " + sanitizedUrl);
Uri sanitizedURL = builder.build();
Logger.printInfo(() -> "Sanitized URL: " + uri + " to: " + sanitizedURL);
return sanitizedUrl;
return sanitizedURL;
} catch (Exception ex) {
Logger.printException(() -> "sanitizeUri failure: " + uri, ex);
Logger.printException(() -> "sanitizeURI failure: " + uri, ex);
return uri;
}
}

View file

@ -23,8 +23,8 @@ public class Requester {
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
String url = apiUrl + route.getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// Request data is in the URL parameters and no body is sent.
// The calling code must set a length if using a request body.
// This request sends data via URL query parameters. No request body is included.
// If a request body is added, the caller must set the appropriate Content-Length header.
connection.setFixedLengthStreamingMode(0);
connection.setRequestMethod(route.getMethod().name());
String agentString = System.getProperty("http.agent")

View file

@ -96,15 +96,15 @@ public abstract class BaseActivityHook extends Activity {
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
// Replace dummy placeholder toolbar.
// This is required to fix submenu title alignment issue with Android ASOP 15+
ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
ViewGroup toolbarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolbarParent, "revanced_toolbar");
toolbarLayoutParams = dummyToolbar.getLayoutParams();
toolBarParent.removeView(dummyToolbar);
toolbarParent.removeView(dummyToolbar);
// Sets appropriate system navigation bar color for the activity.
ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
Toolbar toolbar = new Toolbar(toolbarParent.getContext());
toolbar.setBackgroundColor(getToolbarBackgroundColor());
toolbar.setNavigationIcon(getNavigationIcon());
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
@ -121,7 +121,7 @@ public abstract class BaseActivityHook extends Activity {
onPostToolbarSetup(activity, toolbar, fragment);
toolBarParent.addView(toolbar, 0);
toolbarParent.addView(toolbar, 0);
}
/**

View file

@ -6,6 +6,7 @@ import static app.revanced.extension.shared.patches.CustomBrandingPatch.Branding
import static app.revanced.extension.shared.settings.Setting.parent;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.patches.CustomBrandingPatch;
/**
* Settings shared across multiple apps.
@ -48,13 +49,13 @@ public class BaseSettings {
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SANITIZE_SHARED_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true);
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true);
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", CustomBrandingPatch.getDefaultIconStyle(), true);
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", CustomBrandingPatch.getDefaultAppNameIndex(), true);
public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG));

View file

@ -43,7 +43,7 @@ public class BooleanSetting extends Setting<Boolean> {
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Boolean)} was intnded.
* accidental usage when {@link #save(Boolean)} was intended.
*/
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
setting.value = Objects.requireNonNull(newValue);

View file

@ -72,7 +72,7 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
}
/**
* @param enumName Enum name. Casing does not matter.
* @param enumName Enum name. Casing does not matter.
* @return Enum of this type with the same declared name.
* @throws IllegalArgumentException if the name is not a valid enum of this type.
*/

View file

@ -274,60 +274,6 @@ public abstract class Setting<T> {
load();
}
/**
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
*/
public static <T> void migrateOldSettingToNew(Setting<T> oldSetting, Setting<T> newSetting) {
if (oldSetting == newSetting) throw new IllegalArgumentException();
if (!oldSetting.isSetToDefault()) {
Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting);
newSetting.save(oldSetting.value);
oldSetting.resetToDefault();
}
}
/**
* Migrate an old Setting value previously stored in a different SharedPreference.
* <p>
* This method will be deleted in the future.
*/
@SuppressWarnings({"rawtypes", "NewApi"})
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do.
}
Object newValue = setting.get();
final Object migratedValue;
if (setting instanceof BooleanSetting) {
migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue);
} else if (setting instanceof IntegerSetting) {
migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue);
} else if (setting instanceof LongSetting) {
migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue);
} else if (setting instanceof FloatSetting) {
migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue);
} else if (setting instanceof StringSetting) {
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
} else {
Logger.printException(() -> "Unknown setting: " + setting);
// Remove otherwise it'll show a toast on every launch.
oldPrefs.preferences.edit().remove(settingKey).apply();
return;
}
oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting.
if (migratedValue.equals(newValue)) {
Logger.printDebug(() -> "Value does not need migrating: " + settingKey);
return; // Old value is already equal to the new setting value.
}
Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey);
//noinspection unchecked
setting.save(migratedValue);
}
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
@ -419,7 +365,7 @@ public abstract class Setting<T> {
}
/**
* @return if the currently set value is the same as {@link #defaultValue}
* @return if the currently set value is the same as {@link #defaultValue}.
*/
public boolean isSetToDefault() {
return value.equals(defaultValue);
@ -450,7 +396,7 @@ public abstract class Setting<T> {
/**
* @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
* @return the value stored using the import/export key. Do not set any values in this method.
* @return the value stored using the import/export key. Do not set any values in this method.
*/
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;

View file

@ -208,7 +208,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
boolean syncSettingValue,
boolean applySettingToPreference) {
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
// Alternatively this could iterate through all Settings and check for any matching Preferences,
// but there are many more Settings than UI preferences so it's more efficient to only check
// the Preferences.
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {

View file

@ -72,7 +72,7 @@ public class FeatureFlagsManagerPreference extends Preference {
*/
private static final Set<Long> FLAGS_TO_IGNORE = Set.of(
45386834L, // 'You' tab settings icon.
45685201L // Bold icons. Forcing off interferes with patch changes and YT icons are broken.
45532100L // Cairo flag. Turning this off with all other flags causes the settings menu to be a mix of old/new.
);
/**
@ -131,9 +131,10 @@ public class FeatureFlagsManagerPreference extends Preference {
disabledFlags.removeAll(FLAGS_TO_IGNORE);
if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) {
// String does not need to be localized because it's basically impossible
// to reach the settings menu without encountering at least 1 flag.
Utils.showToastShort("No feature flags logged yet");
// It's impossible to reach the settings menu without reaching at least one flag.
// So if theres no flags, then that means the user has just enabled debugging
// but has not restarted the app yet.
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_no_flags"));
return;
}

View file

@ -36,18 +36,18 @@ public class SharedPrefCategory {
}
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
preferences.edit().putString(key, (value == null ? null : value.toString())).commit();
}
/**
* Removes any preference data type that has the specified key.
*/
public void removeKey(@NonNull String key) {
preferences.edit().remove(Objects.requireNonNull(key)).apply();
preferences.edit().remove(Objects.requireNonNull(key)).commit();
}
public void saveBoolean(@NonNull String key, boolean value) {
preferences.edit().putBoolean(key, value).apply();
preferences.edit().putBoolean(key, value).commit();
}
/**

View file

@ -9,36 +9,36 @@ import android.util.AttributeSet;
import app.revanced.extension.shared.Logger;
/**
* Simple preference that opens a url when clicked.
* Simple preference that opens a URL when clicked.
*/
@SuppressWarnings("deprecation")
public class UrlLinkPreference extends Preference {
public class URLLinkPreference extends Preference {
protected String externalUrl;
protected String externalURL;
{
setOnPreferenceClickListener(pref -> {
if (externalUrl == null) {
if (externalURL == null) {
Logger.printException(() -> "URL not set " + getClass().getSimpleName());
return false;
}
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(externalUrl));
i.setData(Uri.parse(externalURL));
pref.getContext().startActivity(i);
return true;
});
}
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public UrlLinkPreference(Context context, AttributeSet attrs) {
public URLLinkPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public UrlLinkPreference(Context context) {
public URLLinkPreference(Context context) {
super(context);
}
}

View file

@ -20,7 +20,7 @@ import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
import app.revanced.extension.shared.settings.preference.URLLinkPreference;
/**
* Abstract base class for search result items, defining common fields and behavior.
@ -167,7 +167,7 @@ public abstract class BaseSearchResultItem {
if (pref instanceof SwitchPreference) return ViewType.SWITCH;
if (pref instanceof ListPreference) return ViewType.LIST;
if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER;
if (pref instanceof UrlLinkPreference) return ViewType.URL_LINK;
if (pref instanceof URLLinkPreference) return ViewType.URL_LINK;
if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS;
return ViewType.REGULAR;
}

View file

@ -36,7 +36,7 @@ import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
import app.revanced.extension.shared.settings.preference.URLLinkPreference;
import app.revanced.extension.shared.ui.ColorDot;
/**
@ -436,7 +436,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
}
/**
* Normalizes string for comparison (removes extra characters, spaces etc).
* Normalizes string for comparison (removes extra characters, spaces etc.).
*/
protected String normalizeString(String input) {
if (TextUtils.isEmpty(input)) return "";
@ -609,8 +609,8 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
boolean hasNavigationCapability(Preference preference) {
// PreferenceScreen always allows navigation.
if (preference instanceof PreferenceScreen) return true;
// UrlLinkPreference does not navigate to a new screen, it opens an external URL.
if (preference instanceof UrlLinkPreference) return false;
// URLLinkPreference does not navigate to a new screen, it opens an external URL.
if (preference instanceof URLLinkPreference) return false;
// Other group types that might have their own screens.
if (preference instanceof PreferenceGroup) {
// Check if it has its own fragment or intent.

View file

@ -31,7 +31,7 @@ import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.spoof.ClientType;
/**
* Video streaming data. Fetching is tied to the behavior YT uses,
* Video streaming data. Fetching is tied to the behavior YT uses,
* where this class fetches the streams only when YT fetches.
* <p>
* Effectively the cache expiration of these fetches is the same as the stock app,
@ -86,7 +86,7 @@ public class StreamingDataRequest {
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(