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
|
|
@ -4,12 +4,12 @@ import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.EnumSetting;
|
import app.revanced.extension.shared.settings.EnumSetting;
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
public class Settings extends BaseSettings {
|
public class Settings extends YouTubeAndMusicSettings {
|
||||||
|
|
||||||
// Ads
|
// Ads
|
||||||
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
|
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.shared.patches.components;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
|
@ -15,13 +15,15 @@ import java.util.regex.Pattern;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.ByteTrieSearch;
|
import app.revanced.extension.shared.ByteTrieSearch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
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.
|
* Allows custom filtering using a path and optionally a proto buffer string.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class CustomFilter extends Filter {
|
public final class CustomFilter extends Filter {
|
||||||
|
|
||||||
private static void showInvalidSyntaxToast(@NonNull String expression) {
|
private static void showInvalidSyntaxToast(@NonNull String expression) {
|
||||||
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
|
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
|
||||||
|
|
@ -45,7 +47,7 @@ final class CustomFilter extends Filter {
|
||||||
@NonNull
|
@NonNull
|
||||||
@SuppressWarnings("ConstantConditions")
|
@SuppressWarnings("ConstantConditions")
|
||||||
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
|
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
|
||||||
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
|
String rawCustomFilterText = YouTubeAndMusicSettings.CUSTOM_FILTER_STRINGS.get();
|
||||||
if (rawCustomFilterText.isBlank()) {
|
if (rawCustomFilterText.isBlank()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +102,7 @@ final class CustomFilter extends Filter {
|
||||||
ByteTrieSearch bufferSearch;
|
ByteTrieSearch bufferSearch;
|
||||||
|
|
||||||
CustomFilterGroup(boolean startsWith, @NonNull String path) {
|
CustomFilterGroup(boolean startsWith, @NonNull String path) {
|
||||||
super(Settings.CUSTOM_FILTER, path);
|
super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
|
||||||
this.startsWith = startsWith;
|
this.startsWith = startsWith;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +147,7 @@ final class CustomFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
// All callbacks are custom filter groups.
|
// All callbacks are custom filter groups.
|
||||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.shared.patches.litho;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
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.
|
* Filters litho based components.
|
||||||
*
|
*
|
||||||
|
|
@ -14,11 +17,11 @@ import java.util.List;
|
||||||
* either an identifier or a path.
|
* either an identifier or a path.
|
||||||
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
* 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)
|
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
|
||||||
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
* or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
||||||
*
|
*
|
||||||
* All callbacks must be registered before the constructor completes.
|
* All callbacks must be registered before the constructor completes.
|
||||||
*/
|
*/
|
||||||
abstract class Filter {
|
public abstract class Filter {
|
||||||
|
|
||||||
public enum FilterContentType {
|
public enum FilterContentType {
|
||||||
IDENTIFIER,
|
IDENTIFIER,
|
||||||
|
|
@ -65,7 +68,7 @@ abstract class Filter {
|
||||||
* @param contentIndex Matched index of the identifier or path.
|
* @param contentIndex Matched index of the identifier or path.
|
||||||
* @return True if the litho component should be filtered out.
|
* @return True if the litho component should be filtered out.
|
||||||
*/
|
*/
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.shared.patches.litho;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.ByteTrieSearch;
|
import app.revanced.extension.shared.ByteTrieSearch;
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
import app.revanced.extension.shared.TrieSearch;
|
import app.revanced.extension.shared.TrieSearch;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
|
|
||||||
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
public abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
|
||||||
|
|
||||||
private final List<T> filterGroups = new ArrayList<>();
|
private final List<T> filterGroups = new ArrayList<>();
|
||||||
private final TrieSearch<V> search = createSearchGraph();
|
private final TrieSearch<V> search = createSearchGraph();
|
||||||
|
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
protected final void addAll(final T... groups) {
|
public final void addAll(final T... groups) {
|
||||||
filterGroups.addAll(Arrays.asList(groups));
|
filterGroups.addAll(Arrays.asList(groups));
|
||||||
|
|
||||||
for (T group : groups) {
|
for (T group : groups) {
|
||||||
|
|
@ -41,18 +42,7 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
|
||||||
return filterGroups.iterator();
|
return filterGroups.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public FilterGroup.FilterGroupResult check(V stack) {
|
||||||
public void forEach(@NonNull Consumer<? super T> action) {
|
|
||||||
filterGroups.forEach(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Spliterator<T> spliterator() {
|
|
||||||
return filterGroups.spliterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected FilterGroup.FilterGroupResult check(V stack) {
|
|
||||||
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
|
||||||
search.matches(stack, result);
|
search.matches(stack, result);
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -60,9 +50,8 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract TrieSearch<V> createSearchGraph();
|
protected abstract TrieSearch<V> createSearchGraph();
|
||||||
}
|
|
||||||
|
|
||||||
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
|
public static final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
|
||||||
protected StringTrieSearch createSearchGraph() {
|
protected StringTrieSearch createSearchGraph() {
|
||||||
return new StringTrieSearch();
|
return new StringTrieSearch();
|
||||||
}
|
}
|
||||||
|
|
@ -73,8 +62,9 @@ final class StringFilterGroupList extends FilterGroupList<String, StringFilterGr
|
||||||
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
|
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
|
||||||
* than a prefix tree to search for only 1 pattern.
|
* than a prefix tree to search for only 1 pattern.
|
||||||
*/
|
*/
|
||||||
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
public static final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
|
||||||
protected ByteTrieSearch createSearchGraph() {
|
protected ByteTrieSearch createSearchGraph() {
|
||||||
return new ByteTrieSearch();
|
return new ByteTrieSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.shared.patches.litho;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
@ -7,9 +7,11 @@ import java.nio.ByteBuffer;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
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")
|
@SuppressWarnings("unused")
|
||||||
public final class LithoFilterPatch {
|
public final class LithoFilterPatch {
|
||||||
|
|
@ -36,7 +38,7 @@ public final class LithoFilterPatch {
|
||||||
builder.append(identifier);
|
builder.append(identifier);
|
||||||
builder.append(" Path: ");
|
builder.append(" Path: ");
|
||||||
builder.append(path);
|
builder.append(path);
|
||||||
if (Settings.DEBUG_PROTOBUFFER.get()) {
|
if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) {
|
||||||
builder.append(" BufferStrings: ");
|
builder.append(" BufferStrings: ");
|
||||||
findAsciiStrings(builder, buffer);
|
findAsciiStrings(builder, buffer);
|
||||||
}
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,9 @@ import java.util.List;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
|
@ -153,7 +156,7 @@ public final class AdsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == playerShoppingShelf) {
|
if (matchedGroup == playerShoppingShelf) {
|
||||||
return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered();
|
return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
|
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
|
@ -19,7 +21,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
isVideoQualityMenuVisible = true;
|
isVideoQualityMenuVisible = true;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class ButtonsFilter extends Filter {
|
public final class ButtonsFilter extends Filter {
|
||||||
private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.e";
|
private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.e";
|
||||||
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.e";
|
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.e";
|
||||||
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.e";
|
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.e";
|
||||||
|
|
@ -118,7 +122,7 @@ final class ButtonsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == likeSubscribeGlow) {
|
if (matchedGroup == likeSubscribeGlow) {
|
||||||
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class CommentsFilter extends Filter {
|
public final class CommentsFilter extends Filter {
|
||||||
|
|
||||||
private static final String COMMENT_COMPOSER_PATH = "comment_composer.e";
|
private static final String COMMENT_COMPOSER_PATH = "comment_composer.e";
|
||||||
|
|
||||||
|
|
@ -88,7 +90,7 @@ final class CommentsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == chipBar) {
|
if (matchedGroup == chipBar) {
|
||||||
// Playlist sort button uses same components and must only filter if the player is opened.
|
// Playlist sort button uses same components and must only filter if the player is opened.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class DescriptionComponentsFilter extends Filter {
|
public final class DescriptionComponentsFilter extends Filter {
|
||||||
|
|
||||||
private static final String INFOCARDS_SECTION_PATH = "infocards_section.e";
|
private static final String INFOCARDS_SECTION_PATH = "infocards_section.e";
|
||||||
|
|
||||||
|
|
@ -128,7 +131,7 @@ final class DescriptionComponentsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
|
|
||||||
if (matchedGroup == aiGeneratedVideoSummarySection || matchedGroup == hypePoints) {
|
if (matchedGroup == aiGeneratedVideoSummarySection || matchedGroup == hypePoints) {
|
||||||
|
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
|
||||||
import app.revanced.extension.shared.ByteTrieSearch;
|
|
||||||
|
|
||||||
abstract class FilterGroup<T> {
|
|
||||||
final static class FilterGroupResult {
|
|
||||||
private BooleanSetting setting;
|
|
||||||
private int matchedIndex;
|
|
||||||
private int matchedLength;
|
|
||||||
// In the future it might be useful to include which pattern matched,
|
|
||||||
// but for now that is not needed.
|
|
||||||
|
|
||||||
FilterGroupResult() {
|
|
||||||
this(null, -1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
|
||||||
setValues(setting, matchedIndex, matchedLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
|
|
||||||
this.setting = setting;
|
|
||||||
this.matchedIndex = matchedIndex;
|
|
||||||
this.matchedLength = matchedLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A null value if the group has no setting,
|
|
||||||
* or if no match is returned from {@link FilterGroupList#check(Object)}.
|
|
||||||
*/
|
|
||||||
public BooleanSetting getSetting() {
|
|
||||||
return setting;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFiltered() {
|
|
||||||
return matchedIndex >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matched index of first pattern that matched, or -1 if nothing matched.
|
|
||||||
*/
|
|
||||||
public int getMatchedIndex() {
|
|
||||||
return matchedIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Length of the matched filter pattern.
|
|
||||||
*/
|
|
||||||
public int getMatchedLength() {
|
|
||||||
return matchedLength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final BooleanSetting setting;
|
|
||||||
protected final T[] filters;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a new filter group.
|
|
||||||
*
|
|
||||||
* @param setting The associated setting.
|
|
||||||
* @param filters The filters.
|
|
||||||
*/
|
|
||||||
@SafeVarargs
|
|
||||||
public FilterGroup(final BooleanSetting setting, final T... filters) {
|
|
||||||
this.setting = setting;
|
|
||||||
this.filters = filters;
|
|
||||||
if (filters.length == 0) {
|
|
||||||
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return setting == null || setting.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return If {@link FilterGroupList} should include this group when searching.
|
|
||||||
* By default, all filters are included except non enabled settings that require reboot.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
||||||
public boolean includeInSearch() {
|
|
||||||
return isEnabled() || !setting.rebootApp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract FilterGroupResult check(final T stack);
|
|
||||||
}
|
|
||||||
|
|
||||||
class StringFilterGroup extends FilterGroup<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 ByteArrayFilterGroupList#check(byte[])},
|
|
||||||
* which uses a prefix tree to give better performance.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class HideInfoCardsFilter extends Filter {
|
public final class HideInfoCardsFilter extends Filter {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.ByteTrieSearch;
|
import app.revanced.extension.shared.ByteTrieSearch;
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
import app.revanced.extension.shared.TrieSearch;
|
import app.revanced.extension.shared.TrieSearch;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
@ -41,7 +43,7 @@ import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
* - When using whole word syntax, some keywords may need additional pluralized variations.
|
* - When using whole word syntax, some keywords may need additional pluralized variations.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
final class KeywordContentFilter extends Filter {
|
public final class KeywordContentFilter extends Filter {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strings found in the buffer for every videos. Full strings should be specified.
|
* Strings found in the buffer for every videos. Full strings should be specified.
|
||||||
|
|
@ -554,7 +556,7 @@ final class KeywordContentFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ import androidx.annotation.Nullable;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
|
||||||
import app.revanced.extension.youtube.patches.ChangeHeaderPatch;
|
import app.revanced.extension.youtube.patches.ChangeHeaderPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||||
|
|
@ -342,7 +345,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
// This identifier is used not only in players but also in search results:
|
// This identifier is used not only in players but also in search results:
|
||||||
// https://github.com/ReVanced/revanced-patches/issues/3245
|
// https://github.com/ReVanced/revanced-patches/issues/3245
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
|
@ -36,7 +38,7 @@ public final class PlaybackSpeedMenuFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == oldPlaybackMenuGroup) {
|
if (matchedGroup == oldPlaybackMenuGroup) {
|
||||||
isOldPlaybackSpeedMenuVisible = true;
|
isOldPlaybackSpeedMenuVisible = true;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ package app.revanced.extension.youtube.patches.components;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class PlayerFlyoutMenuItemsFilter extends Filter {
|
public final class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||||
|
|
||||||
public static final class HideAudioFlyoutMenuAvailability implements Setting.Availability {
|
public static final class HideAudioFlyoutMenuAvailability implements Setting.Availability {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -94,7 +97,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == videoQualityMenuFooter) {
|
if (matchedGroup == videoQualityMenuFooter) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ import app.revanced.extension.youtube.patches.VideoInformation;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.TrieSearch;
|
import app.revanced.extension.shared.TrieSearch;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches for video id's in the proto buffer of Shorts dislike.
|
* Searches for video id's in the proto buffer of Shorts dislike.
|
||||||
|
|
@ -84,13 +87,13 @@ public final class ReturnYouTubeDislikeFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(buffer);
|
FilterGroupResult result = videoIdFilterGroup.check(buffer);
|
||||||
if (result.isFiltered()) {
|
if (result.isFiltered()) {
|
||||||
String matchedVideoId = findVideoId(buffer);
|
String matchedVideoId = findVideoId(buffer);
|
||||||
// Matched video will be null if in incognito mode.
|
// Matched video will be null if in incognito mode.
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
@ -339,7 +342,7 @@ public final class ShortsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (contentType == FilterContentType.PATH) {
|
if (contentType == FilterContentType.PATH) {
|
||||||
if (matchedGroup == subscribeButton || matchedGroup == joinButton
|
if (matchedGroup == subscribeButton || matchedGroup == joinButton
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import android.graphics.Color;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.EnumSetting;
|
import app.revanced.extension.shared.settings.EnumSetting;
|
||||||
|
|
@ -49,7 +50,7 @@ import app.revanced.extension.youtube.patches.MiniplayerPatch;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
||||||
|
|
||||||
public class Settings extends BaseSettings {
|
public class Settings extends YouTubeAndMusicSettings {
|
||||||
// Video
|
// Video
|
||||||
public static final BooleanSetting ADVANCED_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_advanced_video_quality_menu", TRUE);
|
public static final BooleanSetting ADVANCED_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_advanced_video_quality_menu", TRUE);
|
||||||
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE);
|
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE);
|
||||||
|
|
@ -274,11 +275,6 @@ public class Settings extends BaseSettings {
|
||||||
public static final BooleanSetting CHANGE_START_PAGE_ALWAYS = new BooleanSetting("revanced_change_start_page_always", FALSE, true,
|
public static final BooleanSetting CHANGE_START_PAGE_ALWAYS = new BooleanSetting("revanced_change_start_page_always", FALSE, true,
|
||||||
new ChangeStartPageTypeAvailability());
|
new ChangeStartPageTypeAvailability());
|
||||||
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "19.01.34", true, parent(SPOOF_APP_VERSION));
|
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "19.01.34", true, parent(SPOOF_APP_VERSION));
|
||||||
|
|
||||||
// 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));
|
|
||||||
|
|
||||||
// Navigation buttons
|
// Navigation buttons
|
||||||
public static final BooleanSetting HIDE_HOME_BUTTON = new BooleanSetting("revanced_hide_home_button", FALSE, true);
|
public static final BooleanSetting HIDE_HOME_BUTTON = new BooleanSetting("revanced_hide_home_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", TRUE, true);
|
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", TRUE, true);
|
||||||
|
|
@ -368,8 +364,6 @@ public class Settings extends BaseSettings {
|
||||||
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
|
||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("revanced_spoof_video_streams_av1", FALSE, true,
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("revanced_spoof_video_streams_av1", FALSE, true,
|
||||||
"revanced_spoof_video_streams_av1_user_dialog_message", new SpoofClientAv1Availability());
|
"revanced_spoof_video_streams_av1_user_dialog_message", new SpoofClientAv1Availability());
|
||||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
|
|
||||||
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
|
|
||||||
|
|
||||||
// Swipe controls
|
// Swipe controls
|
||||||
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
|
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
|
||||||
|
|
@ -411,7 +405,9 @@ public class Settings extends BaseSettings {
|
||||||
|
|
||||||
// SponsorBlock
|
// SponsorBlock
|
||||||
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
||||||
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
|
/**
|
||||||
|
* Do not use id setting directly. Instead use {@link SponsorBlockSettings}.
|
||||||
|
*/
|
||||||
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "", parent(SB_ENABLED));
|
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "", parent(SB_ENABLED));
|
||||||
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
||||||
|
|
|
||||||
|
|
@ -472,6 +472,10 @@ public final class app/revanced/patches/music/layout/compactheader/HideCategoryB
|
||||||
public static final fun getHideCategoryBar ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getHideCategoryBar ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatchKt {
|
||||||
|
public static final fun getHideLayoutComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColorKt {
|
public final class app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColorKt {
|
||||||
public static final fun getChangeMiniplayerColor ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getChangeMiniplayerColor ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
@ -521,6 +525,10 @@ public final class app/revanced/patches/music/misc/gms/GmsCoreSupportPatchKt {
|
||||||
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/music/misc/litho/filter/LithoFilterPatchKt {
|
||||||
|
public static final fun getLithoFilterPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatchKt {
|
public final class app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatchKt {
|
||||||
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
@ -932,6 +940,12 @@ public final class app/revanced/patches/shared/misc/hex/Replacement {
|
||||||
public final fun getReplacementBytesPadded ()[B
|
public final fun getReplacementBytesPadded ()[B
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/shared/misc/litho/filter/LithoFilterPatchKt {
|
||||||
|
public static final fun getAddLithoFilter ()Lkotlin/jvm/functions/Function1;
|
||||||
|
public static final fun lithoFilterPatch (Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
public static synthetic fun lithoFilterPatch$default (Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/shared/misc/mapping/ResourceElement {
|
public final class app/revanced/patches/shared/misc/mapping/ResourceElement {
|
||||||
public final fun component1 ()Ljava/lang/String;
|
public final fun component1 ()Ljava/lang/String;
|
||||||
public final fun component2 ()Ljava/lang/String;
|
public final fun component2 ()Ljava/lang/String;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package app.revanced.patches.music.layout.hide.general
|
||||||
|
|
||||||
|
import app.revanced.patches.music.misc.litho.filter.lithoFilterPatch
|
||||||
|
import app.revanced.patches.music.misc.settings.settingsPatch
|
||||||
|
import app.revanced.patches.shared.layout.hide.general.hideLayoutComponentsPatch
|
||||||
|
|
||||||
|
val hideLayoutComponentsPatch = hideLayoutComponentsPatch(
|
||||||
|
lithoFilterPatch = lithoFilterPatch,
|
||||||
|
settingsPatch = settingsPatch,
|
||||||
|
filterClasses = setOf("Lapp/revanced/extension/shared/patches/components/CustomFilter;"),
|
||||||
|
compatibleWithPackages = arrayOf("com.google.android.apps.youtube.music" to setOf("7.29.52", "8.10.52"))
|
||||||
|
)
|
||||||
|
|
@ -7,20 +7,15 @@ import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val enableDebuggingPatch = enableDebuggingPatch(
|
val enableDebuggingPatch = enableDebuggingPatch(
|
||||||
block = {
|
sharedExtensionPatch = sharedExtensionPatch,
|
||||||
dependsOn(
|
settingsPatch = settingsPatch,
|
||||||
sharedExtensionPatch,
|
compatibleWithPackages = arrayOf(
|
||||||
settingsPatch,
|
"com.google.android.apps.youtube.music" to setOf(
|
||||||
)
|
|
||||||
|
|
||||||
compatibleWith(
|
|
||||||
"com.google.android.apps.youtube.music"(
|
|
||||||
"7.29.52",
|
"7.29.52",
|
||||||
"8.10.52"
|
"8.10.52"
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
},
|
|
||||||
// String feature flag does not appear to be present with YT Music.
|
// String feature flag does not appear to be present with YT Music.
|
||||||
hookStringFeatureFlag = false,
|
hookStringFeatureFlag = false,
|
||||||
preferenceScreen = PreferenceScreen.MISC
|
preferenceScreen = PreferenceScreen.MISC,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package app.revanced.patches.music.misc.litho.filter
|
||||||
|
|
||||||
|
import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
||||||
|
import app.revanced.patches.music.shared.conversionContextFingerprintToString
|
||||||
|
import app.revanced.patches.shared.misc.litho.filter.lithoFilterPatch
|
||||||
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
val lithoFilterPatch = lithoFilterPatch(
|
||||||
|
componentCreateInsertionIndex = {
|
||||||
|
// No supported version clobbers p2 so we can just do our things before the return instruction.
|
||||||
|
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
|
||||||
|
},
|
||||||
|
conversionContextFingerprintToString = conversionContextFingerprintToString,
|
||||||
|
) {
|
||||||
|
dependsOn(sharedExtensionPatch)
|
||||||
|
}
|
||||||
|
|
@ -11,3 +11,19 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
|
||||||
method.name == "onCreate" && classDef.type == YOUTUBE_MUSIC_MAIN_ACTIVITY_CLASS_TYPE
|
method.name == "onCreate" && classDef.type == YOUTUBE_MUSIC_MAIN_ACTIVITY_CLASS_TYPE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val conversionContextFingerprintToString = fingerprint {
|
||||||
|
parameters()
|
||||||
|
strings(
|
||||||
|
"ConversionContext{containerInternal=",
|
||||||
|
", gridColumnCount=",
|
||||||
|
", gridColumnIndex=",
|
||||||
|
", templateLoggerFactory=",
|
||||||
|
", rootDisposableContainer=",
|
||||||
|
", elementId=",
|
||||||
|
", identifierProperty="
|
||||||
|
)
|
||||||
|
custom { method, _ ->
|
||||||
|
method.name == "toString"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package app.revanced.patches.shared.layout.hide.general
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.BytecodePatchContext
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
|
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
|
import app.revanced.patches.music.misc.settings.PreferenceScreen
|
||||||
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.InputType
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.TextPreference
|
||||||
|
|
||||||
|
internal fun hideLayoutComponentsPatch(
|
||||||
|
lithoFilterPatch: Patch<*>,
|
||||||
|
settingsPatch: Patch<*>,
|
||||||
|
additionalDependencies: Set<Patch<*>> = emptySet(),
|
||||||
|
filterClasses: Set<String>,
|
||||||
|
vararg compatibleWithPackages: Pair<String, Set<String>?>,
|
||||||
|
executeBlock: BytecodePatchContext.() -> Unit = {},
|
||||||
|
) = bytecodePatch(
|
||||||
|
name = "Hide layout components",
|
||||||
|
description = "Adds options to hide general layout components.",
|
||||||
|
) {
|
||||||
|
dependsOn(
|
||||||
|
lithoFilterPatch,
|
||||||
|
settingsPatch,
|
||||||
|
*additionalDependencies.toTypedArray(),
|
||||||
|
addResourcesPatch,
|
||||||
|
)
|
||||||
|
|
||||||
|
compatibleWith(packages = compatibleWithPackages)
|
||||||
|
|
||||||
|
execute {
|
||||||
|
addResources("shared", "layout.hide.general.hideLayoutComponentsPatch")
|
||||||
|
|
||||||
|
PreferenceScreen.GENERAL.addPreferences(
|
||||||
|
PreferenceScreenPreference(
|
||||||
|
key = "revanced_custom_filter_screen",
|
||||||
|
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
|
||||||
|
preferences = setOf(
|
||||||
|
SwitchPreference("revanced_custom_filter"),
|
||||||
|
TextPreference("revanced_custom_filter_strings", inputType = InputType.TEXT_MULTI_LINE),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
filterClasses.forEach { className ->
|
||||||
|
addLithoFilter(className)
|
||||||
|
}
|
||||||
|
|
||||||
|
executeBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,23 +2,14 @@ package app.revanced.patches.shared.misc.debugging
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.patch.BytecodePatchBuilder
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.BytecodePatchContext
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patcher.patch.resourcePatch
|
import app.revanced.patcher.patch.resourcePatch
|
||||||
import app.revanced.patches.all.misc.resources.addResources
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
import app.revanced.patches.shared.misc.settings.preference.BasePreference
|
import app.revanced.patches.shared.misc.settings.preference.*
|
||||||
import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen
|
|
||||||
import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
|
|
||||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
|
||||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.util.*
|
||||||
import app.revanced.util.ResourceGroup
|
|
||||||
import app.revanced.util.copyResources
|
|
||||||
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
|
||||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
|
||||||
|
|
@ -29,23 +20,27 @@ private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
* Patch shared with YouTube and YT Music.
|
* Patch shared with YouTube and YT Music.
|
||||||
*/
|
*/
|
||||||
internal fun enableDebuggingPatch(
|
internal fun enableDebuggingPatch(
|
||||||
block: BytecodePatchBuilder.() -> Unit = {},
|
sharedExtensionPatch: Patch<*>,
|
||||||
executeBlock: BytecodePatchContext.() -> Unit = {},
|
settingsPatch: Patch<*>,
|
||||||
|
vararg compatibleWithPackages: Pair<String, Set<String>>,
|
||||||
hookStringFeatureFlag: Boolean,
|
hookStringFeatureFlag: Boolean,
|
||||||
preferenceScreen: BasePreferenceScreen.Screen,
|
preferenceScreen: BasePreferenceScreen.Screen,
|
||||||
additionalDebugPreferences: List<BasePreference> = emptyList()
|
|
||||||
) = bytecodePatch(
|
) = bytecodePatch(
|
||||||
name = "Enable debugging",
|
name = "Enable debugging",
|
||||||
description = "Adds options for debugging and exporting ReVanced logs to the clipboard.",
|
description = "Adds options for debugging and exporting ReVanced logs to the clipboard.",
|
||||||
) {
|
) {
|
||||||
|
compatibleWith(packages = compatibleWithPackages)
|
||||||
|
|
||||||
dependsOn(
|
dependsOn(
|
||||||
|
sharedExtensionPatch,
|
||||||
|
settingsPatch,
|
||||||
addResourcesPatch,
|
addResourcesPatch,
|
||||||
resourcePatch {
|
resourcePatch {
|
||||||
execute {
|
execute {
|
||||||
copyResources(
|
copyResources(
|
||||||
"settings",
|
"settings",
|
||||||
ResourceGroup("drawable",
|
ResourceGroup(
|
||||||
|
"drawable",
|
||||||
// Action buttons.
|
// Action buttons.
|
||||||
"revanced_settings_copy_all.xml",
|
"revanced_settings_copy_all.xml",
|
||||||
"revanced_settings_deselect_all.xml",
|
"revanced_settings_deselect_all.xml",
|
||||||
|
|
@ -61,21 +56,13 @@ internal fun enableDebuggingPatch(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
block()
|
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
executeBlock()
|
|
||||||
|
|
||||||
addResources("shared", "misc.debugging.enableDebuggingPatch")
|
addResources("shared", "misc.debugging.enableDebuggingPatch")
|
||||||
|
|
||||||
val preferences = mutableSetOf<BasePreference>(
|
val preferences = setOf(
|
||||||
SwitchPreference("revanced_debug"),
|
SwitchPreference("revanced_debug"),
|
||||||
)
|
SwitchPreference("revanced_debug_protobuffer"),
|
||||||
|
|
||||||
preferences.addAll(additionalDebugPreferences)
|
|
||||||
|
|
||||||
preferences.addAll(
|
|
||||||
listOf(
|
|
||||||
SwitchPreference("revanced_debug_stacktrace"),
|
SwitchPreference("revanced_debug_stacktrace"),
|
||||||
SwitchPreference("revanced_debug_toast_on_error"),
|
SwitchPreference("revanced_debug_toast_on_error"),
|
||||||
NonInteractivePreference(
|
NonInteractivePreference(
|
||||||
|
|
@ -94,7 +81,6 @@ internal fun enableDebuggingPatch(
|
||||||
selectable = true
|
selectable = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
preferenceScreen.addPreferences(
|
preferenceScreen.addPreferences(
|
||||||
PreferenceScreenPreference(
|
PreferenceScreenPreference(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package app.revanced.patches.shared.misc.litho.filter
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
import app.revanced.util.containsLiteralInstruction
|
||||||
|
import app.revanced.util.literal
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
|
||||||
|
internal val lithoFilterFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
||||||
|
custom { _, classDef ->
|
||||||
|
classDef.endsWith("/LithoFilterPatch;")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a method that use the protobuf of our component.
|
||||||
|
*/
|
||||||
|
internal val protobufBufferReferenceFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||||
|
returns("V")
|
||||||
|
parameters("I", "Ljava/nio/ByteBuffer;")
|
||||||
|
opcodes(
|
||||||
|
Opcode.IPUT,
|
||||||
|
Opcode.INVOKE_VIRTUAL,
|
||||||
|
Opcode.MOVE_RESULT,
|
||||||
|
Opcode.SUB_INT_2ADDR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val componentContextParserFingerprint = fingerprint {
|
||||||
|
strings("Number of bits must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val emptyComponentFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
|
||||||
|
parameters()
|
||||||
|
strings("EmptyComponent")
|
||||||
|
custom { _, classDef ->
|
||||||
|
classDef.methods.filter { AccessFlags.STATIC.isSet(it.accessFlags) }.size == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val componentCreateFingerprint = fingerprint {
|
||||||
|
strings(
|
||||||
|
"Element missing correct type extension",
|
||||||
|
"Element missing type"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val lithoThreadExecutorFingerprint = fingerprint {
|
||||||
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
||||||
|
parameters("I", "I", "I")
|
||||||
|
custom { method, classDef ->
|
||||||
|
classDef.superclass == "Ljava/util/concurrent/ThreadPoolExecutor;" &&
|
||||||
|
method.containsLiteralInstruction(1L) // 1L = default thread timeout.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
@file:Suppress("SpellCheckingInspection")
|
||||||
|
|
||||||
|
package app.revanced.patches.shared.misc.litho.filter
|
||||||
|
|
||||||
|
import app.revanced.patcher.Fingerprint
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
|
import app.revanced.patcher.patch.BytecodePatchBuilder
|
||||||
|
import app.revanced.patcher.patch.BytecodePatchContext
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||||
|
import app.revanced.util.addInstructionsAtControlFlowLabel
|
||||||
|
import app.revanced.util.findFreeRegister
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||||
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
import com.android.tools.smali.dexlib2.iface.Method
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to add a hook point to the extension stub.
|
||||||
|
*/
|
||||||
|
lateinit var addLithoFilter: (String) -> Unit
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of filters added to the static field array.
|
||||||
|
*/
|
||||||
|
private var filterCount = 0
|
||||||
|
|
||||||
|
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/patches/litho/LithoFilterPatch;"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A patch that allows to filter Litho components based on their identifier or path.
|
||||||
|
*
|
||||||
|
* @param componentCreateInsertionIndex The index to insert the filtering code in the component create method.
|
||||||
|
* @param conversionContextFingerprintToString The fingerprint of the conversion context to string method.
|
||||||
|
* @param executeBlock The additional execution block of the patch.
|
||||||
|
* @param block The additional block to build the patch.
|
||||||
|
*/
|
||||||
|
internal fun lithoFilterPatch(
|
||||||
|
componentCreateInsertionIndex: Method.() -> Int,
|
||||||
|
conversionContextFingerprintToString: Fingerprint,
|
||||||
|
executeBlock: BytecodePatchContext.() -> Unit = {},
|
||||||
|
block: BytecodePatchBuilder.() -> Unit = {},
|
||||||
|
) = bytecodePatch(
|
||||||
|
description = "Hooks the method which parses the bytes into a ComponentContext to filter components.",
|
||||||
|
) {
|
||||||
|
dependsOn(
|
||||||
|
sharedExtensionPatch(),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The following patch inserts a hook into the method that parses the bytes into a ComponentContext.
|
||||||
|
* This method contains a StringBuilder object that represents the pathBuilder of the component.
|
||||||
|
* The pathBuilder is used to filter components by their path.
|
||||||
|
*
|
||||||
|
* Additionally, the method contains a reference to the component's identifier.
|
||||||
|
* The identifier is used to filter components by their identifier.
|
||||||
|
*
|
||||||
|
* The protobuf buffer is passed along from a different injection point before the filtering occurs.
|
||||||
|
* The buffer is a large byte array that represents the component tree.
|
||||||
|
* This byte array is searched for strings that indicate the current component.
|
||||||
|
*
|
||||||
|
* All modifications done here must allow all the original code to still execute
|
||||||
|
* even when filtering, otherwise memory leaks or poor app performance may occur.
|
||||||
|
*
|
||||||
|
* The following pseudocode shows how this patch works:
|
||||||
|
*
|
||||||
|
* class SomeOtherClass {
|
||||||
|
* // Called before ComponentContextParser.parseComponent() method.
|
||||||
|
* public void someOtherMethod(ByteBuffer byteBuffer) {
|
||||||
|
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* class CreateComponentClass {
|
||||||
|
* public Component createComponent() {
|
||||||
|
* ...
|
||||||
|
*
|
||||||
|
* if (extensionClass.shouldFilter(identifier, path)) { // Inserted by this patch.
|
||||||
|
* return emptyComponent;
|
||||||
|
* }
|
||||||
|
* return originalUnpatchedComponent; // Original code.
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
execute {
|
||||||
|
// Remove dummy filter from extenion static field
|
||||||
|
// and add the filters included during patching.
|
||||||
|
lithoFilterFingerprint.method.apply {
|
||||||
|
removeInstructions(2, 4) // Remove dummy filter.
|
||||||
|
|
||||||
|
addLithoFilter = { classDescriptor ->
|
||||||
|
addInstructions(
|
||||||
|
2,
|
||||||
|
"""
|
||||||
|
new-instance v1, $classDescriptor
|
||||||
|
invoke-direct { v1 }, $classDescriptor-><init>()V
|
||||||
|
const/16 v2, ${filterCount++}
|
||||||
|
aput-object v1, v0, v2
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an interceptor to steal the protobuf of our component.
|
||||||
|
protobufBufferReferenceFingerprint.method.addInstruction(
|
||||||
|
0,
|
||||||
|
"invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Hook the method that parses bytes into a ComponentContext.
|
||||||
|
// Allow the method to run to completion, and override the
|
||||||
|
// return value with an empty component if it should be filtered.
|
||||||
|
// It is important to allow the original code to always run to completion,
|
||||||
|
// otherwise high memory usage and poor app performance can occur.
|
||||||
|
|
||||||
|
// Find the identifier/path fields of the conversion context.
|
||||||
|
val conversionContextIdentifierField = componentContextParserFingerprint.let {
|
||||||
|
// Identifier field is loaded just before the string declaration.
|
||||||
|
val index = it.method.indexOfFirstInstructionReversedOrThrow(
|
||||||
|
it.stringMatches!!.first().index
|
||||||
|
) {
|
||||||
|
// Our instruction reads a String from a field of the ConversionContext class.
|
||||||
|
val reference = getReference<FieldReference>()
|
||||||
|
reference?.definingClass == conversionContextFingerprintToString.originalClassDef.type
|
||||||
|
&& reference.type == "Ljava/lang/String;"
|
||||||
|
}
|
||||||
|
|
||||||
|
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val conversionContextPathBuilderField = conversionContextFingerprintToString.originalClassDef
|
||||||
|
.fields.single { field -> field.type == "Ljava/lang/StringBuilder;" }
|
||||||
|
|
||||||
|
// Find class and methods to create an empty component.
|
||||||
|
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
|
||||||
|
// The only static method in the class.
|
||||||
|
method ->
|
||||||
|
AccessFlags.STATIC.isSet(method.accessFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
val emptyComponentField = classBy {
|
||||||
|
// Only one field that matches.
|
||||||
|
it.type == builderMethodDescriptor.returnType
|
||||||
|
}!!.immutableClass.fields.single()
|
||||||
|
|
||||||
|
// Match all component creations methods
|
||||||
|
componentCreateFingerprint.method.apply {
|
||||||
|
val insertIndex = componentCreateInsertionIndex()
|
||||||
|
val freeRegister = findFreeRegister(insertIndex)
|
||||||
|
val identifierRegister = findFreeRegister(insertIndex, freeRegister)
|
||||||
|
val pathRegister = findFreeRegister(insertIndex, freeRegister, identifierRegister)
|
||||||
|
|
||||||
|
addInstructionsAtControlFlowLabel(
|
||||||
|
insertIndex,
|
||||||
|
"""
|
||||||
|
move-object/from16 v$freeRegister, p2 # ConversionContext parameter
|
||||||
|
check-cast v$freeRegister, ${conversionContextFingerprintToString.originalClassDef.type} # Check we got the actual ConversionContext
|
||||||
|
|
||||||
|
# Get identifier and path from ConversionContext
|
||||||
|
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
|
||||||
|
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
|
||||||
|
|
||||||
|
# Check if the component should be filtered.
|
||||||
|
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
|
||||||
|
move-result v$freeRegister
|
||||||
|
if-eqz v$freeRegister, :unfiltered
|
||||||
|
|
||||||
|
# Return an empty component
|
||||||
|
move-object/from16 v$freeRegister, p1
|
||||||
|
invoke-static { v$freeRegister }, $builderMethodDescriptor
|
||||||
|
move-result-object v$freeRegister
|
||||||
|
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
|
||||||
|
return-object v$freeRegister
|
||||||
|
|
||||||
|
:unfiltered
|
||||||
|
nop
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check if needed in music
|
||||||
|
// Change Litho thread executor to 1 thread to fix layout issue in unpatched YouTube.
|
||||||
|
lithoThreadExecutorFingerprint.method.addInstructions(
|
||||||
|
0,
|
||||||
|
"""
|
||||||
|
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->getExecutorCorePoolSize(I)I
|
||||||
|
move-result p1
|
||||||
|
invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->getExecutorMaxThreads(I)I
|
||||||
|
move-result p2
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
executeBlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize {
|
||||||
|
// Save the number of filters added.
|
||||||
|
lithoFilterFingerprint.method.replaceInstruction(0, "const/16 v0, $filterCount")
|
||||||
|
}
|
||||||
|
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
@ -7,13 +7,13 @@ import app.revanced.patcher.patch.resourcePatch
|
||||||
import app.revanced.patches.all.misc.resources.addResources
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
import app.revanced.patches.shared.misc.fix.verticalscroll.verticalScrollPatch
|
import app.revanced.patches.shared.misc.fix.verticalscroll.verticalScrollPatch
|
||||||
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.shared.misc.mapping.get
|
import app.revanced.patches.shared.misc.mapping.get
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.ad.getpremium.hideGetPremiumPatch
|
import app.revanced.patches.youtube.ad.getpremium.hideGetPremiumPatch
|
||||||
import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch
|
import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,34 +9,27 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
|
||||||
import app.revanced.patcher.patch.resourcePatch
|
import app.revanced.patcher.patch.resourcePatch
|
||||||
import app.revanced.patcher.util.smali.ExternalLabel
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
import app.revanced.patches.all.misc.resources.addResources
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
import app.revanced.patches.shared.layout.hide.general.hideLayoutComponentsPatch
|
||||||
import app.revanced.patches.shared.misc.mapping.get
|
import app.revanced.patches.shared.misc.mapping.get
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||||
import app.revanced.patches.shared.misc.settings.preference.*
|
import app.revanced.patches.shared.misc.settings.preference.*
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch
|
import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater
|
||||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||||
import app.revanced.util.findFreeRegister
|
import app.revanced.util.*
|
||||||
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
|
||||||
import app.revanced.util.getReference
|
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
|
||||||
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
|
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.Method
|
import com.android.tools.smali.dexlib2.iface.Method
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
|
||||||
|
|
||||||
var expandButtonDownId = -1L
|
var expandButtonDownId = -1L
|
||||||
private set
|
private set
|
||||||
|
|
@ -108,33 +101,34 @@ private const val DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME =
|
||||||
private const val COMMENTS_FILTER_CLASS_NAME =
|
private const val COMMENTS_FILTER_CLASS_NAME =
|
||||||
"Lapp/revanced/extension/youtube/patches/components/CommentsFilter;"
|
"Lapp/revanced/extension/youtube/patches/components/CommentsFilter;"
|
||||||
private const val CUSTOM_FILTER_CLASS_NAME =
|
private const val CUSTOM_FILTER_CLASS_NAME =
|
||||||
"Lapp/revanced/extension/youtube/patches/components/CustomFilter;"
|
"Lapp/revanced/extension/shared/patches/components/CustomFilter;"
|
||||||
private const val KEYWORD_FILTER_CLASS_NAME =
|
private const val KEYWORD_FILTER_CLASS_NAME =
|
||||||
"Lapp/revanced/extension/youtube/patches/components/KeywordContentFilter;"
|
"Lapp/revanced/extension/youtube/patches/components/KeywordContentFilter;"
|
||||||
|
|
||||||
val hideLayoutComponentsPatch = bytecodePatch(
|
|
||||||
name = "Hide layout components",
|
|
||||||
description = "Adds options to hide general layout components.",
|
|
||||||
|
|
||||||
) {
|
val hideLayoutComponentsPatch = hideLayoutComponentsPatch(
|
||||||
dependsOn(
|
lithoFilterPatch = lithoFilterPatch,
|
||||||
lithoFilterPatch,
|
settingsPatch = settingsPatch,
|
||||||
settingsPatch,
|
additionalDependencies = setOf(
|
||||||
addResourcesPatch,
|
|
||||||
hideLayoutComponentsResourcePatch,
|
hideLayoutComponentsResourcePatch,
|
||||||
navigationBarHookPatch,
|
navigationBarHookPatch,
|
||||||
)
|
),
|
||||||
|
filterClasses = setOf(
|
||||||
compatibleWith(
|
LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR,
|
||||||
"com.google.android.youtube"(
|
DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME,
|
||||||
|
COMMENTS_FILTER_CLASS_NAME,
|
||||||
|
KEYWORD_FILTER_CLASS_NAME,
|
||||||
|
CUSTOM_FILTER_CLASS_NAME
|
||||||
|
),
|
||||||
|
compatibleWithPackages = arrayOf(
|
||||||
|
"com.google.android.youtube" to setOf(
|
||||||
"19.34.42",
|
"19.34.42",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.13.41",
|
"20.13.41",
|
||||||
"20.14.43",
|
"20.14.43",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
execute {
|
|
||||||
addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch")
|
addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch")
|
||||||
|
|
||||||
PreferenceScreen.PLAYER.addPreferences(
|
PreferenceScreen.PLAYER.addPreferences(
|
||||||
|
|
@ -252,23 +246,6 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
||||||
SwitchPreference("revanced_hide_doodles"),
|
SwitchPreference("revanced_hide_doodles"),
|
||||||
)
|
)
|
||||||
|
|
||||||
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
|
|
||||||
PreferenceScreenPreference(
|
|
||||||
key = "revanced_custom_filter_screen",
|
|
||||||
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
|
|
||||||
preferences = setOf(
|
|
||||||
SwitchPreference("revanced_custom_filter"),
|
|
||||||
TextPreference("revanced_custom_filter_strings", inputType = InputType.TEXT_MULTI_LINE),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
addLithoFilter(LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR)
|
|
||||||
addLithoFilter(DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME)
|
|
||||||
addLithoFilter(COMMENTS_FILTER_CLASS_NAME)
|
|
||||||
addLithoFilter(KEYWORD_FILTER_CLASS_NAME)
|
|
||||||
addLithoFilter(CUSTOM_FILTER_CLASS_NAME)
|
|
||||||
|
|
||||||
// region Mix playlists
|
// region Mix playlists
|
||||||
|
|
||||||
(if (is_20_09_or_greater) parseElementFromBufferFingerprint
|
(if (is_20_09_or_greater) parseElementFromBufferFingerprint
|
||||||
|
|
@ -279,9 +256,11 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
||||||
val startIndex = it.patternMatch!!.startIndex
|
val startIndex = it.patternMatch!!.startIndex
|
||||||
val conversionContextRegister = getInstruction<TwoRegisterInstruction>(startIndex).registerA
|
val conversionContextRegister = getInstruction<TwoRegisterInstruction>(startIndex).registerA
|
||||||
val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC }
|
val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC }
|
||||||
val returnEmptyComponentRegister = (returnEmptyComponentInstruction as FiveRegisterInstruction).registerC
|
val returnEmptyComponentRegister =
|
||||||
|
(returnEmptyComponentInstruction as FiveRegisterInstruction).registerC
|
||||||
val insertIndex = startIndex + 1
|
val insertIndex = startIndex + 1
|
||||||
val freeRegister = findFreeRegister(insertIndex, conversionContextRegister, returnEmptyComponentRegister)
|
val freeRegister =
|
||||||
|
findFreeRegister(insertIndex, conversionContextRegister, returnEmptyComponentRegister)
|
||||||
|
|
||||||
addInstructionsWithLabels(
|
addInstructionsWithLabels(
|
||||||
insertIndex,
|
insertIndex,
|
||||||
|
|
@ -483,4 +462,3 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
||||||
"$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V"
|
"$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import app.revanced.patches.all.misc.resources.addResources
|
||||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
|
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
|
||||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch
|
import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_19_41_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_19_41_or_greater
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
|
||||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
|
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,22 @@
|
||||||
package app.revanced.patches.youtube.misc.debugging
|
package app.revanced.patches.youtube.misc.debugging
|
||||||
|
|
||||||
import app.revanced.patches.all.misc.resources.addResources
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch;
|
||||||
import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch
|
import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
|
||||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val enableDebuggingPatch = enableDebuggingPatch(
|
val enableDebuggingPatch = enableDebuggingPatch(
|
||||||
block = {
|
sharedExtensionPatch = sharedExtensionPatch,
|
||||||
dependsOn(
|
settingsPatch = settingsPatch,
|
||||||
sharedExtensionPatch,
|
compatibleWithPackages = arrayOf(
|
||||||
settingsPatch,
|
"com.google.android.youtube" to setOf(
|
||||||
)
|
|
||||||
|
|
||||||
compatibleWith(
|
|
||||||
"com.google.android.youtube"(
|
|
||||||
"19.34.42",
|
"19.34.42",
|
||||||
"20.07.39",
|
"20.07.39",
|
||||||
"20.13.41",
|
"20.13.41",
|
||||||
"20.14.43",
|
"20.14.43",
|
||||||
)
|
)
|
||||||
)
|
),
|
||||||
},
|
|
||||||
executeBlock = {
|
|
||||||
addResources("youtube", "misc.debugging.enableDebuggingPatch")
|
|
||||||
},
|
|
||||||
hookStringFeatureFlag = true,
|
hookStringFeatureFlag = true,
|
||||||
preferenceScreen = PreferenceScreen.MISC,
|
preferenceScreen = PreferenceScreen.MISC,
|
||||||
additionalDebugPreferences = listOf(SwitchPreference("revanced_debug_protobuffer"))
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,8 @@
|
||||||
package app.revanced.patches.youtube.misc.litho.filter
|
package app.revanced.patches.youtube.misc.litho.filter
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
import app.revanced.patcher.fingerprint
|
||||||
import app.revanced.util.containsLiteralInstruction
|
|
||||||
import app.revanced.util.literal
|
import app.revanced.util.literal
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
|
||||||
|
|
||||||
internal val componentContextParserFingerprint = fingerprint {
|
|
||||||
strings("Number of bits must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val componentCreateFingerprint = fingerprint {
|
|
||||||
strings(
|
|
||||||
"Element missing correct type extension",
|
|
||||||
"Element missing type"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val lithoFilterFingerprint = fingerprint {
|
|
||||||
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
|
||||||
custom { _, classDef ->
|
|
||||||
classDef.endsWith("/LithoFilterPatch;")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val protobufBufferReferenceFingerprint = fingerprint {
|
|
||||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
|
||||||
returns("V")
|
|
||||||
parameters("I", "Ljava/nio/ByteBuffer;")
|
|
||||||
opcodes(
|
|
||||||
Opcode.IPUT,
|
|
||||||
Opcode.INVOKE_VIRTUAL,
|
|
||||||
Opcode.MOVE_RESULT,
|
|
||||||
Opcode.SUB_INT_2ADDR,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val emptyComponentFingerprint = fingerprint {
|
|
||||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR)
|
|
||||||
parameters()
|
|
||||||
strings("EmptyComponent")
|
|
||||||
custom { _, classDef ->
|
|
||||||
classDef.methods.filter { AccessFlags.STATIC.isSet(it.accessFlags) }.size == 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val lithoThreadExecutorFingerprint = fingerprint {
|
|
||||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
|
||||||
parameters("I", "I", "I")
|
|
||||||
custom { method, classDef ->
|
|
||||||
classDef.superclass == "Ljava/util/concurrent/ThreadPoolExecutor;" &&
|
|
||||||
method.containsLiteralInstruction(1L) // 1L = default thread timeout.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val lithoComponentNameUpbFeatureFlagFingerprint = fingerprint {
|
internal val lithoComponentNameUpbFeatureFlagFingerprint = fingerprint {
|
||||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||||
|
|
|
||||||
|
|
@ -3,194 +3,38 @@
|
||||||
package app.revanced.patches.youtube.misc.litho.filter
|
package app.revanced.patches.youtube.misc.litho.filter
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
|
import app.revanced.patcher.patch.BytecodePatchContext
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
import app.revanced.patches.shared.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_19_17_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_19_17_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_20_05_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
||||||
import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
|
import app.revanced.patches.youtube.shared.conversionContextFingerprintToString
|
||||||
import app.revanced.util.addInstructionsAtControlFlowLabel
|
|
||||||
import app.revanced.util.findFreeRegister
|
|
||||||
import app.revanced.util.getReference
|
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
|
||||||
|
|
||||||
|
@Deprecated("Use the shared one instead", ReplaceWith("app.revanced.patches.shared.misc.litho.filter.addLithoFilter"))
|
||||||
lateinit var addLithoFilter: (String) -> Unit
|
lateinit var addLithoFilter: (String) -> Unit
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/components/LithoFilterPatch;"
|
val lithoFilterPatch = lithoFilterPatch(
|
||||||
|
componentCreateInsertionIndex = {
|
||||||
val lithoFilterPatch = bytecodePatch(
|
if (is_19_17_or_greater) {
|
||||||
description = "Hooks the method which parses the bytes into a ComponentContext to filter components.",
|
|
||||||
) {
|
|
||||||
dependsOn(
|
|
||||||
sharedExtensionPatch,
|
|
||||||
versionCheckPatch,
|
|
||||||
)
|
|
||||||
|
|
||||||
var filterCount = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The following patch inserts a hook into the method that parses the bytes into a ComponentContext.
|
|
||||||
* This method contains a StringBuilder object that represents the pathBuilder of the component.
|
|
||||||
* The pathBuilder is used to filter components by their path.
|
|
||||||
*
|
|
||||||
* Additionally, the method contains a reference to the component's identifier.
|
|
||||||
* The identifier is used to filter components by their identifier.
|
|
||||||
*
|
|
||||||
* The protobuf buffer is passed along from a different injection point before the filtering occurs.
|
|
||||||
* The buffer is a large byte array that represents the component tree.
|
|
||||||
* This byte array is searched for strings that indicate the current component.
|
|
||||||
*
|
|
||||||
* All modifications done here must allow all the original code to still execute
|
|
||||||
* even when filtering, otherwise memory leaks or poor app performance may occur.
|
|
||||||
*
|
|
||||||
* The following pseudocode shows how this patch works:
|
|
||||||
*
|
|
||||||
* class SomeOtherClass {
|
|
||||||
* // Called before ComponentContextParser.parseComponent() method.
|
|
||||||
* public void someOtherMethod(ByteBuffer byteBuffer) {
|
|
||||||
* ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch.
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* class CreateComponentClass {
|
|
||||||
* public Component createComponent() {
|
|
||||||
* ...
|
|
||||||
*
|
|
||||||
* if (extensionClass.shouldFilter(identifier, path)) { // Inserted by this patch.
|
|
||||||
* return emptyComponent;
|
|
||||||
* }
|
|
||||||
* return originalUnpatchedComponent; // Original code.
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
execute {
|
|
||||||
// Remove dummy filter from extenion static field
|
|
||||||
// and add the filters included during patching.
|
|
||||||
lithoFilterFingerprint.method.apply {
|
|
||||||
removeInstructions(2, 4) // Remove dummy filter.
|
|
||||||
|
|
||||||
addLithoFilter = { classDescriptor ->
|
|
||||||
addInstructions(
|
|
||||||
2,
|
|
||||||
"""
|
|
||||||
new-instance v1, $classDescriptor
|
|
||||||
invoke-direct { v1 }, $classDescriptor-><init>()V
|
|
||||||
const/16 v2, ${filterCount++}
|
|
||||||
aput-object v1, v0, v2
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Pass the buffer into extension.
|
|
||||||
|
|
||||||
protobufBufferReferenceFingerprint.method.addInstruction(
|
|
||||||
0,
|
|
||||||
"invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V",
|
|
||||||
)
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Hook the method that parses bytes into a ComponentContext.
|
|
||||||
|
|
||||||
// Allow the method to run to completion, and override the
|
|
||||||
// return value with an empty component if it should be filtered.
|
|
||||||
// It is important to allow the original code to always run to completion,
|
|
||||||
// otherwise high memory usage and poor app performance can occur.
|
|
||||||
|
|
||||||
// Find the identifier/path fields of the conversion context.
|
|
||||||
val conversionContextIdentifierField = componentContextParserFingerprint.let {
|
|
||||||
// Identifier field is loaded just before the string declaration.
|
|
||||||
val index = it.method.indexOfFirstInstructionReversedOrThrow(
|
|
||||||
it.stringMatches!!.first().index
|
|
||||||
) {
|
|
||||||
val reference = getReference<FieldReference>()
|
|
||||||
reference?.definingClass == conversionContextFingerprintToString.originalClassDef.type
|
|
||||||
&& reference.type == "Ljava/lang/String;"
|
|
||||||
}
|
|
||||||
|
|
||||||
it.method.getInstruction<ReferenceInstruction>(index).getReference<FieldReference>()!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val conversionContextPathBuilderField = conversionContextFingerprintToString.originalClassDef
|
|
||||||
.fields.single { field -> field.type == "Ljava/lang/StringBuilder;" }
|
|
||||||
|
|
||||||
// Find class and methods to create an empty component.
|
|
||||||
val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.single {
|
|
||||||
// The only static method in the class.
|
|
||||||
method -> AccessFlags.STATIC.isSet(method.accessFlags)
|
|
||||||
}
|
|
||||||
val emptyComponentField = classBy {
|
|
||||||
// Only one field that matches.
|
|
||||||
it.type == builderMethodDescriptor.returnType
|
|
||||||
}!!.immutableClass.fields.single()
|
|
||||||
|
|
||||||
componentCreateFingerprint.method.apply {
|
|
||||||
val insertIndex = if (is_19_17_or_greater) {
|
|
||||||
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
|
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
|
||||||
} else {
|
} else {
|
||||||
// 19.16 clobbers p2 so must check at start of the method and not at the return index.
|
// 19.16 clobbers p2 so must check at start of the method
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
val freeRegister = findFreeRegister(insertIndex)
|
conversionContextFingerprintToString = conversionContextFingerprintToString,
|
||||||
val identifierRegister = findFreeRegister(insertIndex, freeRegister)
|
executeBlock = BytecodePatchContext::executeBlock,
|
||||||
val pathRegister = findFreeRegister(insertIndex, freeRegister, identifierRegister)
|
) {
|
||||||
|
dependsOn(versionCheckPatch)
|
||||||
addInstructionsAtControlFlowLabel(
|
|
||||||
insertIndex,
|
|
||||||
"""
|
|
||||||
move-object/from16 v$freeRegister, p2
|
|
||||||
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
|
|
||||||
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
|
|
||||||
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
|
|
||||||
move-result v$freeRegister
|
|
||||||
if-eqz v$freeRegister, :unfiltered
|
|
||||||
|
|
||||||
# Return an empty component
|
|
||||||
move-object/from16 v$freeRegister, p1
|
|
||||||
invoke-static { v$freeRegister }, $builderMethodDescriptor
|
|
||||||
move-result-object v$freeRegister
|
|
||||||
iget-object v$freeRegister, v$freeRegister, $emptyComponentField
|
|
||||||
return-object v$freeRegister
|
|
||||||
|
|
||||||
:unfiltered
|
|
||||||
nop
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
private fun BytecodePatchContext.executeBlock() {
|
||||||
|
|
||||||
|
|
||||||
// region Change Litho thread executor to 1 thread to fix layout issue in unpatched YouTube.
|
|
||||||
|
|
||||||
lithoThreadExecutorFingerprint.method.addInstructions(
|
|
||||||
0,
|
|
||||||
"""
|
|
||||||
invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->getExecutorCorePoolSize(I)I
|
|
||||||
move-result p1
|
|
||||||
invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->getExecutorMaxThreads(I)I
|
|
||||||
move-result p2
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
|
|
||||||
// region A/B test of new Litho native code.
|
// region A/B test of new Litho native code.
|
||||||
|
|
||||||
// Turn off native code that handles litho component names. If this feature is on then nearly
|
// Turn off native code that handles litho component names. If this feature is on then nearly
|
||||||
|
|
@ -219,9 +63,8 @@ val lithoFilterPatch = bytecodePatch(
|
||||||
}
|
}
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
}
|
|
||||||
|
|
||||||
finalize {
|
// Set the addLithoFilter function to the one from the shared patch.
|
||||||
lithoFilterFingerprint.method.replaceInstruction(0, "const/16 v0, $filterCount")
|
// This is done for backwards compatibility.
|
||||||
}
|
addLithoFilter = app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.recyclerviewtree.hook.addRecyclerViewTreeHook
|
import app.revanced.patches.youtube.misc.recyclerviewtree.hook.addRecyclerViewTreeHook
|
||||||
import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch
|
import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import app.revanced.patches.shared.misc.settings.preference.InputType
|
||||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.TextPreference
|
import app.revanced.patches.shared.misc.settings.preference.TextPreference
|
||||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
import app.revanced.patches.shared.misc.litho.filter.addLithoFilter
|
||||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||||
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
|
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
|
||||||
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,14 @@ You will not be notified of any unexpected events."</string>
|
||||||
<string name="revanced_debug_feature_flags_manager_toast_saved">Flags saved</string>
|
<string name="revanced_debug_feature_flags_manager_toast_saved">Flags saved</string>
|
||||||
<string name="revanced_debug_feature_flags_manager_toast_reset">Flags reset</string>
|
<string name="revanced_debug_feature_flags_manager_toast_reset">Flags reset</string>
|
||||||
<string name="revanced_debug_feature_flags_manager_toast_copied">Flags copied to clipboard</string>
|
<string name="revanced_debug_feature_flags_manager_toast_copied">Flags copied to clipboard</string>
|
||||||
|
<string name="revanced_debug_protobuffer_title">Log protocol buffer</string>
|
||||||
|
<string name="revanced_debug_protobuffer_summary_on">Debug logs include proto buffer</string>
|
||||||
|
<string name="revanced_debug_protobuffer_summary_off">Debug logs do not include proto buffer</string>
|
||||||
|
<string name="revanced_debug_protobuffer_user_dialog_message">"Enabling this setting will log additional layout data, including on-screen text for some UI components.
|
||||||
|
|
||||||
|
This can help identify components when creating custom filters.
|
||||||
|
|
||||||
|
However, enabling this will also log some user data such as your IP address."</string>
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="misc.privacy.sanitizeSharingLinksPatch">
|
<patch id="misc.privacy.sanitizeSharingLinksPatch">
|
||||||
<string name="revanced_sanitize_sharing_links_title">Sanitize sharing links</string>
|
<string name="revanced_sanitize_sharing_links_title">Sanitize sharing links</string>
|
||||||
|
|
@ -182,6 +190,17 @@ You will not be notified of any unexpected events."</string>
|
||||||
<string name="revanced_replace_music_with_youtube_summary_on">Shared links use youtube.com</string>
|
<string name="revanced_replace_music_with_youtube_summary_on">Shared links use youtube.com</string>
|
||||||
<string name="revanced_replace_music_with_youtube_summary_off">Shared links use music.youtube.com</string>
|
<string name="revanced_replace_music_with_youtube_summary_off">Shared links use music.youtube.com</string>
|
||||||
</patch>
|
</patch>
|
||||||
|
<patch id="layout.hide.general.hideLayoutComponentsPatch">
|
||||||
|
<string name="revanced_custom_filter_screen_title">Custom filter</string>
|
||||||
|
<string name="revanced_custom_filter_screen_summary">Hide components using custom filters</string>
|
||||||
|
<string name="revanced_custom_filter_title">Enable custom filter</string>
|
||||||
|
<string name="revanced_custom_filter_summary_on">Custom filter is enabled</string>
|
||||||
|
<string name="revanced_custom_filter_summary_off">Custom filter is disabled</string>
|
||||||
|
<string name="revanced_custom_filter_strings_title">Custom filter</string>
|
||||||
|
<!-- 'Component path builder strings' is the technical name for identifying the Litho UI layout items to hide. This is an advanced feature and most users will never use this. -->
|
||||||
|
<string name="revanced_custom_filter_strings_summary">List of component path builder strings to filter separated by new line</string>
|
||||||
|
<string name="revanced_custom_filter_toast_invalid_syntax">Invalid custom filter: %s</string>
|
||||||
|
</patch>
|
||||||
</app>
|
</app>
|
||||||
<app id="youtube">
|
<app id="youtube">
|
||||||
<patch id="misc.settings.settingsPatch">
|
<patch id="misc.settings.settingsPatch">
|
||||||
|
|
@ -206,16 +225,6 @@ You will not be notified of any unexpected events."</string>
|
||||||
<string name="revanced_shorts_disable_background_playback_summary_on">Shorts background play is disabled</string>
|
<string name="revanced_shorts_disable_background_playback_summary_on">Shorts background play is disabled</string>
|
||||||
<string name="revanced_shorts_disable_background_playback_summary_off">Shorts background play is enabled</string>
|
<string name="revanced_shorts_disable_background_playback_summary_off">Shorts background play is enabled</string>
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="misc.debugging.enableDebuggingPatch">
|
|
||||||
<string name="revanced_debug_protobuffer_title">Log protocol buffer</string>
|
|
||||||
<string name="revanced_debug_protobuffer_summary_on">Debug logs include proto buffer</string>
|
|
||||||
<string name="revanced_debug_protobuffer_summary_off">Debug logs do not include proto buffer</string>
|
|
||||||
<string name="revanced_debug_protobuffer_user_dialog_message">"Enabling this setting will log additional layout data, including on-screen text for some UI components.
|
|
||||||
|
|
||||||
This can help identify components when creating custom filters.
|
|
||||||
|
|
||||||
However, enabling this will also log some user data such as your IP address."</string>
|
|
||||||
</patch>
|
|
||||||
<patch id="layout.hide.general.hideLayoutComponentsPatch">
|
<patch id="layout.hide.general.hideLayoutComponentsPatch">
|
||||||
<string name="revanced_hide_album_cards_title">Hide album cards</string>
|
<string name="revanced_hide_album_cards_title">Hide album cards</string>
|
||||||
<string name="revanced_hide_album_cards_summary_on">Album cards are hidden</string>
|
<string name="revanced_hide_album_cards_summary_on">Album cards are hidden</string>
|
||||||
|
|
@ -446,15 +455,6 @@ If a Doodle is currently showing in your region and this hide setting is on, the
|
||||||
<string name="revanced_hide_comments_thanks_button_title">Hide Thanks button</string>
|
<string name="revanced_hide_comments_thanks_button_title">Hide Thanks button</string>
|
||||||
<string name="revanced_hide_comments_thanks_button_summary_on">Thanks button is hidden</string>
|
<string name="revanced_hide_comments_thanks_button_summary_on">Thanks button is hidden</string>
|
||||||
<string name="revanced_hide_comments_thanks_button_summary_off">Thanks button is shown</string>
|
<string name="revanced_hide_comments_thanks_button_summary_off">Thanks button is shown</string>
|
||||||
<string name="revanced_custom_filter_screen_title">Custom filter</string>
|
|
||||||
<string name="revanced_custom_filter_screen_summary">Hide components using custom filters</string>
|
|
||||||
<string name="revanced_custom_filter_title">Enable custom filter</string>
|
|
||||||
<string name="revanced_custom_filter_summary_on">Custom filter is enabled</string>
|
|
||||||
<string name="revanced_custom_filter_summary_off">Custom filter is disabled</string>
|
|
||||||
<string name="revanced_custom_filter_strings_title">Custom filter</string>
|
|
||||||
<!-- 'Component path builder strings' is the technical name for identifying the Litho UI layout items to hide. This is an advanced feature and most users will never use this. -->
|
|
||||||
<string name="revanced_custom_filter_strings_summary">List of component path builder strings to filter separated by new line</string>
|
|
||||||
<string name="revanced_custom_filter_toast_invalid_syntax">Invalid custom filter: %s</string>
|
|
||||||
<string name="revanced_hide_view_count_title">Hide view count</string>
|
<string name="revanced_hide_view_count_title">Hide view count</string>
|
||||||
<string name="revanced_hide_view_count_summary_on">View count is hidden in feed and search results</string>
|
<string name="revanced_hide_view_count_summary_on">View count is hidden in feed and search results</string>
|
||||||
<string name="revanced_hide_view_count_summary_off">View count is shown in feed and search results</string>
|
<string name="revanced_hide_view_count_summary_off">View count is shown in feed and search results</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue