feat(YouTube Music): Add Hide layout components patch (#6365)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
rospino74
2026-01-22 18:36:37 +01:00
committed by GitHub
parent 156441d3cf
commit 71ce8230a9
43 changed files with 1114 additions and 952 deletions

View File

@@ -4,12 +4,12 @@ import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
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.EnumSetting;
import app.revanced.extension.shared.spoof.ClientType;
public class Settings extends BaseSettings {
public class Settings extends YouTubeAndMusicSettings {
// Ads
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);

View File

@@ -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;
@@ -15,13 +15,15 @@ import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.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.
*/
@SuppressWarnings("unused")
final class CustomFilter extends Filter {
public final class CustomFilter extends Filter {
private static void showInvalidSyntaxToast(@NonNull String expression) {
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
@@ -45,7 +47,7 @@ final class CustomFilter extends Filter {
@NonNull
@SuppressWarnings("ConstantConditions")
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
String rawCustomFilterText = YouTubeAndMusicSettings.CUSTOM_FILTER_STRINGS.get();
if (rawCustomFilterText.isBlank()) {
return Collections.emptyList();
}
@@ -100,7 +102,7 @@ final class CustomFilter extends Filter {
ByteTrieSearch bufferSearch;
CustomFilterGroup(boolean startsWith, @NonNull String path) {
super(Settings.CUSTOM_FILTER, path);
super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
this.startsWith = startsWith;
}
@@ -145,7 +147,7 @@ final class CustomFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// All callbacks are custom filter groups.
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
@@ -159,4 +161,4 @@ final class CustomFilter extends Filter {
return custom.bufferSearch.matches(buffer);
}
}
}

View File

@@ -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.Arrays;
import java.util.List;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
/**
* Filters litho based components.
*
@@ -14,11 +17,11 @@ import java.util.List;
* either an identifier or a path.
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
* or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
*
* All callbacks must be registered before the constructor completes.
*/
abstract class Filter {
public abstract class Filter {
public enum FilterContentType {
IDENTIFIER,
@@ -65,7 +68,7 @@ abstract class Filter {
* @param contentIndex Matched index of the identifier or path.
* @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) {
return true;
}

View File

@@ -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);
}
}
}

View File

@@ -1,21 +1,22 @@
package app.revanced.extension.youtube.patches.components;
package app.revanced.extension.shared.patches.litho;
import androidx.annotation.NonNull;
import java.util.*;
import java.util.function.Consumer;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.TrieSearch;
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
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 TrieSearch<V> search = createSearchGraph();
@SafeVarargs
protected final void addAll(final T... groups) {
public final void addAll(final T... groups) {
filterGroups.addAll(Arrays.asList(groups));
for (T group : groups) {
@@ -41,18 +42,7 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
return filterGroups.iterator();
}
@Override
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) {
public FilterGroup.FilterGroupResult check(V stack) {
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
search.matches(stack, result);
return result;
@@ -60,21 +50,21 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
}
protected abstract TrieSearch<V> createSearchGraph();
}
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
protected StringTrieSearch createSearchGraph() {
return new StringTrieSearch();
public static final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
protected StringTrieSearch createSearchGraph() {
return new StringTrieSearch();
}
}
}
/**
* If searching for a single byte pattern, then it is slightly better to use
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
* than a prefix tree to search for only 1 pattern.
*/
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
protected ByteTrieSearch createSearchGraph() {
return new ByteTrieSearch();
/**
* If searching for a single byte pattern, then it is slightly better to use
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
* than a prefix tree to search for only 1 pattern.
*/
public static final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
protected ByteTrieSearch createSearchGraph() {
return new ByteTrieSearch();
}
}
}

View File

@@ -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.Nullable;
@@ -7,9 +7,11 @@ import java.nio.ByteBuffer;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BaseSettings;
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")
public final class LithoFilterPatch {
@@ -36,7 +38,7 @@ public final class LithoFilterPatch {
builder.append(identifier);
builder.append(" Path: ");
builder.append(path);
if (Settings.DEBUG_PROTOBUFFER.get()) {
if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) {
builder.append(" BufferStrings: ");
findAsciiStrings(builder, buffer);
}

View File

@@ -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));
}

View File

@@ -11,6 +11,9 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
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;
@SuppressWarnings("unused")
@@ -153,8 +156,8 @@ public final class AdsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == playerShoppingShelf) {
return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered();
}

View File

@@ -1,5 +1,7 @@
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.settings.Settings;
@@ -19,7 +21,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
isVideoQualityMenuVisible = true;

View File

@@ -1,9 +1,13 @@
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;
@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 VIDEO_ACTION_BAR_PATH_PREFIX = "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
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == likeSubscribeGlow) {
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))

View File

@@ -1,10 +1,12 @@
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.shared.PlayerType;
@SuppressWarnings("unused")
final class CommentsFilter extends Filter {
public final class CommentsFilter extends Filter {
private static final String COMMENT_COMPOSER_PATH = "comment_composer.e";
@@ -88,8 +90,8 @@ final class CommentsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == chipBar) {
// Playlist sort button uses same components and must only filter if the player is opened.
return PlayerType.getCurrent().isMaximizedOrFullscreen()

View File

@@ -1,11 +1,14 @@
package app.revanced.extension.youtube.patches.components;
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.shared.PlayerType;
@SuppressWarnings("unused")
final class DescriptionComponentsFilter extends Filter {
public final class DescriptionComponentsFilter extends Filter {
private static final String INFOCARDS_SECTION_PATH = "infocards_section.e";
@@ -128,8 +131,8 @@ final class DescriptionComponentsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == aiGeneratedVideoSummarySection || matchedGroup == hypePoints) {
// Only hide if player is open, in case this component is used somewhere else.

View File

@@ -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);
}
}

View File

@@ -1,6 +1,8 @@
package app.revanced.extension.youtube.patches.components;
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")
public final class HideInfoCardsFilter extends Filter {

View File

@@ -17,6 +17,8 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.TrieSearch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
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.
*/
@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.
@@ -554,8 +556,8 @@ final class KeywordContentFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
return false;
}

View File

@@ -14,6 +14,9 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
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.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
@@ -342,7 +345,7 @@ public final class LayoutComponentsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// This identifier is used not only in players but also in search results:
// https://github.com/ReVanced/revanced-patches/issues/3245

View File

@@ -1,5 +1,7 @@
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.settings.Settings;
@@ -36,7 +38,7 @@ public final class PlaybackSpeedMenuFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == oldPlaybackMenuGroup) {
isOldPlaybackSpeedMenuVisible = true;

View File

@@ -3,13 +3,16 @@ package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
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.shared.ShortsPlayerState;
import java.util.List;
@SuppressWarnings("unused")
public class PlayerFlyoutMenuItemsFilter extends Filter {
public final class PlayerFlyoutMenuItemsFilter extends Filter {
public static final class HideAudioFlyoutMenuAvailability implements Setting.Availability {
@Override
@@ -94,7 +97,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == videoQualityMenuFooter) {
return true;

View File

@@ -13,6 +13,9 @@ import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.shared.Logger;
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.
@@ -84,13 +87,13 @@ public final class ReturnYouTubeDislikeFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
return false;
}
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(buffer);
FilterGroupResult result = videoIdFilterGroup.check(buffer);
if (result.isFiltered()) {
String matchedVideoId = findVideoId(buffer);
// Matched video will be null if in incognito mode.

View File

@@ -11,6 +11,9 @@ import java.util.Arrays;
import java.util.List;
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.shared.NavigationBar;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -339,7 +342,7 @@ public final class ShortsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentType == FilterContentType.PATH) {
if (matchedGroup == subscribeButton || matchedGroup == joinButton

View File

@@ -32,6 +32,7 @@ import android.graphics.Color;
import app.revanced.extension.shared.Logger;
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.BooleanSetting;
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.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
public class Settings extends BaseSettings {
public class Settings extends YouTubeAndMusicSettings {
// Video
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);
@@ -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,
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));
// 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
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);
@@ -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 BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("revanced_spoof_video_streams_av1", FALSE, true,
"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
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
@@ -382,7 +376,7 @@ public class Settings extends BaseSettings {
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_threshold", 30, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final IntegerSetting SWIPE_VOLUME_SENSITIVITY = new IntegerSetting("revanced_swipe_volume_sensitivity", 1, true, parent(SWIPE_VOLUME));
public static final EnumSetting<SwipeOverlayStyle> SWIPE_OVERLAY_STYLE = new EnumSetting<>("revanced_swipe_overlay_style", SwipeOverlayStyle.HORIZONTAL,true,
public static final EnumSetting<SwipeOverlayStyle> SWIPE_OVERLAY_STYLE = new EnumSetting<>("revanced_swipe_overlay_style", SwipeOverlayStyle.HORIZONTAL, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_text_overlay_size", 14, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
@@ -411,7 +405,9 @@ public class Settings extends BaseSettings {
// SponsorBlock
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 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));
@@ -460,7 +456,7 @@ public class Settings extends BaseSettings {
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFFFF", false, false);
// Deprecated migrations
private static final StringSetting DEPRECATED_SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_value", "#FF0033");
private static final StringSetting DEPRECATED_SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_value", "#FF0033");
private static final FloatSetting DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f, false, false);
private static final FloatSetting DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f, false, false);
@@ -512,7 +508,7 @@ public class Settings extends BaseSettings {
// or is spoofing to a version the same or newer than this app.
if (!SPOOF_APP_VERSION_TARGET.isSetToDefault() &&
(SPOOF_APP_VERSION_TARGET.get().compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0
|| (Utils.getAppVersionName().compareTo(SPOOF_APP_VERSION_TARGET.get()) <= 0))) {
|| (Utils.getAppVersionName().compareTo(SPOOF_APP_VERSION_TARGET.get()) <= 0))) {
Logger.printInfo(() -> "Resetting spoof app version");
SPOOF_APP_VERSION_TARGET.resetToDefault();
SPOOF_APP_VERSION.resetToDefault();

View File

@@ -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 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 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 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 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 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 fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;

View File

@@ -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"))
)

View File

@@ -7,20 +7,15 @@ import app.revanced.patches.shared.misc.debugging.enableDebuggingPatch
@Suppress("unused")
val enableDebuggingPatch = enableDebuggingPatch(
block = {
dependsOn(
sharedExtensionPatch,
settingsPatch,
sharedExtensionPatch = sharedExtensionPatch,
settingsPatch = settingsPatch,
compatibleWithPackages = arrayOf(
"com.google.android.apps.youtube.music" to setOf(
"7.29.52",
"8.10.52"
)
compatibleWith(
"com.google.android.apps.youtube.music"(
"7.29.52",
"8.10.52"
)
)
},
),
// String feature flag does not appear to be present with YT Music.
hookStringFeatureFlag = false,
preferenceScreen = PreferenceScreen.MISC
preferenceScreen = PreferenceScreen.MISC,
)

View File

@@ -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)
}

View File

@@ -11,3 +11,19 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
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"
}
}

View File

@@ -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()
}
}

View File

@@ -2,23 +2,14 @@ package app.revanced.patches.shared.misc.debugging
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.all.misc.resources.addResources
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.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.*
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
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 app.revanced.util.*
import com.android.tools.smali.dexlib2.Opcode
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.
*/
internal fun enableDebuggingPatch(
block: BytecodePatchBuilder.() -> Unit = {},
executeBlock: BytecodePatchContext.() -> Unit = {},
sharedExtensionPatch: Patch<*>,
settingsPatch: Patch<*>,
vararg compatibleWithPackages: Pair<String, Set<String>>,
hookStringFeatureFlag: Boolean,
preferenceScreen: BasePreferenceScreen.Screen,
additionalDebugPreferences: List<BasePreference> = emptyList()
) = bytecodePatch(
name = "Enable debugging",
description = "Adds options for debugging and exporting ReVanced logs to the clipboard.",
) {
compatibleWith(packages = compatibleWithPackages)
dependsOn(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
resourcePatch {
execute {
copyResources(
"settings",
ResourceGroup("drawable",
ResourceGroup(
"drawable",
// Action buttons.
"revanced_settings_copy_all.xml",
"revanced_settings_deselect_all.xml",
@@ -61,38 +56,29 @@ internal fun enableDebuggingPatch(
}
)
block()
execute {
executeBlock()
addResources("shared", "misc.debugging.enableDebuggingPatch")
val preferences = mutableSetOf<BasePreference>(
val preferences = setOf(
SwitchPreference("revanced_debug"),
)
preferences.addAll(additionalDebugPreferences)
preferences.addAll(
listOf(
SwitchPreference("revanced_debug_stacktrace"),
SwitchPreference("revanced_debug_toast_on_error"),
NonInteractivePreference(
"revanced_debug_export_logs_to_clipboard",
tag = "app.revanced.extension.shared.settings.preference.ExportLogToClipboardPreference",
selectable = true
),
NonInteractivePreference(
"revanced_debug_logs_clear_buffer",
tag = "app.revanced.extension.shared.settings.preference.ClearLogBufferPreference",
selectable = true
),
NonInteractivePreference(
"revanced_debug_feature_flags_manager",
tag = "app.revanced.extension.shared.settings.preference.FeatureFlagsManagerPreference",
selectable = true
)
SwitchPreference("revanced_debug_protobuffer"),
SwitchPreference("revanced_debug_stacktrace"),
SwitchPreference("revanced_debug_toast_on_error"),
NonInteractivePreference(
"revanced_debug_export_logs_to_clipboard",
tag = "app.revanced.extension.shared.settings.preference.ExportLogToClipboardPreference",
selectable = true
),
NonInteractivePreference(
"revanced_debug_logs_clear_buffer",
tag = "app.revanced.extension.shared.settings.preference.ClearLogBufferPreference",
selectable = true
),
NonInteractivePreference(
"revanced_debug_feature_flags_manager",
tag = "app.revanced.extension.shared.settings.preference.FeatureFlagsManagerPreference",
selectable = true
)
)

View File

@@ -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.
}
}

View File

@@ -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()
}

View File

@@ -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.addResourcesPatch
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.resourceMappingPatch
import app.revanced.patches.shared.misc.mapping.resourceMappings
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.ad.getpremium.hideGetPremiumPatch
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.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch

View File

@@ -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.settings.preference.PreferenceScreenPreference
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.settings.PreferenceScreen

View File

@@ -9,34 +9,27 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.util.smali.ExternalLabel
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.resourceMappingPatch
import app.revanced.patches.shared.misc.mapping.resourceMappings
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.navigation.navigationBarHookPatch
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.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.util.findFreeRegister
import app.revanced.util.findInstructionIndicesReversedOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import app.revanced.util.*
import com.android.tools.smali.dexlib2.Opcode
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.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
var expandButtonDownId = -1L
private set
@@ -108,184 +101,170 @@ private const val DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME =
private const val COMMENTS_FILTER_CLASS_NAME =
"Lapp/revanced/extension/youtube/patches/components/CommentsFilter;"
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 =
"Lapp/revanced/extension/youtube/patches/components/KeywordContentFilter;"
val hideLayoutComponentsPatch = bytecodePatch(
name = "Hide layout components",
description = "Adds options to hide general layout components.",
) {
dependsOn(
lithoFilterPatch,
settingsPatch,
addResourcesPatch,
val hideLayoutComponentsPatch = hideLayoutComponentsPatch(
lithoFilterPatch = lithoFilterPatch,
settingsPatch = settingsPatch,
additionalDependencies = setOf(
hideLayoutComponentsResourcePatch,
navigationBarHookPatch,
)
compatibleWith(
"com.google.android.youtube"(
),
filterClasses = setOf(
LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR,
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",
"20.07.39",
"20.13.41",
"20.14.43",
)
)
) {
addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch")
execute {
addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch")
PreferenceScreen.PLAYER.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_description_components_screen",
preferences = setOf(
SwitchPreference("revanced_hide_ai_generated_video_summary_section"),
SwitchPreference("revanced_hide_ask_section"),
SwitchPreference("revanced_hide_attributes_section"),
SwitchPreference("revanced_hide_chapters_section"),
SwitchPreference("revanced_hide_featured_links_section"),
SwitchPreference("revanced_hide_featured_videos_section"),
SwitchPreference("revanced_hide_info_cards_section"),
SwitchPreference("revanced_hide_how_this_was_made_section"),
SwitchPreference("revanced_hide_hype_points"),
SwitchPreference("revanced_hide_key_concepts_section"),
SwitchPreference("revanced_hide_podcast_section"),
SwitchPreference("revanced_hide_subscribe_button"),
SwitchPreference("revanced_hide_transcript_section"),
),
),
PreferenceScreenPreference(
"revanced_comments_screen",
preferences = setOf(
SwitchPreference("revanced_hide_comments_ai_chat_summary"),
SwitchPreference("revanced_hide_comments_ai_summary"),
SwitchPreference("revanced_hide_comments_channel_guidelines"),
SwitchPreference("revanced_hide_comments_by_members_header"),
SwitchPreference("revanced_hide_comments_section"),
SwitchPreference("revanced_hide_comments_community_guidelines"),
SwitchPreference("revanced_hide_comments_create_a_short_button"),
SwitchPreference("revanced_hide_comments_emoji_and_timestamp_buttons"),
SwitchPreference("revanced_hide_comments_preview_comment"),
SwitchPreference("revanced_hide_comments_thanks_button"),
),
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
),
SwitchPreference("revanced_hide_channel_bar"),
SwitchPreference("revanced_hide_channel_watermark"),
SwitchPreference("revanced_hide_crowdfunding_box"),
SwitchPreference("revanced_hide_emergency_box"),
SwitchPreference("revanced_hide_info_panels"),
SwitchPreference("revanced_hide_join_membership_button"),
SwitchPreference("revanced_hide_medical_panels"),
SwitchPreference("revanced_hide_quick_actions"),
SwitchPreference("revanced_hide_related_videos"),
SwitchPreference("revanced_hide_subscribers_community_guidelines"),
SwitchPreference("revanced_hide_timed_reactions"),
)
PreferenceScreen.PLAYER.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_description_components_screen",
preferences = setOf(
SwitchPreference("revanced_hide_ai_generated_video_summary_section"),
SwitchPreference("revanced_hide_ask_section"),
SwitchPreference("revanced_hide_attributes_section"),
SwitchPreference("revanced_hide_chapters_section"),
SwitchPreference("revanced_hide_featured_links_section"),
SwitchPreference("revanced_hide_featured_videos_section"),
SwitchPreference("revanced_hide_info_cards_section"),
SwitchPreference("revanced_hide_how_this_was_made_section"),
SwitchPreference("revanced_hide_hype_points"),
SwitchPreference("revanced_hide_key_concepts_section"),
SwitchPreference("revanced_hide_podcast_section"),
SwitchPreference("revanced_hide_subscribe_button"),
SwitchPreference("revanced_hide_transcript_section"),
PreferenceScreen.FEED.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_keyword_content_screen",
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
preferences = setOf(
SwitchPreference("revanced_hide_keyword_content_home"),
SwitchPreference("revanced_hide_keyword_content_subscriptions"),
SwitchPreference("revanced_hide_keyword_content_search"),
TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about",
tag = "app.revanced.extension.shared.settings.preference.BulletPointPreference"
),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about_whole_words",
tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference",
),
),
PreferenceScreenPreference(
"revanced_comments_screen",
preferences = setOf(
SwitchPreference("revanced_hide_comments_ai_chat_summary"),
SwitchPreference("revanced_hide_comments_ai_summary"),
SwitchPreference("revanced_hide_comments_channel_guidelines"),
SwitchPreference("revanced_hide_comments_by_members_header"),
SwitchPreference("revanced_hide_comments_section"),
SwitchPreference("revanced_hide_comments_community_guidelines"),
SwitchPreference("revanced_hide_comments_create_a_short_button"),
SwitchPreference("revanced_hide_comments_emoji_and_timestamp_buttons"),
SwitchPreference("revanced_hide_comments_preview_comment"),
SwitchPreference("revanced_hide_comments_thanks_button"),
),
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
),
PreferenceScreenPreference(
key = "revanced_hide_filter_bar_screen",
preferences = setOf(
SwitchPreference("revanced_hide_filter_bar_feed_in_feed"),
SwitchPreference("revanced_hide_filter_bar_feed_in_related_videos"),
SwitchPreference("revanced_hide_filter_bar_feed_in_search"),
SwitchPreference("revanced_hide_filter_bar_feed_in_history"),
),
SwitchPreference("revanced_hide_channel_bar"),
SwitchPreference("revanced_hide_channel_watermark"),
SwitchPreference("revanced_hide_crowdfunding_box"),
SwitchPreference("revanced_hide_emergency_box"),
SwitchPreference("revanced_hide_info_panels"),
SwitchPreference("revanced_hide_join_membership_button"),
SwitchPreference("revanced_hide_medical_panels"),
SwitchPreference("revanced_hide_quick_actions"),
SwitchPreference("revanced_hide_related_videos"),
SwitchPreference("revanced_hide_subscribers_community_guidelines"),
SwitchPreference("revanced_hide_timed_reactions"),
)
PreferenceScreen.FEED.addPreferences(
PreferenceScreenPreference(
key = "revanced_hide_keyword_content_screen",
sorting = PreferenceScreenPreference.Sorting.UNSORTED,
preferences = setOf(
SwitchPreference("revanced_hide_keyword_content_home"),
SwitchPreference("revanced_hide_keyword_content_subscriptions"),
SwitchPreference("revanced_hide_keyword_content_search"),
TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about",
tag = "app.revanced.extension.shared.settings.preference.BulletPointPreference"
),
NonInteractivePreference(
key = "revanced_hide_keyword_content_about_whole_words",
tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference",
),
),
),
PreferenceScreenPreference(
key = "revanced_channel_screen",
preferences = setOf(
SwitchPreference("revanced_hide_community_button"),
SwitchPreference("revanced_hide_for_you_shelf"),
SwitchPreference("revanced_hide_join_button"),
SwitchPreference("revanced_hide_links_preview"),
SwitchPreference("revanced_hide_members_shelf"),
SwitchPreference("revanced_hide_store_button"),
SwitchPreference("revanced_hide_subscribe_button_in_channel_page"),
),
PreferenceScreenPreference(
key = "revanced_hide_filter_bar_screen",
preferences = setOf(
SwitchPreference("revanced_hide_filter_bar_feed_in_feed"),
SwitchPreference("revanced_hide_filter_bar_feed_in_related_videos"),
SwitchPreference("revanced_hide_filter_bar_feed_in_search"),
SwitchPreference("revanced_hide_filter_bar_feed_in_history"),
),
),
PreferenceScreenPreference(
key = "revanced_channel_screen",
preferences = setOf(
SwitchPreference("revanced_hide_community_button"),
SwitchPreference("revanced_hide_for_you_shelf"),
SwitchPreference("revanced_hide_join_button"),
SwitchPreference("revanced_hide_links_preview"),
SwitchPreference("revanced_hide_members_shelf"),
SwitchPreference("revanced_hide_store_button"),
SwitchPreference("revanced_hide_subscribe_button_in_channel_page"),
),
),
SwitchPreference("revanced_hide_album_cards"),
SwitchPreference("revanced_hide_artist_cards"),
SwitchPreference("revanced_hide_chips_shelf"),
SwitchPreference("revanced_hide_community_posts"),
SwitchPreference("revanced_hide_compact_banner"),
SwitchPreference("revanced_hide_expandable_card"),
SwitchPreference("revanced_hide_floating_microphone_button"),
SwitchPreference(
key = "revanced_hide_horizontal_shelves",
tag = "app.revanced.extension.shared.settings.preference.BulletPointSwitchPreference"
),
SwitchPreference("revanced_hide_image_shelf"),
SwitchPreference("revanced_hide_latest_posts"),
SwitchPreference("revanced_hide_mix_playlists"),
SwitchPreference("revanced_hide_movies_section"),
SwitchPreference("revanced_hide_notify_me_button"),
SwitchPreference("revanced_hide_playables"),
SwitchPreference("revanced_hide_show_more_button"),
SwitchPreference("revanced_hide_surveys"),
SwitchPreference("revanced_hide_ticket_shelf"),
SwitchPreference("revanced_hide_upload_time"),
SwitchPreference("revanced_hide_video_recommendation_labels"),
SwitchPreference("revanced_hide_view_count"),
SwitchPreference("revanced_hide_visual_spacer"),
SwitchPreference("revanced_hide_doodles"),
)
),
SwitchPreference("revanced_hide_album_cards"),
SwitchPreference("revanced_hide_artist_cards"),
SwitchPreference("revanced_hide_chips_shelf"),
SwitchPreference("revanced_hide_community_posts"),
SwitchPreference("revanced_hide_compact_banner"),
SwitchPreference("revanced_hide_expandable_card"),
SwitchPreference("revanced_hide_floating_microphone_button"),
SwitchPreference(
key = "revanced_hide_horizontal_shelves",
tag = "app.revanced.extension.shared.settings.preference.BulletPointSwitchPreference"
),
SwitchPreference("revanced_hide_image_shelf"),
SwitchPreference("revanced_hide_latest_posts"),
SwitchPreference("revanced_hide_mix_playlists"),
SwitchPreference("revanced_hide_movies_section"),
SwitchPreference("revanced_hide_notify_me_button"),
SwitchPreference("revanced_hide_playables"),
SwitchPreference("revanced_hide_show_more_button"),
SwitchPreference("revanced_hide_surveys"),
SwitchPreference("revanced_hide_ticket_shelf"),
SwitchPreference("revanced_hide_upload_time"),
SwitchPreference("revanced_hide_video_recommendation_labels"),
SwitchPreference("revanced_hide_view_count"),
SwitchPreference("revanced_hide_visual_spacer"),
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),
),
),
)
// region Mix playlists
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)
(if (is_20_09_or_greater) parseElementFromBufferFingerprint
else if (is_20_07_or_greater) parseElementFromBufferLegacy2007Fingerprint
else parseElementFromBufferLegacy1901Fingerprint).let {
it.method.apply {
val byteArrayParameter = "p3"
val startIndex = it.patternMatch!!.startIndex
val conversionContextRegister = getInstruction<TwoRegisterInstruction>(startIndex).registerA
val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC }
val returnEmptyComponentRegister =
(returnEmptyComponentInstruction as FiveRegisterInstruction).registerC
val insertIndex = startIndex + 1
val freeRegister =
findFreeRegister(insertIndex, conversionContextRegister, returnEmptyComponentRegister)
// region Mix playlists
(if (is_20_09_or_greater) parseElementFromBufferFingerprint
else if (is_20_07_or_greater) parseElementFromBufferLegacy2007Fingerprint
else parseElementFromBufferLegacy1901Fingerprint).let {
it.method.apply {
val byteArrayParameter = "p3"
val startIndex = it.patternMatch!!.startIndex
val conversionContextRegister = getInstruction<TwoRegisterInstruction>(startIndex).registerA
val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC }
val returnEmptyComponentRegister = (returnEmptyComponentInstruction as FiveRegisterInstruction).registerC
val insertIndex = startIndex + 1
val freeRegister = findFreeRegister(insertIndex, conversionContextRegister, returnEmptyComponentRegister)
addInstructionsWithLabels(
insertIndex,
"""
addInstructionsWithLabels(
insertIndex,
"""
invoke-static { v$conversionContextRegister, $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->filterMixPlaylists(Ljava/lang/Object;[B)Z
move-result v$freeRegister
if-eqz v$freeRegister, :show
@@ -294,193 +273,192 @@ val hideLayoutComponentsPatch = bytecodePatch(
:show
nop
""",
ExternalLabel("return_empty_component", returnEmptyComponentInstruction),
)
}
ExternalLabel("return_empty_component", returnEmptyComponentInstruction),
)
}
}
// endregion
// endregion
// region Watermark (legacy code for old versions of YouTube)
// region Watermark (legacy code for old versions of YouTube)
showWatermarkFingerprint.match(
playerOverlayFingerprint.originalClassDef,
).method.apply {
val index = implementation!!.instructions.size - 5
showWatermarkFingerprint.match(
playerOverlayFingerprint.originalClassDef,
).method.apply {
val index = implementation!!.instructions.size - 5
removeInstruction(index)
addInstructions(
index,
"""
removeInstruction(index)
addInstructions(
index,
"""
invoke-static {}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->showWatermark()Z
move-result p2
""",
)
}
)
}
// endregion
// endregion
// region Show more button
// region Show more button
hideShowMoreButtonFingerprint.method.apply {
val moveRegisterIndex = hideShowMoreButtonFingerprint.patternMatch!!.endIndex
val viewRegister = getInstruction<OneRegisterInstruction>(moveRegisterIndex).registerA
hideShowMoreButtonFingerprint.method.apply {
val moveRegisterIndex = hideShowMoreButtonFingerprint.patternMatch!!.endIndex
val viewRegister = getInstruction<OneRegisterInstruction>(moveRegisterIndex).registerA
val insertIndex = moveRegisterIndex + 1
addInstruction(
insertIndex,
"invoke-static { v$viewRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideShowMoreButton(Landroid/view/View;)V",
)
}
// endregion
// region crowdfunding box
crowdfundingBoxFingerprint.let {
it.method.apply {
val insertIndex = it.patternMatch!!.endIndex
val objectRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
val insertIndex = moveRegisterIndex + 1
addInstruction(
insertIndex,
"invoke-static { v$viewRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideShowMoreButton(Landroid/view/View;)V",
"invoke-static {v$objectRegister}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideCrowdfundingBox(Landroid/view/View;)V",
)
}
}
// endregion
// endregion
// region crowdfunding box
crowdfundingBoxFingerprint.let {
it.method.apply {
val insertIndex = it.patternMatch!!.endIndex
val objectRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
// region hide album cards
addInstruction(
insertIndex,
"invoke-static {v$objectRegister}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideCrowdfundingBox(Landroid/view/View;)V",
)
}
}
albumCardsFingerprint.let {
it.method.apply {
val checkCastAnchorIndex = it.patternMatch!!.endIndex
val insertIndex = checkCastAnchorIndex + 1
val register = getInstruction<OneRegisterInstruction>(checkCastAnchorIndex).registerA
// endregion
// region hide album cards
albumCardsFingerprint.let {
it.method.apply {
val checkCastAnchorIndex = it.patternMatch!!.endIndex
val insertIndex = checkCastAnchorIndex + 1
val register = getInstruction<OneRegisterInstruction>(checkCastAnchorIndex).registerA
addInstruction(
insertIndex,
"invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
addInstruction(
insertIndex,
"invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" +
"->hideAlbumCard(Landroid/view/View;)V",
)
}
)
}
}
// endregion
// endregion
// region hide floating microphone
// region hide floating microphone
showFloatingMicrophoneButtonFingerprint.method.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(fabButtonId)
val booleanIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.IGET_BOOLEAN)
val register = getInstruction<TwoRegisterInstruction>(booleanIndex).registerA
showFloatingMicrophoneButtonFingerprint.method.apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(fabButtonId)
val booleanIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.IGET_BOOLEAN)
val register = getInstruction<TwoRegisterInstruction>(booleanIndex).registerA
addInstructions(
booleanIndex + 1,
"""
addInstructions(
booleanIndex + 1,
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideFloatingMicrophoneButton(Z)Z
move-result v$register
"""
)
}
// endregion
// region 'Yoodles'
yoodlesImageViewFingerprint.method.apply {
findInstructionIndicesReversedOrThrow {
getReference<MethodReference>()?.name == "setImageDrawable"
}.forEach { insertIndex ->
val drawableRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerD
val imageViewRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerC
replaceInstruction(
insertIndex,
"invoke-static { v$imageViewRegister, v$drawableRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->" +
"setDoodleDrawable(Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;)V"
)
}
}
// endregion
// endregion
// region 'Yoodles'
yoodlesImageViewFingerprint.method.apply {
findInstructionIndicesReversedOrThrow {
getReference<MethodReference>()?.name == "setImageDrawable"
}.forEach { insertIndex ->
val drawableRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerD
val imageViewRegister = getInstruction<FiveRegisterInstruction>(insertIndex).registerC
// region hide view count
replaceInstruction(
insertIndex,
"invoke-static { v$imageViewRegister, v$drawableRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->" +
"setDoodleDrawable(Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;)V"
)
}
hideViewCountFingerprint.method.apply {
val startIndex = hideViewCountFingerprint.patternMatch!!.startIndex
var returnStringRegister = getInstruction<OneRegisterInstruction>(startIndex).registerA
// Find the instruction where the text dimension is retrieved.
val applyDimensionIndex = indexOfFirstInstructionReversedOrThrow {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_STATIC &&
reference?.definingClass == "Landroid/util/TypedValue;" &&
reference.returnType == "F" &&
reference.name == "applyDimension" &&
reference.parameterTypes == listOf("I", "F", "Landroid/util/DisplayMetrics;")
}
// endregion
// A float value is passed which is used to determine subtitle text size.
val floatDimensionRegister = getInstruction<OneRegisterInstruction>(
applyDimensionIndex + 1
).registerA
// region hide view count
hideViewCountFingerprint.method.apply {
val startIndex = hideViewCountFingerprint.patternMatch!!.startIndex
var returnStringRegister = getInstruction<OneRegisterInstruction>(startIndex).registerA
// Find the instruction where the text dimension is retrieved.
val applyDimensionIndex = indexOfFirstInstructionReversedOrThrow {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_STATIC &&
reference?.definingClass == "Landroid/util/TypedValue;" &&
reference.returnType == "F" &&
reference.name == "applyDimension" &&
reference.parameterTypes == listOf("I", "F", "Landroid/util/DisplayMetrics;")
}
// A float value is passed which is used to determine subtitle text size.
val floatDimensionRegister = getInstruction<OneRegisterInstruction>(
applyDimensionIndex + 1
).registerA
addInstructions(
applyDimensionIndex - 1,
"""
addInstructions(
applyDimensionIndex - 1,
"""
invoke-static { v$returnStringRegister, v$floatDimensionRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->modifyFeedSubtitleSpan(Landroid/text/SpannableString;F)Landroid/text/SpannableString;
move-result-object v$returnStringRegister
"""
)
}
)
}
// endregion
// endregion
// region hide filter bar
// region hide filter bar
/**
* Patch a [Method] with a given [instructions].
*
* @param RegisterInstruction The type of instruction to get the register from.
* @param insertIndexOffset The offset to add to the end index of the [Match.patternMatch].
* @param hookRegisterOffset The offset to add to the register of the hook.
* @param instructions The instructions to add with the register as a parameter.
*/
fun <RegisterInstruction : OneRegisterInstruction> Fingerprint.patch(
insertIndexOffset: Int = 0,
hookRegisterOffset: Int = 0,
instructions: (Int) -> String,
) = method.apply {
val endIndex = patternMatch!!.endIndex
/**
* Patch a [Method] with a given [instructions].
*
* @param RegisterInstruction The type of instruction to get the register from.
* @param insertIndexOffset The offset to add to the end index of the [Match.patternMatch].
* @param hookRegisterOffset The offset to add to the register of the hook.
* @param instructions The instructions to add with the register as a parameter.
*/
fun <RegisterInstruction : OneRegisterInstruction> Fingerprint.patch(
insertIndexOffset: Int = 0,
hookRegisterOffset: Int = 0,
instructions: (Int) -> String,
) = method.apply {
val endIndex = patternMatch!!.endIndex
val insertIndex = endIndex + insertIndexOffset
val register =
getInstruction<RegisterInstruction>(endIndex + hookRegisterOffset).registerA
val insertIndex = endIndex + insertIndexOffset
val register =
getInstruction<RegisterInstruction>(endIndex + hookRegisterOffset).registerA
addInstructions(insertIndex, instructions(register))
}
addInstructions(insertIndex, instructions(register))
}
filterBarHeightFingerprint.patch<TwoRegisterInstruction> { register ->
"""
filterBarHeightFingerprint.patch<TwoRegisterInstruction> { register ->
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInFeed(I)I
move-result v$register
"""
}
}
searchResultsChipBarFingerprint.patch<OneRegisterInstruction>(-1, -2) { register ->
"""
searchResultsChipBarFingerprint.patch<OneRegisterInstruction>(-1, -2) { register ->
"""
invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInSearch(I)I
move-result v$register
"""
}
}
relatedChipCloudFingerprint.patch<OneRegisterInstruction>(1) { register ->
"invoke-static { v$register }, " +
relatedChipCloudFingerprint.patch<OneRegisterInstruction>(1) { register ->
"invoke-static { v$register }, " +
"$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V"
}
}
}

View File

@@ -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.settings.preference.SwitchPreference
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.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch

View File

@@ -5,7 +5,7 @@ import app.revanced.patches.all.misc.resources.addResources
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.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.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen

View File

@@ -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.SwitchPreference
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.navigation.navigationBarHookPatch
import app.revanced.patches.youtube.misc.playservice.is_19_41_or_greater

View File

@@ -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.SwitchPreference
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.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater

View File

@@ -1,33 +1,22 @@
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.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.settingsPatch
@Suppress("unused")
val enableDebuggingPatch = enableDebuggingPatch(
block = {
dependsOn(
sharedExtensionPatch,
settingsPatch,
sharedExtensionPatch = sharedExtensionPatch,
settingsPatch = settingsPatch,
compatibleWithPackages = arrayOf(
"com.google.android.youtube" to setOf(
"19.34.42",
"20.07.39",
"20.13.41",
"20.14.43",
)
compatibleWith(
"com.google.android.youtube"(
"19.34.42",
"20.07.39",
"20.13.41",
"20.14.43",
)
)
},
executeBlock = {
addResources("youtube", "misc.debugging.enableDebuggingPatch")
},
),
hookStringFeatureFlag = true,
preferenceScreen = PreferenceScreen.MISC,
additionalDebugPreferences = listOf(SwitchPreference("revanced_debug_protobuffer"))
)

View File

@@ -1,58 +1,8 @@
package app.revanced.patches.youtube.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 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 {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)

View File

@@ -3,225 +3,68 @@
package app.revanced.patches.youtube.misc.litho.filter
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.bytecodePatch
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patches.shared.misc.litho.filter.lithoFilterPatch
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_20_05_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
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.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
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
private set
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/components/LithoFilterPatch;"
val lithoFilterPatch = bytecodePatch(
description = "Hooks the method which parses the bytes into a ComponentContext to filter components.",
val lithoFilterPatch = lithoFilterPatch(
componentCreateInsertionIndex = {
if (is_19_17_or_greater) {
indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
} else {
// 19.16 clobbers p2 so must check at start of the method
0
}
},
conversionContextFingerprintToString = conversionContextFingerprintToString,
executeBlock = BytecodePatchContext::executeBlock,
) {
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)
} else {
// 19.16 clobbers p2 so must check at start of the method and not at the return index.
0
}
val freeRegister = findFreeRegister(insertIndex)
val identifierRegister = findFreeRegister(insertIndex, freeRegister)
val pathRegister = findFreeRegister(insertIndex, freeRegister, identifierRegister)
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
// 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.
// Turn off native code that handles litho component names. If this feature is on then nearly
// all litho components have a null name and identifier/path filtering is completely broken.
//
// Flag was removed in 20.05. It appears a new flag might be used instead (45660109L),
// but if the flag is forced on then litho filtering still works correctly.
if (is_19_25_or_greater && !is_20_05_or_greater) {
lithoComponentNameUpbFeatureFlagFingerprint.method.apply {
// Don't use return early, so the debug patch logs if this was originally on.
val insertIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN)
val register = getInstruction<OneRegisterInstruction>(insertIndex).registerA
addInstruction(insertIndex, "const/4 v$register, 0x0")
}
}
// Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf).
// If this is enabled, then the litho protobuffer hook will always show an empty buffer
// since it's no longer handled by the hooked Java code.
lithoConverterBufferUpbFeatureFlagFingerprint.method.apply {
val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstruction(index + 1, "const/4 v$register, 0x0")
}
// endregion
}
finalize {
lithoFilterFingerprint.method.replaceInstruction(0, "const/16 v0, $filterCount")
}
dependsOn(versionCheckPatch)
}
private fun BytecodePatchContext.executeBlock() {
// 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
// all litho components have a null name and identifier/path filtering is completely broken.
//
// Flag was removed in 20.05. It appears a new flag might be used instead (45660109L),
// but if the flag is forced on then litho filtering still works correctly.
if (is_19_25_or_greater && !is_20_05_or_greater) {
lithoComponentNameUpbFeatureFlagFingerprint.method.apply {
// Don't use return early, so the debug patch logs if this was originally on.
val insertIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN)
val register = getInstruction<OneRegisterInstruction>(insertIndex).registerA
addInstruction(insertIndex, "const/4 v$register, 0x0")
}
}
// Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf).
// If this is enabled, then the litho protobuffer hook will always show an empty buffer
// since it's no longer handled by the hooked Java code.
lithoConverterBufferUpbFeatureFlagFingerprint.method.apply {
val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstruction(index + 1, "const/4 v$register, 0x0")
}
// endregion
// Set the addLithoFilter function to the one from the shared patch.
// This is done for backwards compatibility.
addLithoFilter = app.revanced.patches.shared.misc.litho.filter.addLithoFilter
}

View File

@@ -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.settings.preference.SwitchPreference
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.recyclerviewtree.hook.addRecyclerViewTreeHook
import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch

View File

@@ -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.TextPreference
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.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch

View File

@@ -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_reset">Flags reset</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 id="misc.privacy.sanitizeSharingLinksPatch">
<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_off">Shared links use music.youtube.com</string>
</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 id="youtube">
<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_off">Shorts background play is enabled</string>
</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">
<string name="revanced_hide_album_cards_title">Hide album cards</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_summary_on">Thanks button is hidden</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_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>