feat(YouTube Music): Add Hide layout components patch (#6365)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
156441d3cf
commit
71ce8230a9
43 changed files with 1114 additions and 952 deletions
|
|
@ -0,0 +1,164 @@
|
|||
package app.revanced.extension.shared.patches.components;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
|
||||
/**
|
||||
* Allows custom filtering using a path and optionally a proto buffer string.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class CustomFilter extends Filter {
|
||||
|
||||
private static void showInvalidSyntaxToast(@NonNull String expression) {
|
||||
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
|
||||
}
|
||||
|
||||
private static class CustomFilterGroup extends StringFilterGroup {
|
||||
/**
|
||||
* Optional character for the path that indicates the custom filter path must match the start.
|
||||
* Must be the first character of the expression.
|
||||
*/
|
||||
public static final String SYNTAX_STARTS_WITH = "^";
|
||||
|
||||
/**
|
||||
* Optional character that separates the path from a proto buffer string pattern.
|
||||
*/
|
||||
public static final String SYNTAX_BUFFER_SYMBOL = "$";
|
||||
|
||||
/**
|
||||
* @return the parsed objects
|
||||
*/
|
||||
@NonNull
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
|
||||
String rawCustomFilterText = YouTubeAndMusicSettings.CUSTOM_FILTER_STRINGS.get();
|
||||
if (rawCustomFilterText.isBlank()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Map key is the path including optional special characters (^ and/or $)
|
||||
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
|
||||
|
||||
for (String expression : rawCustomFilterText.split("\n")) {
|
||||
if (expression.isBlank()) continue;
|
||||
|
||||
Matcher matcher = pattern.matcher(expression);
|
||||
if (!matcher.find()) {
|
||||
showInvalidSyntaxToast(expression);
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
|
||||
showInvalidSyntaxToast(expression);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use one group object for all expressions with the same path.
|
||||
// This ensures the buffer is searched exactly once
|
||||
// when multiple paths are used with different buffer strings.
|
||||
CustomFilterGroup group = result.get(mapKey);
|
||||
if (group == null) {
|
||||
group = new CustomFilterGroup(pathStartsWith, path);
|
||||
result.put(mapKey, group);
|
||||
}
|
||||
if (hasBufferSymbol) {
|
||||
group.addBufferString(bufferString);
|
||||
}
|
||||
}
|
||||
|
||||
return result.values();
|
||||
}
|
||||
|
||||
final boolean startsWith;
|
||||
ByteTrieSearch bufferSearch;
|
||||
|
||||
CustomFilterGroup(boolean startsWith, @NonNull String path) {
|
||||
super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
|
||||
this.startsWith = startsWith;
|
||||
}
|
||||
|
||||
void addBufferString(@NonNull String bufferString) {
|
||||
if (bufferSearch == null) {
|
||||
bufferSearch = new ByteTrieSearch();
|
||||
}
|
||||
bufferSearch.addPattern(bufferString.getBytes());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("CustomFilterGroup{");
|
||||
builder.append("path=");
|
||||
if (startsWith) builder.append(SYNTAX_STARTS_WITH);
|
||||
builder.append(filters[0]);
|
||||
|
||||
if (bufferSearch != null) {
|
||||
String delimitingCharacter = "❙";
|
||||
builder.append(", bufferStrings=");
|
||||
builder.append(delimitingCharacter);
|
||||
for (byte[] bufferString : bufferSearch.getPatterns()) {
|
||||
builder.append(new String(bufferString));
|
||||
builder.append(delimitingCharacter);
|
||||
}
|
||||
}
|
||||
builder.append("}");
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public CustomFilter() {
|
||||
Collection<CustomFilterGroup> groups = CustomFilterGroup.parseCustomFilterGroups();
|
||||
|
||||
if (!groups.isEmpty()) {
|
||||
CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]);
|
||||
Logger.printDebug(()-> "Using Custom filters: " + Arrays.toString(groupsArray));
|
||||
addPathCallbacks(groupsArray);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
// All callbacks are custom filter groups.
|
||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||
if (custom.startsWith && contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (custom.bufferSearch == null) {
|
||||
return true; // No buffer filter, only path filtering.
|
||||
}
|
||||
|
||||
return custom.bufferSearch.matches(buffer);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
|
||||
/**
|
||||
* 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 FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
||||
*
|
||||
* All callbacks must be registered before the constructor completes.
|
||||
*/
|
||||
public 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<StringFilterGroup> identifierCallbacks = new ArrayList<>();
|
||||
/**
|
||||
* Path callbacks. Do not add to this instance,
|
||||
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||
*/
|
||||
protected final List<StringFilterGroup> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
*/
|
||||
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,213 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
|
||||
public abstract class FilterGroup<T> {
|
||||
public 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);
|
||||
|
||||
|
||||
public static class StringFilterGroup extends FilterGroup<String> {
|
||||
|
||||
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 FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
|
||||
* which uses a prefix tree to give better performance.
|
||||
*/
|
||||
public static class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.TrieSearch;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
||||
public abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
||||
|
||||
private final List<T> filterGroups = new ArrayList<>();
|
||||
private final TrieSearch<V> search = createSearchGraph();
|
||||
|
||||
@SafeVarargs
|
||||
public 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<T> iterator() {
|
||||
return filterGroups.iterator();
|
||||
}
|
||||
|
||||
public FilterGroup.FilterGroupResult check(V stack) {
|
||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
||||
search.matches(stack, result);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
protected abstract TrieSearch<V> createSearchGraph();
|
||||
|
||||
public static final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
|
||||
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.
|
||||
*/
|
||||
public static final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
||||
protected ByteTrieSearch createSearchGraph() {
|
||||
return new ByteTrieSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
package app.revanced.extension.shared.patches.litho;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class LithoFilterPatch {
|
||||
/**
|
||||
* Simple wrapper to pass the litho parameters through the prefix search.
|
||||
*/
|
||||
private static final class LithoFilterParameters {
|
||||
final String identifier;
|
||||
final String path;
|
||||
final byte[] buffer;
|
||||
|
||||
LithoFilterParameters(String lithoIdentifier, String lithoPath, byte[] buffer) {
|
||||
this.identifier = lithoIdentifier;
|
||||
this.path = lithoPath;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
// Estimate the percentage of the buffer that are Strings.
|
||||
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
|
||||
builder.append( "ID: ");
|
||||
builder.append(identifier);
|
||||
builder.append(" Path: ");
|
||||
builder.append(path);
|
||||
if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) {
|
||||
builder.append(" BufferStrings: ");
|
||||
findAsciiStrings(builder, buffer);
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through a byte array for all ASCII strings.
|
||||
*/
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Litho layout fixed thread pool size override.
|
||||
* <p>
|
||||
* Unpatched YouTube uses a layout fixed thread pool between 1 and 3 threads:
|
||||
* <pre>
|
||||
* 1 thread - > Device has less than 6 cores
|
||||
* 2 threads -> Device has over 6 cores and less than 6GB of memory
|
||||
* 3 threads -> Device has over 6 cores and more than 6GB of memory
|
||||
* </pre>
|
||||
*
|
||||
* Using more than 1 thread causes layout issues such as the You tab watch/playlist shelf
|
||||
* that is sometimes incorrectly hidden (ReVanced is not hiding it), and seems to
|
||||
* fix a race issue if using the active navigation tab status with litho filtering.
|
||||
*/
|
||||
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
|
||||
|
||||
/**
|
||||
* Placeholder for actual filters.
|
||||
*/
|
||||
private static final class DummyFilter extends Filter { }
|
||||
|
||||
private static final Filter[] filters = new Filter[] {
|
||||
new DummyFilter() // Replaced patching, do not touch.
|
||||
};
|
||||
|
||||
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<byte[]> 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<StringFilterGroup> groups,
|
||||
Filter.FilterContentType type) {
|
||||
String filterSimpleName = filter.getClass().getSimpleName();
|
||||
|
||||
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;
|
||||
final boolean isFiltered = filter.isFiltered(parameters.identifier,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return isFiltered;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.22+
|
||||
*/
|
||||
public static void setProtoBuffer(byte[] buffer) {
|
||||
// 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.
|
||||
bufferThreadLocal.set(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.21 and lower.
|
||||
*/
|
||||
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
|
||||
// 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 (buffer == null || !buffer.hasArray()) {
|
||||
// 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 or empty buffer: " + buffer);
|
||||
} else {
|
||||
setProtoBuffer(buffer.array());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isFiltered(String lithoIdentifier, StringBuilder pathBuilder) {
|
||||
try {
|
||||
if (lithoIdentifier.isEmpty() && pathBuilder.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] buffer = bufferThreadLocal.get();
|
||||
// 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 (buffer == null) {
|
||||
buffer = EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
|
||||
LithoFilterParameters parameter = new LithoFilterParameters(
|
||||
lithoIdentifier, pathBuilder.toString(), buffer);
|
||||
Logger.printDebug(() -> "Searching " + parameter);
|
||||
|
||||
if (identifierSearchTree.matches(parameter.identifier, parameter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pathSearchTree.matches(parameter.path, parameter)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isFiltered failure", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getExecutorCorePoolSize(int originalCorePoolSize) {
|
||||
if (originalCorePoolSize != LITHO_LAYOUT_THREAD_POOL_SIZE) {
|
||||
Logger.printDebug(() -> "Overriding core thread pool size from: " + originalCorePoolSize
|
||||
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
return LITHO_LAYOUT_THREAD_POOL_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getExecutorMaxThreads(int originalMaxThreads) {
|
||||
if (originalMaxThreads != LITHO_LAYOUT_THREAD_POOL_SIZE) {
|
||||
Logger.printDebug(() -> "Overriding max thread pool size from: " + originalMaxThreads
|
||||
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
return LITHO_LAYOUT_THREAD_POOL_SIZE;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package app.revanced.extension.shared.settings;
|
||||
|
||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
|
||||
public class YouTubeAndMusicSettings extends BaseSettings {
|
||||
// Custom filter
|
||||
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
|
||||
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
|
||||
|
||||
// Miscellaneous
|
||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
|
||||
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue