diff --git a/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java b/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java deleted file mode 100644 index b9897c10..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/ButtonsPatch.java +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.integrations.patches; - -import app.revanced.integrations.settings.SettingsEnum; - -final class ButtonsPatch extends Filter { - private final BlockRule actionBarRule; - - public ButtonsPatch() { - actionBarRule = new BlockRule(null, "video_action_bar"); - pathRegister.registerAll( - new BlockRule(SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, "|like_button", "dislike_button"), - new BlockRule(SettingsEnum.HIDE_DOWNLOAD_BUTTON, "download_button"), - new BlockRule(SettingsEnum.HIDE_PLAYLIST_BUTTON, "save_to_playlist_button"), - new BlockRule(SettingsEnum.HIDE_CLIP_BUTTON, "|clip_button.eml|"), - new BlockRule(SettingsEnum.HIDE_ACTION_BUTTONS, "ContainerType|video_action_button", "|CellType|CollectionType|CellType|ContainerType|button.eml|") - ); - } - - private boolean canHideActionBar() { - for (BlockRule rule : pathRegister) if (!rule.isEnabled()) return false; - return true; - } - - @Override - public boolean filter(final String path, final String identifier) { - // If everything is hidden, then also hide the video bar itself. - if (canHideActionBar() && actionBarRule.check(identifier).isBlocked()) return true; - - return pathRegister.contains(path); - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/CommentsPatch.java b/app/src/main/java/app/revanced/integrations/patches/CommentsPatch.java deleted file mode 100644 index 41a1887c..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/CommentsPatch.java +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.integrations.patches; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; - -final class CommentsPatch extends Filter { - - public CommentsPatch() { - var comments = new BlockRule(SettingsEnum.HIDE_COMMENTS_SECTION, "video_metadata_carousel", "_comments"); - var previewComment = new BlockRule( - SettingsEnum.HIDE_PREVIEW_COMMENT, - "|carousel_item", - "comments_entry_point_teaser", - "comments_entry_point_simplebox" - ); - - this.pathRegister.registerAll( - comments, - previewComment - ); - } - - @Override - boolean filter(String path, String _identifier) { - if (!pathRegister.contains(path)) return false; - - LogHelper.printDebug(() -> "Blocked: " + path); - - return true; - } -} diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java index 3b3d0676..e69de29b 100644 --- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java +++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java @@ -1,189 +0,0 @@ -package app.revanced.integrations.patches; - -import android.view.View; -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -public final class GeneralAdsPatch extends Filter { - private final String[] IGNORE = { - "home_video_with_context", - "related_video_with_context", - "comment_thread", // skip blocking anything in the comments - "|comment.", // skip blocking anything in the comments replies - "library_recent_shelf", - }; - - private final BlockRule custom = new CustomBlockRule( - SettingsEnum.CUSTOM_FILTER, - SettingsEnum.CUSTOM_FILTER_STRINGS - ); - - public GeneralAdsPatch() { - var communityPosts = new BlockRule(SettingsEnum.HIDE_COMMUNITY_POSTS, "post_base_wrapper"); - var communityGuidelines = new BlockRule(SettingsEnum.HIDE_COMMUNITY_GUIDELINES, "community_guidelines"); - var subscribersCommunityGuidelines = new BlockRule(SettingsEnum.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES, "sponsorships_comments_upsell"); - var channelMemberShelf = new BlockRule(SettingsEnum.HIDE_CHANNEL_MEMBER_SHELF, "member_recognition_shelf"); - var compactBanner = new BlockRule(SettingsEnum.HIDE_COMPACT_BANNER, "compact_banner"); - var inFeedSurvey = new BlockRule(SettingsEnum.HIDE_FEED_SURVEY, "in_feed_survey", "slimline_survey"); - var medicalPanel = new BlockRule(SettingsEnum.HIDE_MEDICAL_PANELS, "medical_panel"); - var merchandise = new BlockRule(SettingsEnum.HIDE_MERCHANDISE_BANNERS, "product_carousel"); - var infoPanel = new BlockRule(SettingsEnum.HIDE_HIDE_INFO_PANELS, "publisher_transparency_panel", "single_item_information_panel"); - var channelGuidelines = new BlockRule(SettingsEnum.HIDE_HIDE_CHANNEL_GUIDELINES, "channel_guidelines_entry_banner"); - var audioTrackButton = new BlockRule(SettingsEnum.HIDE_AUDIO_TRACK_BUTTON, "multi_feed_icon_button"); - var artistCard = new BlockRule(SettingsEnum.HIDE_ARTIST_CARDS, "official_card"); - var chapterTeaser = new BlockRule(SettingsEnum.HIDE_CHAPTER_TEASER, "expandable_metadata", "macro_markers_carousel"); - var viewProducts = new BlockRule(SettingsEnum.HIDE_PRODUCTS_BANNER, "product_item", "products_in_video"); - var webLinkPanel = new BlockRule(SettingsEnum.HIDE_WEB_SEARCH_RESULTS, "web_link_panel"); - var channelBar = new BlockRule(SettingsEnum.HIDE_CHANNEL_BAR, "channel_bar"); - var relatedVideos = new BlockRule(SettingsEnum.HIDE_RELATED_VIDEOS, "fullscreen_related_videos"); - var quickActions = new BlockRule(SettingsEnum.HIDE_QUICK_ACTIONS, "quick_actions"); - var imageShelf = new BlockRule(SettingsEnum.HIDE_IMAGE_SHELF, "image_shelf"); - var graySeparator = new BlockRule(SettingsEnum.HIDE_GRAY_SEPARATOR, - "cell_divider" // layout residue (gray line above the buttoned ad), - ); - var paidContent = new BlockRule(SettingsEnum.HIDE_PAID_CONTENT, "paid_content_overlay"); - var latestPosts = new BlockRule(SettingsEnum.HIDE_HIDE_LATEST_POSTS, "post_shelf"); - var selfSponsor = new BlockRule(SettingsEnum.HIDE_SELF_SPONSOR, "cta_shelf_card"); - var buttonedAd = new BlockRule(SettingsEnum.HIDE_BUTTONED_ADS, - "_buttoned_layout", - "full_width_square_image_layout", - "_ad_with", - "video_display_button_group_layout", - "landscape_image_wide_button_layout" - ); - var generalAds = new BlockRule( - SettingsEnum.HIDE_GENERAL_ADS, - "ads_video_with_context", - "banner_text_icon", - "square_image_layout", - "watch_metadata_app_promo", - "video_display_full_layout", - "hero_promo_image", - "statement_banner", - "carousel_footered_layout", - "text_image_button_layout", - "primetime_promo", - "product_details", - "full_width_portrait_image_layout", - "brand_video_shelf" - ); - var movieAds = new BlockRule( - SettingsEnum.HIDE_MOVIES_SECTION, - "browsy_bar", - "compact_movie", - "horizontal_movie_shelf", - "movie_and_show_upsell_card", - "compact_tvfilm_item", - "offer_module_root" - ); - - this.pathRegister.registerAll( - generalAds, - buttonedAd, - channelBar, - communityPosts, - paidContent, - latestPosts, - movieAds, - chapterTeaser, - communityGuidelines, - quickActions, - relatedVideos, - compactBanner, - inFeedSurvey, - viewProducts, - medicalPanel, - merchandise, - infoPanel, - channelGuidelines, - audioTrackButton, - artistCard, - selfSponsor, - webLinkPanel, - imageShelf, - subscribersCommunityGuidelines, - channelMemberShelf - ); - - var carouselAd = new BlockRule(SettingsEnum.HIDE_GENERAL_ADS, - "carousel_ad" - ); - var shorts = new BlockRule(SettingsEnum.HIDE_SHORTS, - "shorts_shelf", - "inline_shorts", - "shorts_grid" - ); - - this.identifierRegister.registerAll( - shorts, - graySeparator, - carouselAd - ); - } - - public boolean filter(final String path, final String identifier) { - BlockResult result; - - if (custom.isEnabled() && custom.check(path).isBlocked()) - result = BlockResult.CUSTOM; - else if (ReVancedUtils.containsAny(path, IGNORE)) - result = BlockResult.IGNORED; - else if (pathRegister.contains(path) || identifierRegister.contains(identifier)) - result = BlockResult.DEFINED; - else - result = BlockResult.UNBLOCKED; - - LogHelper.printDebug(() -> String.format("%s (ID: %s): %s", result.message, identifier, path)); - - return result.filter; - } - - private enum BlockResult { - UNBLOCKED(false, "Unblocked"), - IGNORED(false, "Ignored"), - DEFINED(true, "Blocked"), - CUSTOM(true, "Custom"); - - final Boolean filter; - final String message; - - BlockResult(boolean filter, String message) { - this.filter = filter; - this.message = message; - } - } - - /** - * Hide a view. - * - * @param condition The setting to check for hiding the view. - * @param view The view to hide. - */ - private static void hideView(SettingsEnum condition, View view) { - if (!condition.getBoolean()) return; - - LogHelper.printDebug(() -> "Hiding view with setting: " + condition); - - ReVancedUtils.HideViewByLayoutParams(view); - } - - /** - * Hide the view, which shows ads in the homepage. - * - * @param view The view, which shows ads. - */ - public static void hideAdAttributionView(View view) { - hideView(SettingsEnum.HIDE_GENERAL_ADS, view); - } - - /** - * Hide the view, which shows reels in the homepage. - * - * @param view The view, which shows reels. - */ - public static void hideReelView(View view) { - hideView(SettingsEnum.HIDE_SHORTS, view); - } - -} diff --git a/app/src/main/java/app/revanced/integrations/patches/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/LithoFilterPatch.java deleted file mode 100644 index 2d8dafb3..00000000 --- a/app/src/main/java/app/revanced/integrations/patches/LithoFilterPatch.java +++ /dev/null @@ -1,140 +0,0 @@ -package app.revanced.integrations.patches; - -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.Spliterator; -import java.util.function.Consumer; - -import app.revanced.integrations.settings.SettingsEnum; -import app.revanced.integrations.utils.LogHelper; -import app.revanced.integrations.utils.ReVancedUtils; - -class BlockRule { - final static class BlockResult { - private final boolean blocked; - private final SettingsEnum setting; - - public BlockResult(final SettingsEnum setting, final boolean blocked) { - this.setting = setting; - this.blocked = blocked; - } - - public SettingsEnum getSetting() { - return setting; - } - - public boolean isBlocked() { - return blocked; - } - } - - protected final SettingsEnum setting; - private final String[] blocks; - - /** - * Initialize a new rule for components. - * - * @param setting The setting which controls the blocking of this component. - * @param blocks The rules to block the component on. - */ - public BlockRule(final SettingsEnum setting, final String... blocks) { - this.setting = setting; - this.blocks = blocks; - } - - public boolean isEnabled() { - return setting.getBoolean(); - } - - public BlockResult check(final String string) { - return new BlockResult(setting, string != null && ReVancedUtils.containsAny(string, blocks)); - } -} - -final class CustomBlockRule extends BlockRule { - /** - * Initialize a new rule for components. - * - * @param setting The setting which controls the blocking of the components. - * @param filter The setting which contains the list of component names. - */ - public CustomBlockRule(final SettingsEnum setting, final SettingsEnum filter) { - super(setting, filter.getString().split(",")); - } -} - - -abstract class Filter { - final protected LithoBlockRegister pathRegister = new LithoBlockRegister(); - final protected LithoBlockRegister identifierRegister = new LithoBlockRegister(); - - abstract boolean filter(final String path, final String identifier); -} - -final class LithoBlockRegister implements Iterable { - private final ArrayList blocks = new ArrayList<>(); - - public void registerAll(BlockRule... blocks) { - this.blocks.addAll(Arrays.asList(blocks)); - } - - @NonNull - @Override - public Iterator iterator() { - return blocks.iterator(); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public void forEach(@NonNull Consumer action) { - blocks.forEach(action); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @NonNull - @Override - public Spliterator spliterator() { - return blocks.spliterator(); - } - - public boolean contains(String path) { - for (var rule : this) { - if (!rule.isEnabled()) continue; - - var result = rule.check(path); - if (result.isBlocked()) { - return true; - } - } - - return false; - } -} - -public final class LithoFilterPatch { - private static final Filter[] filters = new Filter[]{ - new GeneralAdsPatch(), - new ButtonsPatch(), - new CommentsPatch(), - }; - - public static boolean filter(final StringBuilder pathBuilder, final String identifier) { - var path = pathBuilder.toString(); - if (path.isEmpty()) return false; - - LogHelper.printDebug(() -> String.format("Searching (ID: %s): %s", identifier, path)); - - for (var filter : filters) { - if (filter.filter(path, identifier)) return true; - } - - return false; - } -} - diff --git a/app/src/main/java/app/revanced/integrations/patches/litho/AdsFilter.java b/app/src/main/java/app/revanced/integrations/patches/litho/AdsFilter.java new file mode 100644 index 00000000..f920c6fd --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/litho/AdsFilter.java @@ -0,0 +1,297 @@ +package app.revanced.integrations.patches.litho; + +import android.view.View; + +import app.revanced.integrations.adremover.AdRemoverAPI; +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +public final class AdsFilter extends Filter { + private final String[] EXCEPTIONS; + + private final CustomFilterGroup custom; + + public AdsFilter() { + EXCEPTIONS = new String[]{ + "home_video_with_context", + "related_video_with_context", + "comment_thread", // skip filtering anything in the comments + "|comment.", // skip filtering anything in the comments replies + "library_recent_shelf", + }; + + custom = new CustomFilterGroup( + SettingsEnum.ADREMOVER_CUSTOM_ENABLED, + SettingsEnum.ADREMOVER_CUSTOM_REMOVAL + ); + + final var communityPosts = new StringFilterGroup( + SettingsEnum.ADREMOVER_COMMUNITY_POSTS_REMOVAL, + "post_base_wrapper" + ); + + final var communityGuidelines = new StringFilterGroup( + SettingsEnum.ADREMOVER_COMMUNITY_GUIDELINES_REMOVAL, + "community_guidelines" + ); + + final var subscribersCommunityGuidelines = new StringFilterGroup( + SettingsEnum.ADREMOVER_SUBSCRIBERS_COMMUNITY_GUIDELINES_REMOVAL, + "sponsorships_comments_upsell" + ); + + + final var channelMemberShelf = new StringFilterGroup( + SettingsEnum.ADREMOVER_CHANNEL_MEMBER_SHELF_REMOVAL, + "member_recognition_shelf" + ); + + final var compactBanner = new StringFilterGroup( + SettingsEnum.ADREMOVER_COMPACT_BANNER_REMOVAL, + "compact_banner" + ); + + final var inFeedSurvey = new StringFilterGroup( + SettingsEnum.ADREMOVER_FEED_SURVEY_REMOVAL, + "in_feed_survey", + "slimline_survey" + ); + + final var medicalPanel = new StringFilterGroup( + SettingsEnum.ADREMOVER_MEDICAL_PANEL_REMOVAL, + "medical_panel" + ); + + final var paidContent = new StringFilterGroup( + SettingsEnum.ADREMOVER_PAID_CONTENT_REMOVAL, + "paid_content_overlay" + ); + + final var merchandise = new StringFilterGroup( + SettingsEnum.ADREMOVER_MERCHANDISE_REMOVAL, + "product_carousel" + ); + + final var infoPanel = new StringFilterGroup( + SettingsEnum.ADREMOVER_INFO_PANEL_REMOVAL, + "publisher_transparency_panel", + "single_item_information_panel" + ); + + final var latestPosts = new StringFilterGroup( + SettingsEnum.ADREMOVER_HIDE_LATEST_POSTS, + "post_shelf" + ); + + final var channelGuidelines = new StringFilterGroup( + SettingsEnum.ADREMOVER_HIDE_CHANNEL_GUIDELINES, + "channel_guidelines_entry_banner" + ); + + final var audioTrackButton = new StringFilterGroup( + SettingsEnum.HIDE_AUDIO_TRACK_BUTTON, + "multi_feed_icon_button" + ); + + final var artistCard = new StringFilterGroup( + SettingsEnum.HIDE_ARTIST_CARDS, + "official_card" + ); + + final var selfSponsor = new StringFilterGroup( + SettingsEnum.ADREMOVER_SELF_SPONSOR_REMOVAL, + "cta_shelf_card" + ); + + final var chapterTeaser = new StringFilterGroup( + SettingsEnum.ADREMOVER_CHAPTER_TEASER_REMOVAL, + "expandable_metadata", + "macro_markers_carousel" + ); + + final var viewProducts = new StringFilterGroup( + SettingsEnum.ADREMOVER_VIEW_PRODUCTS, + "product_item", + "products_in_video" + ); + + final var webLinkPanel = new StringFilterGroup( + SettingsEnum.ADREMOVER_WEB_SEARCH_RESULTS, + "web_link_panel" + ); + + final var channelBar = new StringFilterGroup( + SettingsEnum.ADREMOVER_CHANNEL_BAR, + "channel_bar" + ); + + final var relatedVideos = new StringFilterGroup( + SettingsEnum.ADREMOVER_RELATED_VIDEOS, + "fullscreen_related_videos" + ); + + final var quickActions = new StringFilterGroup( + SettingsEnum.ADREMOVER_QUICK_ACTIONS, + "quick_actions" + ); + + final var imageShelf = new StringFilterGroup( + SettingsEnum.ADREMOVER_IMAGE_SHELF, + "image_shelf" + ); + + final var graySeparator = new StringFilterGroup( + SettingsEnum.ADREMOVER_GRAY_SEPARATOR, + "cell_divider" // layout residue (gray line above the buttoned ad), + ); + + final var buttonedAd = new StringFilterGroup( + SettingsEnum.ADREMOVER_BUTTONED_REMOVAL, + "_buttoned_layout", + "full_width_square_image_layout", + "_ad_with", + "video_display_button_group_layout", + "landscape_image_wide_button_layout" + ); + + final var generalAds = new StringFilterGroup( + SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL, + "ads_video_with_context", + "banner_text_icon", + "square_image_layout", + "watch_metadata_app_promo", + "video_display_full_layout", + "hero_promo_image", + "statement_banner", + "carousel_footered_layout", + "text_image_button_layout", + "primetime_promo", + "product_details", + "full_width_portrait_image_layout", + "brand_video_shelf" + ); + + final var movieAds = new StringFilterGroup( + SettingsEnum.ADREMOVER_MOVIE_REMOVAL, + "browsy_bar", + "compact_movie", + "horizontal_movie_shelf", + "movie_and_show_upsell_card", + "compact_tvfilm_item", + "offer_module_root" + ); + + this.pathFilterGroups.addAll( + generalAds, + buttonedAd, + channelBar, + communityPosts, + paidContent, + latestPosts, + movieAds, + chapterTeaser, + communityGuidelines, + quickActions, + relatedVideos, + compactBanner, + inFeedSurvey, + viewProducts, + medicalPanel, + merchandise, + infoPanel, + channelGuidelines, + audioTrackButton, + artistCard, + selfSponsor, + webLinkPanel, + imageShelf, + subscribersCommunityGuidelines, + channelMemberShelf + ); + + final var carouselAd = new StringFilterGroup( + SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL, + "carousel_ad" + ); + + final var shorts = new StringFilterGroup( + SettingsEnum.ADREMOVER_SHORTS_REMOVAL, + "reels_player_overlay", + "shorts_shelf", + "inline_shorts", + "shorts_grid" + ); + + this.identifierFilterGroups.addAll( + shorts, + graySeparator, + carouselAd + ); + } + + @Override + public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) { + FilterResult result; + + if (custom.isEnabled() && custom.contains(path).isFiltered()) + result = FilterResult.CUSTOM; + else if (ReVancedUtils.containsAny(path, EXCEPTIONS)) + result = FilterResult.EXCEPTION; + else if (pathFilterGroups.contains(path) || identifierFilterGroups.contains(identifier)) + result = FilterResult.FILTERED; + else + result = FilterResult.UNFILTERED; + + LogHelper.printDebug(() -> String.format("%s (ID: %s): %s", result.message, identifier, path)); + + return result.filter; + } + + private enum FilterResult { + UNFILTERED(false, "Unfiltered"), + EXCEPTION(false, "Exception"), + FILTERED(true, "Filtered"), + CUSTOM(true, "Custom"); + + final Boolean filter; + final String message; + + FilterResult(boolean filter, String message) { + this.filter = filter; + this.message = message; + } + } + + /** + * Hide a view. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + private static void hideView(SettingsEnum condition, View view) { + if (!condition.getBoolean()) return; + + LogHelper.printDebug(() -> "Hiding view with setting: " + condition); + + AdRemoverAPI.HideViewWithLayout1dp(view); + } + + /** + * Hide the view, which shows ads in the homepage. + * + * @param view The view, which shows ads. + */ + public static void hideAdAttributionView(View view) { + hideView(SettingsEnum.ADREMOVER_GENERAL_ADS_REMOVAL, view); + } + + /** + * Hide the view, which shows reels in the homepage. + * + * @param view The view, which shows reels. + */ + public static void hideReelView(View view) { + hideView(SettingsEnum.ADREMOVER_SHORTS_REMOVAL, view); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/litho/ButtonsFilter.java b/app/src/main/java/app/revanced/integrations/patches/litho/ButtonsFilter.java new file mode 100644 index 00000000..8dc7030f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/litho/ButtonsFilter.java @@ -0,0 +1,54 @@ +package app.revanced.integrations.patches.litho; + +import app.revanced.integrations.settings.SettingsEnum; + +final class ButtonsFilter extends Filter { + private final StringFilterGroup actionBarRule; + + public ButtonsFilter() { + actionBarRule = new StringFilterGroup( + null, + "video_action_bar" + ); + + pathFilterGroups.addAll( + new StringFilterGroup( + SettingsEnum.HIDE_LIKE_DISLIKE_BUTTON, + "|like_button", + "dislike_button" + ), + new StringFilterGroup( + SettingsEnum.HIDE_DOWNLOAD_BUTTON, + "download_button" + ), + new StringFilterGroup( + SettingsEnum.HIDE_PLAYLIST_BUTTON, + "save_to_playlist_button" + ), + new StringFilterGroup( + SettingsEnum.HIDE_CLIP_BUTTON, + "|clip_button.eml|" + ), + new StringFilterGroup( + SettingsEnum.HIDE_ACTION_BUTTONS, + "ContainerType|video_action_button", + "|CellType|CollectionType|CellType|ContainerType|button.eml|" + ) + ); + } + + private boolean isEveryFilterGroupEnabled() { + for (StringFilterGroup rule : pathFilterGroups) + if (!rule.isEnabled()) return false; + + return true; + } + + @Override + public boolean isFiltered(final String path, final String identifier, final byte[] _protobufBufferArray) { + if (isEveryFilterGroupEnabled()) + if (actionBarRule.contains(identifier).isFiltered()) return true; + + return super.isFiltered(path, identifier, _protobufBufferArray); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/litho/CommentsFilter.java b/app/src/main/java/app/revanced/integrations/patches/litho/CommentsFilter.java new file mode 100644 index 00000000..9b08c32e --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/litho/CommentsFilter.java @@ -0,0 +1,26 @@ +package app.revanced.integrations.patches.litho; + +import app.revanced.integrations.settings.SettingsEnum; + +final class CommentsFilter extends Filter { + + public CommentsFilter() { + var comments = new StringFilterGroup( + SettingsEnum.HIDE_COMMENTS_SECTION, + "video_metadata_carousel", + "_comments" + ); + + var previewComment = new StringFilterGroup( + SettingsEnum.HIDE_PREVIEW_COMMENT, + "|carousel_item", + "comments_entry_point_teaser", + "comments_entry_point_simplebox" + ); + + this.pathFilterGroups.addAll( + comments, + previewComment + ); + } +} diff --git a/app/src/main/java/app/revanced/integrations/patches/litho/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/patches/litho/LithoFilterPatch.java new file mode 100644 index 00000000..56c15b01 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/patches/litho/LithoFilterPatch.java @@ -0,0 +1,249 @@ +package app.revanced.integrations.patches.litho; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.function.Consumer; + +import app.revanced.integrations.settings.SettingsEnum; +import app.revanced.integrations.utils.LogHelper; +import app.revanced.integrations.utils.ReVancedUtils; + +abstract class FilterGroup { + final static class FilterGroupResult { + private final boolean filtered; + private final SettingsEnum setting; + + public FilterGroupResult(final SettingsEnum setting, final boolean filtered) { + this.setting = setting; + this.filtered = filtered; + } + + public SettingsEnum getSetting() { + return setting; + } + + public boolean isFiltered() { + return filtered; + } + } + + protected final SettingsEnum setting; + protected final T[] filters; + + /** + * Initialize a new filter group. + * + * @param setting The associated setting. + * @param filters The filters. + */ + @SafeVarargs + public FilterGroup(final SettingsEnum setting, final T... filters) { + this.setting = setting; + this.filters = filters; + } + + public boolean isEnabled() { + return setting.getBoolean(); + } + + public abstract FilterGroupResult contains(final T stack); +} + +class StringFilterGroup extends FilterGroup { + + /** + * {@link FilterGroup#FilterGroup(SettingsEnum, Object[])} + */ + public StringFilterGroup(final SettingsEnum setting, final String... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult contains(final String string) { + return new FilterGroupResult(setting, string != null && ReVancedUtils.containsAny(string, filters)); + } +} + +final class CustomFilterGroup extends StringFilterGroup { + + /** + * {@link FilterGroup#FilterGroup(SettingsEnum, Object[])} + */ + public CustomFilterGroup(final SettingsEnum setting, final SettingsEnum filter) { + super(setting, filter.getString().split(",")); + } +} + +final class ByteArrayFilterGroup extends FilterGroup { + // Modified implementation from https://stackoverflow.com/a/1507813 + private int indexOf(final byte[] data, final byte[] pattern) { + // Computes the failure function using a boot-strapping process, + // where the pattern is matched against itself. + + final int[] failure = new int[pattern.length]; + + int j = 0; + for (int i = 1; i < pattern.length; i++) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1]; + } + if (pattern[j] == pattern[i]) { + j++; + } + failure[i] = j; + } + + // Finds the first occurrence of the pattern in the byte array using + // KNP matching algorithm. + + j = 0; + if (data.length == 0) return -1; + + for (int i = 0; i < data.length; i++) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1]; + } + if (pattern[j] == data[i]) { + j++; + } + if (j == pattern.length) { + return i - pattern.length + 1; + } + } + return -1; + } + + /** + * {@link FilterGroup#FilterGroup(SettingsEnum, Object[])} + */ + public ByteArrayFilterGroup(final SettingsEnum setting, final byte[]... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult contains(final byte[] bytes) { + var matched = false; + for (byte[] filter : filters) { + if (indexOf(bytes, filter) == -1) continue; + + matched = true; + break; + } + + final var filtered = matched; + return new FilterGroupResult(setting, filtered); + } +} + +abstract class FilterGroupList> implements Iterable { + private final ArrayList filterGroups = new ArrayList<>(); + + @SafeVarargs + protected final void addAll(final T... filterGroups) { + this.filterGroups.addAll(Arrays.asList(filterGroups)); + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void forEach(@NonNull Consumer action) { + filterGroups.forEach(action); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @NonNull + @Override + public Spliterator spliterator() { + return filterGroups.spliterator(); + } + + protected boolean contains(final V stack) { + for (T filterGroup : this) { + if (!filterGroup.isEnabled()) continue; + + var result = filterGroup.contains(stack); + if (result.isFiltered()) { + return true; + } + } + + return false; + } +} + +final class StringFilterGroupList extends FilterGroupList { +} + +final class ByteArrayFilterGroupList extends FilterGroupList { +} + +abstract class Filter { + final protected StringFilterGroupList pathFilterGroups = new StringFilterGroupList(); + final protected StringFilterGroupList identifierFilterGroups = new StringFilterGroupList(); + final protected ByteArrayFilterGroupList protobufBufferFilterGroup = new ByteArrayFilterGroupList(); + + /** + * Check if the given path, identifier or protobuf buffer is filtered by any {@link FilterGroup}. + * @return True if filtered, false otherwise. + */ + boolean isFiltered(final String path, final String identifier, final byte[] protobufBufferArray) { + if (pathFilterGroups.contains(path)) { + LogHelper.printDebug(() -> String.format("Filtered path: %s", path)); + return true; + } + + if (identifierFilterGroups.contains(identifier)) { + LogHelper.printDebug(() -> String.format("Filtered identifier: %s", identifier)); + return true; + } + + if (protobufBufferFilterGroup.contains(protobufBufferArray)) { + LogHelper.printDebug(() -> "Filtered from protobuf-buffer"); + return true; + } + + return false; + } +} + +@SuppressWarnings("unused") +public final class LithoFilterPatch { + private static final Filter[] filters = new Filter[]{ + new AdsFilter(), + new ButtonsFilter(), + new CommentsFilter(), + }; + + @SuppressWarnings("unused") + public static boolean filter(final StringBuilder pathBuilder, final String identifier, final ByteBuffer protobufBuffer) { + var path = pathBuilder.toString(); + // It is assumed that protobufBuffer is empty as well in this case. + if (path.isEmpty()) return false; + + LogHelper.printDebug(() -> String.format( + "Searching (ID: %s, Buffer-size: %s): %s", + identifier, protobufBuffer.remaining(), path + )); + + var protobufBufferArray = protobufBuffer.array(); + + // check if any filter-group + for (var filter : filters) + if (filter.isFiltered(path, identifier, protobufBufferArray)) return true; + + return false; + } +} \ No newline at end of file