mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-11-30 15:52:55 +01:00
perf(YouTube): Filter litho components using prefix tree (#447)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
0207496926
commit
18f29004b8
@ -2,21 +2,25 @@ package app.revanced.integrations.patches.components;
|
||||
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.StringTrieSearch;
|
||||
|
||||
|
||||
public final class AdsFilter extends Filter {
|
||||
private final String[] exceptions;
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
|
||||
public AdsFilter() {
|
||||
exceptions = new String[]{
|
||||
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",
|
||||
};
|
||||
"library_recent_shelf"
|
||||
);
|
||||
|
||||
final var buttonedAd = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_BUTTONED_ADS,
|
||||
@ -95,11 +99,12 @@ public final class AdsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) {
|
||||
if (ReVancedUtils.containsAny(path, exceptions))
|
||||
public boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
if (exceptions.matches(path))
|
||||
return false;
|
||||
|
||||
return super.isFiltered(path, identifier, _protobufBufferArray);
|
||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
final class ButtonsFilter extends Filter {
|
||||
@ -33,7 +35,8 @@ final class ButtonsFilter extends Filter {
|
||||
SettingsEnum.HIDE_ACTION_BUTTONS,
|
||||
"ContainerType|video_action_button",
|
||||
"|CellType|CollectionType|CellType|ContainerType|button.eml|"
|
||||
)
|
||||
),
|
||||
actionBarRule
|
||||
);
|
||||
}
|
||||
|
||||
@ -45,10 +48,12 @@ final class ButtonsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) {
|
||||
if (isEveryFilterGroupEnabled())
|
||||
if (actionBarRule.check(identifier).isFiltered()) return true;
|
||||
public boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
if (matchedGroup == actionBarRule) {
|
||||
return isEveryFilterGroupEnabled();
|
||||
}
|
||||
|
||||
return super.isFiltered(path, identifier, _protobufBufferArray);
|
||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,16 @@ package app.revanced.integrations.patches.components;
|
||||
|
||||
|
||||
import android.os.Build;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.StringTrieSearch;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public final class LayoutComponentsFilter extends Filter {
|
||||
private final String[] exceptions;
|
||||
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
private final CustomFilterGroup custom;
|
||||
|
||||
private static final ByteArrayAsStringFilterGroup mixPlaylists = new ByteArrayAsStringFilterGroup(
|
||||
@ -20,13 +21,13 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions = new String[]{
|
||||
exceptions.addPatterns(
|
||||
"home_video_with_context",
|
||||
"related_video_with_context",
|
||||
"comment_thread", // skip filtering anything in the comments
|
||||
"|comment.", // skip filtering anything in the comments replies
|
||||
"library_recent_shelf",
|
||||
};
|
||||
"library_recent_shelf"
|
||||
);
|
||||
|
||||
custom = new CustomFilterGroup(
|
||||
SettingsEnum.CUSTOM_FILTER,
|
||||
@ -160,7 +161,8 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
artistCard,
|
||||
imageShelf,
|
||||
subscribersCommunityGuidelines,
|
||||
channelMemberShelf
|
||||
channelMemberShelf,
|
||||
custom
|
||||
);
|
||||
|
||||
this.identifierFilterGroups.addAll(
|
||||
@ -170,19 +172,21 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) {
|
||||
if (custom.isEnabled() && custom.check(path).isFiltered())
|
||||
return true;
|
||||
|
||||
if (ReVancedUtils.containsAny(path, exceptions))
|
||||
public boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
if (matchedGroup != custom && exceptions.matches(path))
|
||||
return false; // Exceptions are not filtered.
|
||||
|
||||
return super.isFiltered(path, identifier, _protobufBufferArray);
|
||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
}
|
||||
|
||||
|
||||
// Called from a different place then the other filters.
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Called from a different place then the other filters.
|
||||
*/
|
||||
public static boolean filterMixPlaylists(final byte[] bytes) {
|
||||
return mixPlaylists.isEnabled() && mixPlaylists.check(bytes).isFiltered();
|
||||
return mixPlaylists.check(bytes).isFiltered();
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,30 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.Spliterator;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
|
||||
abstract class FilterGroup<T> {
|
||||
final static class FilterGroupResult {
|
||||
private final boolean filtered;
|
||||
private final SettingsEnum setting;
|
||||
SettingsEnum setting;
|
||||
boolean filtered;
|
||||
|
||||
public FilterGroupResult(final SettingsEnum setting, final boolean filtered) {
|
||||
FilterGroupResult(SettingsEnum setting, boolean filtered) {
|
||||
this.setting = setting;
|
||||
this.filtered = filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* A null value if the group has no setting,
|
||||
* or if no match is returned from {@link FilterGroupList#check(Object)}.
|
||||
*/
|
||||
public SettingsEnum getSetting() {
|
||||
return setting;
|
||||
}
|
||||
@ -48,51 +47,87 @@ abstract class FilterGroup<T> {
|
||||
public FilterGroup(final SettingsEnum 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.getBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If {@link FilterGroupList} should include this group when searching.
|
||||
* By default, all filters are included except non enabled settings that require reboot.
|
||||
*/
|
||||
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<String> {
|
||||
|
||||
/**
|
||||
* {@link FilterGroup#FilterGroup(SettingsEnum, Object[])}
|
||||
*/
|
||||
public StringFilterGroup(final SettingsEnum setting, final String... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final String string) {
|
||||
return new FilterGroupResult(setting, string != null && ReVancedUtils.containsAny(string, filters));
|
||||
return new FilterGroupResult(setting,
|
||||
(setting == null || setting.getBoolean()) && ReVancedUtils.containsAny(string, filters));
|
||||
}
|
||||
}
|
||||
|
||||
final class CustomFilterGroup extends StringFilterGroup {
|
||||
|
||||
/**
|
||||
* {@link FilterGroup#FilterGroup(SettingsEnum, Object[])}
|
||||
*/
|
||||
public CustomFilterGroup(final SettingsEnum setting, final SettingsEnum filter) {
|
||||
super(setting, filter.getString().split(","));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<byte[]> {
|
||||
|
||||
private int[][] failurePatterns;
|
||||
|
||||
// Modified implementation from https://stackoverflow.com/a/1507813
|
||||
private int indexOf(final byte[] data, final byte[] pattern) {
|
||||
if (data.length == 0)
|
||||
return -1;
|
||||
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[] failure = new int[pattern.length];
|
||||
final int patternLength = pattern.length;
|
||||
final int[] failure = new int[patternLength];
|
||||
|
||||
int j = 0;
|
||||
for (int i = 1; i < pattern.length; i++) {
|
||||
for (int i = 1, j = 0; i < patternLength; i++) {
|
||||
while (j > 0 && pattern[j] != pattern[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
@ -101,54 +136,43 @@ class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||
}
|
||||
failure[i] = j;
|
||||
}
|
||||
|
||||
// Finds the first occurrence of the pattern in the byte array using
|
||||
// KMP matching algorithm.
|
||||
|
||||
j = 0;
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
while (j > 0 && pattern[j] != data[i]) {
|
||||
j = failure[j - 1];
|
||||
}
|
||||
if (pattern[j] == data[i]) {
|
||||
j++;
|
||||
}
|
||||
if (j == pattern.length) {
|
||||
return i - pattern.length + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
return failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link FilterGroup#FilterGroup(SettingsEnum, Object[])}
|
||||
*/
|
||||
public ByteArrayFilterGroup(final SettingsEnum setting, final byte[]... filters) {
|
||||
public ByteArrayFilterGroup(SettingsEnum setting, byte[]... filters) {
|
||||
super(setting, filters);
|
||||
}
|
||||
|
||||
private void buildFailurePatterns() {
|
||||
LogHelper.printDebug(() -> "Building failure array for: " + this);
|
||||
failurePatterns = new int[filters.length][];
|
||||
int i = 0;
|
||||
for (byte[] pattern : filters) {
|
||||
failurePatterns[i++] = createFailurePattern(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterGroupResult check(final byte[] bytes) {
|
||||
var matched = false;
|
||||
for (byte[] filter : filters) {
|
||||
if (indexOf(bytes, filter) == -1)
|
||||
continue;
|
||||
|
||||
matched = true;
|
||||
break;
|
||||
if (isEnabled()) {
|
||||
if (failurePatterns == null) {
|
||||
buildFailurePatterns(); // Lazy load.
|
||||
}
|
||||
for (int i = 0, length = filters.length; i < length; i++) {
|
||||
if (indexOf(bytes, filters[i], failurePatterns[i]) >= 0) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final var filtered = matched;
|
||||
return new FilterGroupResult(setting, filtered);
|
||||
return new FilterGroupResult(setting, matched);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class ByteArrayAsStringFilterGroup extends ByteArrayFilterGroup {
|
||||
|
||||
/**
|
||||
* {@link ByteArrayFilterGroup#ByteArrayFilterGroup(SettingsEnum, byte[]...)}
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public ByteArrayAsStringFilterGroup(SettingsEnum setting, String... filters) {
|
||||
super(setting, Arrays.stream(filters).map(String::getBytes).toArray(byte[][]::new));
|
||||
@ -156,11 +180,38 @@ final class ByteArrayAsStringFilterGroup extends ByteArrayFilterGroup {
|
||||
}
|
||||
|
||||
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
||||
private final ArrayList<T> filterGroups = new ArrayList<>();
|
||||
|
||||
private final List<T> filterGroups = new ArrayList<>();
|
||||
/**
|
||||
* Search graph. Created only if needed.
|
||||
*/
|
||||
private TrieSearch<V> search;
|
||||
|
||||
@SafeVarargs
|
||||
protected final void addAll(final T... filterGroups) {
|
||||
this.filterGroups.addAll(Arrays.asList(filterGroups));
|
||||
protected final void addAll(final T... groups) {
|
||||
filterGroups.addAll(Arrays.asList(groups));
|
||||
search = null; // Rebuild, if already created.
|
||||
}
|
||||
|
||||
protected final void buildSearch() {
|
||||
LogHelper.printDebug(() -> "Creating prefix search tree for: " + this);
|
||||
search = createSearchGraph();
|
||||
for (T group : filterGroups) {
|
||||
if (!group.includeInSearch()) {
|
||||
continue;
|
||||
}
|
||||
for (V pattern : group.filters) {
|
||||
search.addPattern(pattern, (textSearched, matchedStartIndex, callbackParameter) -> {
|
||||
if (group.isEnabled()) {
|
||||
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
|
||||
result.setting = group.setting;
|
||||
result.filtered = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -182,94 +233,207 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
|
||||
return filterGroups.spliterator();
|
||||
}
|
||||
|
||||
protected boolean contains(final V stack) {
|
||||
for (T filterGroup : this) {
|
||||
if (!filterGroup.isEnabled())
|
||||
continue;
|
||||
|
||||
var result = filterGroup.check(stack);
|
||||
if (result.isFiltered()) {
|
||||
return true;
|
||||
}
|
||||
protected FilterGroup.FilterGroupResult check(V stack) {
|
||||
if (search == null) {
|
||||
buildSearch(); // Lazy load.
|
||||
}
|
||||
|
||||
return false;
|
||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(null, false);
|
||||
search.matches(stack, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected abstract TrieSearch<V> createSearchGraph();
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
||||
protected ByteTrieSearch createSearchGraph() {
|
||||
return new ByteTrieSearch();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Filter {
|
||||
final protected StringFilterGroupList pathFilterGroups = new StringFilterGroupList();
|
||||
final protected StringFilterGroupList identifierFilterGroups = new StringFilterGroupList();
|
||||
final protected ByteArrayFilterGroupList protobufBufferFilterGroups = new ByteArrayFilterGroupList();
|
||||
/**
|
||||
* All group filters must be set before the constructor call completes.
|
||||
* Otherwise {@link #isFiltered(String, String, byte[], FilterGroupList, FilterGroup, int)}
|
||||
* will never be called for any matches.
|
||||
*/
|
||||
|
||||
protected final StringFilterGroupList pathFilterGroups = new StringFilterGroupList();
|
||||
protected final StringFilterGroupList identifierFilterGroups = new StringFilterGroupList();
|
||||
/**
|
||||
* A collection of {@link ByteArrayFilterGroup} that are always searched for (no matter what).
|
||||
*
|
||||
* If possible, avoid adding values to this list and instead use a path or identifier filter
|
||||
* for the item you are looking for. Then inside
|
||||
* {@link #isFiltered(String, String, byte[], FilterGroupList, FilterGroup, int)},
|
||||
* the buffer can then be searched using using a different
|
||||
* {@link ByteArrayFilterGroupList} or a {@link ByteArrayFilterGroup}.
|
||||
* This way, the expensive buffer searching only occurs if the cheap and fast path/identifier is already found.
|
||||
*/
|
||||
protected final ByteArrayFilterGroupList protobufBufferFilterGroups = new ByteArrayFilterGroupList();
|
||||
|
||||
/**
|
||||
* Check if the given path, identifier or protobuf buffer is filtered by any
|
||||
* {@link FilterGroup}. Method is called off the main thread.
|
||||
* Called after an enabled filter has been matched.
|
||||
* Default implementation is to always filter the matched item.
|
||||
* Subclasses can perform additional or different checks if needed.
|
||||
*
|
||||
* @return True if filtered, false otherwise.
|
||||
* Method is called off the main thread.
|
||||
*
|
||||
* @param matchedList The list the group filter belongs to.
|
||||
* @param matchedGroup The actual filter that matched.
|
||||
* @param matchedIndex Matched index of string/array.
|
||||
* @return True if the litho item should be filtered out.
|
||||
*/
|
||||
boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) {
|
||||
if (pathFilterGroups.contains(path)) {
|
||||
LogHelper.printDebug(() -> String.format("Filtered path: %s", path));
|
||||
return true;
|
||||
@SuppressWarnings("rawtypes")
|
||||
boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
if (SettingsEnum.DEBUG.getBoolean()) {
|
||||
if (pathFilterGroups == matchedList) {
|
||||
LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered path: " + path);
|
||||
} else if (identifierFilterGroups == matchedList) {
|
||||
LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered identifier: " + identifier);
|
||||
} else if (protobufBufferFilterGroups == matchedList) {
|
||||
LogHelper.printDebug(() -> getClass().getSimpleName() + " Filtered from protobuf-buffer");
|
||||
}
|
||||
}
|
||||
|
||||
if (identifierFilterGroups.contains(identifier)) {
|
||||
LogHelper.printDebug(() -> String.format("Filtered identifier: %s", identifier));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (protobufBufferFilterGroups.contains(protobufBufferArray)) {
|
||||
LogHelper.printDebug(() -> "Filtered from protobuf-buffer");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@SuppressWarnings("unused")
|
||||
public final class LithoFilterPatch {
|
||||
/**
|
||||
* Simple wrapper to pass the litho parameters through the prefix search.
|
||||
*/
|
||||
private static final class LithoFilterParameters {
|
||||
final String path;
|
||||
final String identifier;
|
||||
final byte[] protoBuffer;
|
||||
|
||||
LithoFilterParameters(StringBuilder lithoPath, String lithoIdentifier, ByteBuffer protoBuffer) {
|
||||
this.path = lithoPath.toString();
|
||||
this.identifier = lithoIdentifier;
|
||||
this.protoBuffer = protoBuffer.array();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
// Estimate the percentage of the buffer that are Strings.
|
||||
StringBuilder builder = new StringBuilder(protoBuffer.length / 2);
|
||||
builder.append( "ID: ");
|
||||
builder.append(identifier);
|
||||
builder.append(" Path: ");
|
||||
builder.append(path);
|
||||
// TODO: allow turning on/off buffer logging with a debug setting?
|
||||
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) {
|
||||
builder.append(new String(buffer, start, end - start));
|
||||
builder.append(delimitingCharacter);
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final Filter[] filters = new Filter[] {
|
||||
new DummyFilter() // Replaced by patch.
|
||||
};
|
||||
|
||||
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
||||
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
|
||||
private static final ByteTrieSearch protoSearchTree = new ByteTrieSearch();
|
||||
|
||||
static {
|
||||
for (Filter filter : filters) {
|
||||
filterGroupLists(pathSearchTree, filter, filter.pathFilterGroups);
|
||||
filterGroupLists(identifierSearchTree, filter, filter.identifierFilterGroups);
|
||||
filterGroupLists(protoSearchTree, filter, filter.protobufBufferFilterGroups);
|
||||
}
|
||||
|
||||
LogHelper.printDebug(() -> "Using: "
|
||||
+ pathSearchTree.numberOfPatterns() + " path filters"
|
||||
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB), "
|
||||
+ identifierSearchTree.numberOfPatterns() + " identifier filters"
|
||||
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
|
||||
+ protoSearchTree.numberOfPatterns() + " buffer filters"
|
||||
+ " (" + protoSearchTree.getEstimatedMemorySize() + " KB)");
|
||||
}
|
||||
|
||||
private static <T> void filterGroupLists(TrieSearch<T> pathSearchTree,
|
||||
Filter filter, FilterGroupList<T, ? extends FilterGroup<T>> list) {
|
||||
for (FilterGroup<T> group : list) {
|
||||
if (!group.includeInSearch()) {
|
||||
continue;
|
||||
}
|
||||
for (T pattern : group.filters) {
|
||||
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, callbackParameter) -> {
|
||||
if (!group.isEnabled()) return false;
|
||||
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
||||
return filter.isFiltered(parameters.path, parameters.identifier, parameters.protoBuffer,
|
||||
list, group, matchedStartIndex);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean filter(final StringBuilder pathBuilder, final String identifier,
|
||||
final ByteBuffer protobufBuffer) {
|
||||
// TODO: Maybe this can be moved to the Filter class, to prevent unnecessary
|
||||
// string creation
|
||||
// because some filters might not need the path.
|
||||
var path = pathBuilder.toString();
|
||||
public static boolean filter(@NonNull StringBuilder pathBuilder, @Nullable String lithoIdentifier,
|
||||
@NonNull ByteBuffer protobufBuffer) {
|
||||
try {
|
||||
// It is assumed that protobufBuffer is empty as well in this case.
|
||||
if (pathBuilder.length() == 0)
|
||||
return false;
|
||||
|
||||
// It is assumed that protobufBuffer is empty as well in this case.
|
||||
if (path.isEmpty())
|
||||
return false;
|
||||
LithoFilterParameters parameter = new LithoFilterParameters(pathBuilder, lithoIdentifier, protobufBuffer);
|
||||
LogHelper.printDebug(() -> "Searching " + parameter);
|
||||
|
||||
LogHelper.printDebug(() -> String.format(
|
||||
"Searching (ID: %s, Buffer-size: %s): %s",
|
||||
identifier, protobufBuffer.remaining(), path));
|
||||
|
||||
var protobufBufferArray = protobufBuffer.array();
|
||||
|
||||
for (var filter : filters) {
|
||||
var filtered = filter.isFiltered(path, identifier, protobufBufferArray);
|
||||
|
||||
LogHelper.printDebug(
|
||||
() -> String.format("%s (ID: %s): %s", filtered ? "Filtered" : "Unfiltered", identifier, path));
|
||||
|
||||
if (filtered)
|
||||
return true;
|
||||
if (pathSearchTree.matches(parameter.path, parameter)) return true;
|
||||
if (parameter.identifier != null) {
|
||||
if (identifierSearchTree.matches(parameter.identifier, parameter)) return true;
|
||||
}
|
||||
if (protoSearchTree.matches(parameter.protoBuffer, parameter)) return true;
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Litho filter failure", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
// Abuse LithoFilter for CustomPlaybackSpeedPatch.
|
||||
public final class PlaybackSpeedMenuFilterPatch extends Filter {
|
||||
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
|
||||
@ -13,8 +15,9 @@ public final class PlaybackSpeedMenuFilterPatch extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) {
|
||||
isPlaybackSpeedMenuVisible = super.isFiltered(path, identifier, protobufBufferArray);
|
||||
boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
isPlaybackSpeedMenuVisible = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -2,14 +2,22 @@ package app.revanced.integrations.patches.components;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
|
||||
// Search the buffer only if the flyout menu identifier is found.
|
||||
// Handle the searching in this class instead of adding to the global filter group (which searches all the time)
|
||||
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
public PlayerFlyoutMenuItemsFilter() {
|
||||
protobufBufferFilterGroups.addAll(
|
||||
identifierFilterGroups.addAll(new StringFilterGroup(null, "overflow_menu_item.eml|"));
|
||||
|
||||
flyoutFilterGroupList.addAll(
|
||||
new ByteArrayAsStringFilterGroup(
|
||||
SettingsEnum.HIDE_QUALITY_MENU,
|
||||
"yt_outline_gear"
|
||||
@ -54,10 +62,13 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(String path, String identifier, byte[] _protobufBufferArray) {
|
||||
if (identifier != null && identifier.startsWith("overflow_menu_item.eml|"))
|
||||
return super.isFiltered(path, identifier, _protobufBufferArray);
|
||||
|
||||
boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
// Only 1 group is added to the parent class, so the matched group must be the overflow menu.
|
||||
if (matchedIndex == 0 && flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
|
||||
// Super class handles logging.
|
||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedList, matchedGroup, matchedIndex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,40 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import android.view.View;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.hideViewBy1dpUnderCondition;
|
||||
import static app.revanced.integrations.utils.ReVancedUtils.hideViewUnderCondition;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
public final class ShortsFilter extends Filter {
|
||||
// Set by patch.
|
||||
public static PivotBar pivotBar;
|
||||
final StringFilterGroupList shortsFilterGroup = new StringFilterGroupList();
|
||||
private final StringFilterGroup reelChannelBar = new StringFilterGroup(
|
||||
null,
|
||||
"reel_channel_bar"
|
||||
);
|
||||
private static final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar";
|
||||
public static PivotBar pivotBar; // Set by patch.
|
||||
|
||||
private final StringFilterGroup channelBar;
|
||||
private final StringFilterGroup soundButton;
|
||||
private final StringFilterGroup infoPanel;
|
||||
|
||||
public ShortsFilter() {
|
||||
channelBar = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_CHANNEL_BAR,
|
||||
REEL_CHANNEL_BAR_PATH
|
||||
);
|
||||
|
||||
soundButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_SOUND_BUTTON,
|
||||
"reel_pivot_button"
|
||||
);
|
||||
|
||||
infoPanel = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_INFO_PANEL,
|
||||
"shorts_info_panel_overview"
|
||||
);
|
||||
|
||||
final var thanksButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_THANKS_BUTTON,
|
||||
"suggested_action"
|
||||
@ -32,21 +50,6 @@ public final class ShortsFilter extends Filter {
|
||||
"sponsor_button"
|
||||
);
|
||||
|
||||
final var soundButton = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_SOUND_BUTTON,
|
||||
"reel_pivot_button"
|
||||
);
|
||||
|
||||
final var infoPanel = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_INFO_PANEL,
|
||||
"shorts_info_panel_overview"
|
||||
);
|
||||
|
||||
final var channelBar = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS_CHANNEL_BAR,
|
||||
"reel_channel_bar"
|
||||
);
|
||||
|
||||
final var shorts = new StringFilterGroup(
|
||||
SettingsEnum.HIDE_SHORTS,
|
||||
"shorts_shelf",
|
||||
@ -55,22 +58,21 @@ public final class ShortsFilter extends Filter {
|
||||
"shorts_video_cell"
|
||||
);
|
||||
|
||||
shortsFilterGroup.addAll(soundButton, infoPanel);
|
||||
pathFilterGroups.addAll(joinButton, subscribeButton, channelBar);
|
||||
pathFilterGroups.addAll(joinButton, subscribeButton, channelBar, soundButton, infoPanel);
|
||||
identifierFilterGroups.addAll(shorts, thanksButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(final String path, final String identifier,
|
||||
final byte[] protobufBufferArray) {
|
||||
boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
if (matchedGroup == soundButton || matchedGroup == infoPanel || matchedGroup == channelBar) return true;
|
||||
|
||||
// Filter the path only when reelChannelBar is visible.
|
||||
if (reelChannelBar.check(path).isFiltered())
|
||||
if (this.pathFilterGroups.contains(path)) return true;
|
||||
if (pathFilterGroups == matchedList) {
|
||||
return path.contains(REEL_CHANNEL_BAR_PATH);
|
||||
}
|
||||
|
||||
if (shortsFilterGroup.contains(path)) return true;
|
||||
|
||||
return this.identifierFilterGroups.contains(identifier);
|
||||
return identifierFilterGroups == matchedList;
|
||||
}
|
||||
|
||||
public static void hideShortsShelf(final View shortsShelfView) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.integrations.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
|
||||
// Abuse LithoFilter for OldVideoQualityMenuPatch.
|
||||
@ -15,8 +17,9 @@ public final class VideoQualityMenuFilterPatch extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) {
|
||||
isVideoQualityMenuVisible = super.isFiltered(path, identifier, protobufBufferArray);
|
||||
boolean isFiltered(String path, @Nullable String identifier, byte[] protobufBufferArray,
|
||||
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
|
||||
isVideoQualityMenuVisible = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ public final class OldVideoQualityMenuPatch {
|
||||
// The quality menu is a RecyclerView with 3 children. The third child is the "Advanced" quality menu.
|
||||
addRecyclerListener(linearLayout, 3, 2, recyclerView -> {
|
||||
// Check if the current view is the quality menu.
|
||||
if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) {// Hide the video quality menu.
|
||||
if (VideoQualityMenuFilterPatch.isVideoQualityMenuVisible) {
|
||||
VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false;
|
||||
linearLayout.setVisibility(View.GONE);
|
||||
|
||||
// Click the "Advanced" quality menu to show the "old" quality menu.
|
||||
|
@ -1,22 +1,19 @@
|
||||
package app.revanced.integrations.patches.playback.speed;
|
||||
|
||||
import static app.revanced.integrations.patches.playback.quality.OldVideoQualityMenuPatch.addRecyclerListener;
|
||||
|
||||
import android.preference.ListPreference;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.litho.ComponentHost;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import app.revanced.integrations.patches.components.PlaybackSpeedMenuFilterPatch;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import com.facebook.litho.ComponentHost;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import static app.revanced.integrations.patches.playback.quality.OldVideoQualityMenuPatch.addRecyclerListener;
|
||||
|
||||
public class CustomPlaybackSpeedPatch {
|
||||
/**
|
||||
@ -110,23 +107,24 @@ public class CustomPlaybackSpeedPatch {
|
||||
}
|
||||
|
||||
/*
|
||||
* To reduce copy paste between two similar code paths.
|
||||
* To reduce copy and paste between two similar code paths.
|
||||
*/
|
||||
public static void onFlyoutMenuCreate(final LinearLayout linearLayout) {
|
||||
// The playback rate menu is a RecyclerView with 2 children. The third child is the "Advanced" quality menu.
|
||||
addRecyclerListener(linearLayout, 2, 1, recyclerView -> {
|
||||
if (PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible &&
|
||||
recyclerView.getChildCount() == 1 &&
|
||||
recyclerView.getChildAt(0) instanceof ComponentHost
|
||||
) {
|
||||
linearLayout.setVisibility(View.GONE);
|
||||
if (PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible) {
|
||||
PlaybackSpeedMenuFilterPatch.isPlaybackSpeedMenuVisible = false;
|
||||
|
||||
// Close the new Playback speed menu and instead show the old one.
|
||||
showOldPlaybackSpeedMenu();
|
||||
if (recyclerView.getChildCount() == 1 && recyclerView.getChildAt(0) instanceof ComponentHost) {
|
||||
linearLayout.setVisibility(View.GONE);
|
||||
|
||||
// DismissView [R.id.touch_outside] is the 1st ChildView of the 3rd ParentView.
|
||||
((ViewGroup) linearLayout.getParent().getParent().getParent())
|
||||
.getChildAt(0).performClick();
|
||||
// Close the new Playback speed menu and instead show the old one.
|
||||
showOldPlaybackSpeedMenu();
|
||||
|
||||
// DismissView [R.id.touch_outside] is the 1st ChildView of the 3rd ParentView.
|
||||
((ViewGroup) linearLayout.getParent().getParent().getParent())
|
||||
.getChildAt(0).performClick();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
package app.revanced.integrations.utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ByteTrieSearch extends TrieSearch<byte[]> {
|
||||
|
||||
private static final class ByteTrieNode extends TrieNode<byte[]> {
|
||||
TrieNode<byte[]> createNode() {
|
||||
return new ByteTrieNode();
|
||||
}
|
||||
char getCharValue(byte[] text, int index) {
|
||||
return (char) text[index];
|
||||
}
|
||||
}
|
||||
|
||||
public ByteTrieSearch() {
|
||||
super(new ByteTrieNode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPattern(@NonNull byte[] pattern) {
|
||||
super.addPattern(pattern, pattern.length, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPattern(@NonNull byte[] pattern, @NonNull TriePatternMatchedCallback<byte[]> callback) {
|
||||
super.addPattern(pattern, pattern.length, Objects.requireNonNull(callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@NonNull byte[] textToSearch, @Nullable Object callbackParameter) {
|
||||
return super.matches(textToSearch, textToSearch.length, callbackParameter);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package app.revanced.integrations.utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Text pattern searching using a prefix tree (trie).
|
||||
*/
|
||||
public final class StringTrieSearch extends TrieSearch<String> {
|
||||
|
||||
private static final class StringTrieNode extends TrieNode<String> {
|
||||
TrieNode<String> createNode() {
|
||||
return new StringTrieNode();
|
||||
}
|
||||
char getCharValue(String text, int index) {
|
||||
return text.charAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
public StringTrieSearch() {
|
||||
super(new StringTrieNode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPattern(@NonNull String pattern) {
|
||||
super.addPattern(pattern, pattern.length(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPattern(@NonNull String pattern, @NonNull TriePatternMatchedCallback<String> callback) {
|
||||
super.addPattern(pattern, pattern.length(), Objects.requireNonNull(callback));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(@NonNull String textToSearch, @Nullable Object callbackParameter) {
|
||||
return super.matches(textToSearch, textToSearch.length(), callbackParameter);
|
||||
}
|
||||
}
|
@ -0,0 +1,305 @@
|
||||
package app.revanced.integrations.utils;
|
||||
|
||||
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.
|
||||
*
|
||||
* Currently only supports ASCII non-control characters (letters/numbers/symbols).
|
||||
* But could be modified to also support UTF-8 unicode.
|
||||
*/
|
||||
public abstract class TrieSearch<T> {
|
||||
|
||||
public interface TriePatternMatchedCallback<T> {
|
||||
/**
|
||||
* 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 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, 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<T> {
|
||||
final T pattern;
|
||||
final int patternLength;
|
||||
final int patternStartIndex;
|
||||
final TriePatternMatchedCallback<T> callback;
|
||||
|
||||
TrieCompressedPath(T pattern, int patternLength, int patternStartIndex, TriePatternMatchedCallback<T> callback) {
|
||||
this.pattern = pattern;
|
||||
this.patternLength = patternLength;
|
||||
this.patternStartIndex = patternStartIndex;
|
||||
this.callback = callback;
|
||||
}
|
||||
boolean matches(TrieNode<T> 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, callbackParameter);
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class TrieNode<T> {
|
||||
// Support only ASCII letters/numbers/symbols and filter out all control characters.
|
||||
private static final char MIN_VALID_CHAR = 32; // Space character.
|
||||
private static final char MAX_VALID_CHAR = 126; // 127 = delete character.
|
||||
private static final int NUMBER_OF_CHILDREN = MAX_VALID_CHAR - MIN_VALID_CHAR + 1;
|
||||
|
||||
private static boolean isInvalidRange(char character) {
|
||||
return character < MIN_VALID_CHAR || character > MAX_VALID_CHAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<T> leaf;
|
||||
|
||||
/**
|
||||
* All child nodes. Only present if no compressed leaf exist.
|
||||
*/
|
||||
@Nullable
|
||||
private TrieNode<T>[] children;
|
||||
|
||||
/**
|
||||
* Callbacks for all patterns that end at this node.
|
||||
*/
|
||||
@Nullable
|
||||
private List<TriePatternMatchedCallback<T>> endOfPatternCallback;
|
||||
|
||||
/**
|
||||
* @param pattern Pattern to add.
|
||||
* @param patternLength Length of the pattern.
|
||||
* @param patternIndex Current recursive index 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 patternLength, int patternIndex,
|
||||
@Nullable TriePatternMatchedCallback<T> 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[NUMBER_OF_CHILDREN];
|
||||
TrieCompressedPath<T> temp = leaf;
|
||||
leaf = null;
|
||||
addPattern(temp.pattern, temp.patternLength, temp.patternStartIndex, temp.callback);
|
||||
// Continue onward and add the parameter pattern.
|
||||
} else if (children == null) {
|
||||
leaf = new TrieCompressedPath<>(pattern, patternLength, patternIndex, callback);
|
||||
return;
|
||||
}
|
||||
char character = getCharValue(pattern, patternIndex);
|
||||
if (isInvalidRange(character)) {
|
||||
throw new IllegalArgumentException("invalid character at index " + patternIndex + ": " + pattern);
|
||||
}
|
||||
character -= MIN_VALID_CHAR; // Adjust to the array range.
|
||||
TrieNode<T> child = children[character];
|
||||
if (child == null) {
|
||||
child = createNode();
|
||||
children[character] = child;
|
||||
}
|
||||
child.addPattern(pattern, patternLength, patternIndex + 1, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param searchText Text to search for patterns in.
|
||||
* @param searchTextLength Length of the search text.
|
||||
* @param searchTextIndex Current recursive search text index. Also, the end index of the current pattern match.
|
||||
* @param currentMatchLength current search depth, and also the length of the current pattern match.
|
||||
* @return If any pattern matches, and it's associated callback halted the search.
|
||||
*/
|
||||
private boolean matches(T searchText, int searchTextLength, int searchTextIndex, int currentMatchLength,
|
||||
Object callbackParameter) {
|
||||
if (leaf != null && leaf.matches(this,
|
||||
searchText, searchTextLength, searchTextIndex, callbackParameter)) {
|
||||
return true; // Leaf exists and it matched the search text.
|
||||
}
|
||||
if (endOfPatternCallback != null) {
|
||||
final int matchStartIndex = searchTextIndex - currentMatchLength;
|
||||
for (@Nullable TriePatternMatchedCallback<T> callback : endOfPatternCallback) {
|
||||
if (callback == null) {
|
||||
return true; // No callback and all matches are valid.
|
||||
}
|
||||
if (callback.patternMatched(searchText, matchStartIndex, callbackParameter)) {
|
||||
return true; // Callback confirmed the match.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (children == null) {
|
||||
return false; // Reached a graph end point and there's no further patterns to search.
|
||||
}
|
||||
|
||||
if (searchTextIndex == searchTextLength) {
|
||||
return false; // Reached end of the search text and found no matches.
|
||||
}
|
||||
|
||||
char character = getCharValue(searchText, searchTextIndex);
|
||||
if (isInvalidRange(character)) {
|
||||
return false; // Not an ASCII letter/number/symbol.
|
||||
}
|
||||
character -= MIN_VALID_CHAR; // Adjust to the array range.
|
||||
TrieNode<T> child = children[character];
|
||||
if (child == null) {
|
||||
return false;
|
||||
}
|
||||
return child.matches(searchText, searchTextLength, searchTextIndex + 1,
|
||||
currentMatchLength + 1, callbackParameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = 3; // 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 += NUMBER_OF_CHILDREN;
|
||||
for (TrieNode<T> child : children) {
|
||||
if (child != null) {
|
||||
numberOfPointers += child.estimatedNumberOfPointersUsed();
|
||||
}
|
||||
}
|
||||
}
|
||||
return numberOfPointers;
|
||||
}
|
||||
|
||||
abstract TrieNode<T> createNode();
|
||||
abstract char getCharValue(T text, int index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root node, and it's children represent the first pattern characters.
|
||||
*/
|
||||
private final TrieNode<T> root;
|
||||
|
||||
/**
|
||||
* Patterns to match.
|
||||
*/
|
||||
private final List<T> patterns = new ArrayList<>();
|
||||
|
||||
TrieSearch(@NonNull TrieNode<T> root) {
|
||||
this.root = Objects.requireNonNull(root);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public final void addPatterns(@NonNull T... patterns) {
|
||||
for (T pattern : patterns) {
|
||||
addPattern(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback<T> callback) {
|
||||
if (patternLength == 0) return; // Nothing to match
|
||||
|
||||
patterns.add(pattern);
|
||||
root.addPattern(pattern, patternLength, 0, callback);
|
||||
}
|
||||
|
||||
boolean matches(@NonNull T textToSearch, int textToSearchLength, @Nullable Object callbackParameter) {
|
||||
if (patterns.size() == 0) {
|
||||
return false; // No patterns were added.
|
||||
}
|
||||
for (int i = 0; i < textToSearchLength; i++) {
|
||||
if (root.matches(textToSearch, textToSearchLength, i, 0, callbackParameter)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Estimated memory size (in kilobytes) of this instance.
|
||||
*/
|
||||
public int getEstimatedMemorySize() {
|
||||
if (patterns.size() == 0) {
|
||||
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<T> getPatterns() {
|
||||
return Collections.unmodifiableList(patterns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 abstract void addPattern(@NonNull T pattern);
|
||||
|
||||
/**
|
||||
* @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 abstract void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback<T> callback);
|
||||
|
||||
/**
|
||||
* Searches through text, looking for any substring that matches any pattern in this tree.
|
||||
*
|
||||
* @param textToSearch Text to search through.
|
||||
* @param callbackParameter Optional parameter passed to the callbacks.
|
||||
* @return If any pattern matched, and it's callback halted searching.
|
||||
*/
|
||||
public abstract boolean matches(@NonNull T textToSearch, @Nullable Object callbackParameter);
|
||||
|
||||
/**
|
||||
* Identical to {@link #matches(Object, Object)} but with a null callback parameter.
|
||||
*/
|
||||
public final boolean matches(@NonNull T textToSearch) {
|
||||
return matches(textToSearch, null);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user