diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index cc53abf5d3..f0c17e8c26 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -54,6 +54,7 @@ private Utils() { * * @return The manifest 'Version' entry of the patches.jar used during patching. */ + @SuppressWarnings("SameReturnValue") public static String getPatchesReleaseVersion() { return ""; // Value is replaced during patching. } diff --git a/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java b/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java index c9f7536b52..65d29b97ea 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java +++ b/app/src/main/java/app/revanced/integrations/youtube/ThemeHelper.java @@ -39,6 +39,7 @@ public static void setActivityTheme(Activity activity) { /** * Injection point. */ + @SuppressWarnings("SameReturnValue") private static String darkThemeResourceName() { // Value is changed by Theme patch, if included. return "@color/yt_black3"; @@ -58,6 +59,7 @@ public static int getDarkThemeColor() { /** * Injection point. */ + @SuppressWarnings("SameReturnValue") private static String lightThemeResourceName() { // Value is changed by Theme patch, if included. return "@color/yt_white1"; diff --git a/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java b/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java index 1c927cd2d5..12b385a7fc 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java @@ -380,7 +380,7 @@ private boolean matches(@NonNull T textToSearch, int textToSearchLength, int sta throw new IllegalArgumentException("endIndex: " + endIndex + " is greater than texToSearchLength: " + textToSearchLength); } - if (patterns.size() == 0) { + if (patterns.isEmpty()) { return false; // No patterns were added. } for (int i = startIndex; i < endIndex; i++) { @@ -393,7 +393,7 @@ private boolean matches(@NonNull T textToSearch, int textToSearchLength, int sta * @return Estimated memory size (in kilobytes) of this instance. */ public int getEstimatedMemorySize() { - if (patterns.size() == 0) { + if (patterns.isEmpty()) { return 0; } // Assume the device has less than 32GB of ram (and can use pointer compression), diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/DisableSuggestedVideoEndScreenPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/DisableSuggestedVideoEndScreenPatch.java index 0b7897bac0..27c0a07d52 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/DisableSuggestedVideoEndScreenPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/DisableSuggestedVideoEndScreenPatch.java @@ -1,13 +1,8 @@ package app.revanced.integrations.youtube.patches; import android.annotation.SuppressLint; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; import android.widget.ImageView; -import androidx.annotation.NonNull; -import app.revanced.integrations.shared.Logger; import app.revanced.integrations.youtube.settings.Settings; /** @noinspection unused*/ diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/PlayerOverlaysHookPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/PlayerOverlaysHookPatch.java index 32732cdcc3..c51055e261 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/PlayerOverlaysHookPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/PlayerOverlaysHookPatch.java @@ -2,8 +2,6 @@ import android.view.ViewGroup; -import androidx.annotation.Nullable; - import app.revanced.integrations.youtube.shared.PlayerOverlays; @SuppressWarnings("unused") diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java index 8cf525f3c7..695af32693 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java @@ -278,6 +278,7 @@ public static long getVideoTime() { * * @see VideoState */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isAtEndOfVideo() { return videoTime >= videoLength && videoLength > 0; } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java index 98f1ceebc8..c3813178d9 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java @@ -1,7 +1,6 @@ package app.revanced.integrations.youtube.patches.components; import static app.revanced.integrations.shared.StringRef.str; -import static app.revanced.integrations.youtube.ByteTrieSearch.convertStringsToBytes; import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; import android.os.Build; @@ -10,13 +9,16 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.ByteTrieSearch; +import app.revanced.integrations.youtube.TrieSearch; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.NavigationBar; import app.revanced.integrations.youtube.shared.PlayerType; @@ -65,13 +67,18 @@ final class KeywordContentFilter extends Filter { // Video decoders. "OMX.ffmpeg.vp9.decoder", "OMX.Intel.sw_vd.vp9", - "OMX.sprd.av1.decoder", "OMX.MTK.VIDEO.DECODER.SW.VP9", + "OMX.google.vp9.decoder", + "OMX.google.av1.decoder", + "OMX.sprd.av1.decoder", "c2.android.av1.decoder", + "c2.android.av1-dav1d.decoder", + "c2.android.vp9.decoder", "c2.mtk.sw.vp9.decoder", // User analytics. "https://ad.doubleclick.net/ddm/activity/", "DEVICE_ADVERTISER_ID_FOR_CONVERSION_TRACKING", + "tag_for_child_directed_treatment", // Found in overflow menu such as 'Watch later'. // Litho components frequently found in the buffer that belong to the path filter items. "metadata.eml", "thumbnail.eml", @@ -97,6 +104,7 @@ final class KeywordContentFilter extends Filter { /** * Substrings that are never at the start of the path. */ + @SuppressWarnings("FieldCanBeLocal") private final StringFilterGroup containsFilter = new StringFilterGroup( null, "modern_type_shelf_header_content.eml", @@ -104,6 +112,38 @@ final class KeywordContentFilter extends Filter { "video_card.eml" // Shorts that appear in a horizontal shelf. ); + /** + * Threshold for {@link #filteredVideosPercentage} + * that indicates all or nearly all videos have been filtered. + * This should be close to 100% to reduce false positives. + */ + private static final float ALL_VIDEOS_FILTERED_THRESHOLD = 0.95f; + + private static final float ALL_VIDEOS_FILTERED_SAMPLE_SIZE = 50; + + private static final long ALL_VIDEOS_FILTERED_TIMEOUT_MILLISECONDS = 60 * 1000; // 60 seconds + + /** + * Rolling average of how many videos were filtered by a keyword. + * Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER} + * but a keyword is still hiding all videos. + * + * This check can still fail if some extra UI elements pass the keywords, + * such as the video chapter preview or any other elements. + * + * To test this, add a filter that appears in all videos (such as 'ovd='), + * and open the subscription feed. In practice this does not always identify problems + * in the home feed and search, because the home feed has a finite amount of content and + * search results have a lot of extra video junk that is not hidden and interferes with the detection. + */ + private volatile float filteredVideosPercentage; + + /** + * If filtering is temporarily turned off, the time to resume filtering. + * Field is zero if no timeout is in effect. + */ + private volatile long timeToResumeFiltering; + /** * The last value of {@link Settings#HIDE_KEYWORD_CONTENT_PHRASES} * parsed and loaded into {@link #bufferSearch}. @@ -113,39 +153,6 @@ final class KeywordContentFilter extends Filter { private volatile ByteTrieSearch bufferSearch; - private static boolean hideKeywordSettingIsActive() { - // Must check player type first, as search bar can be active behind the player. - if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { - // For now, consider the under video results the same as the home feed. - return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); - } - - // Must check second, as search can be from any tab. - if (NavigationBar.isSearchBarActive()) { - return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get(); - } - - // Avoid checking navigation button status if all other settings are off. - final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get(); - final boolean hideSubscriptions = Settings.HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS.get(); - if (!hideHome && !hideSubscriptions) { - return false; - } - - NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); - if (selectedNavButton == null) { - return hideHome; // Unknown tab, treat the same as home. - } - if (selectedNavButton == NavigationButton.HOME) { - return hideHome; - } - if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { - return hideSubscriptions; - } - // User is in the Library or Notifications tab. - return false; - } - /** * Change first letter of the first word to use title case. */ @@ -247,11 +254,26 @@ private synchronized void parseKeywords() { // Must be synchronized since Litho keywords.addAll(Arrays.asList(phraseVariations)); } - search.addPatterns(convertStringsToBytes(keywords.toArray(new String[0]))); + for (String keyword : keywords) { + // Use a callback to get the keyword that matched. + // TrieSearch could have this built in, but that's slightly more complicated since + // the strings are stored as a byte array and embedded in the search tree. + TrieSearch.TriePatternMatchedCallback callback = + (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + // noinspection unchecked + ((MutableReference) callbackParameter).value = keyword; + return true; + }; + byte[] stringBytes = keyword.getBytes(StandardCharsets.UTF_8); + search.addPattern(stringBytes, callback); + } + Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords); } bufferSearch = search; + timeToResumeFiltering = 0; + filteredVideosPercentage = 0; lastKeywordPhrasesParsed = rawKeywords; // Must set last. } @@ -260,6 +282,69 @@ public KeywordContentFilter() { addPathCallbacks(startsWithFilter, containsFilter); } + private boolean hideKeywordSettingIsActive() { + if (timeToResumeFiltering != 0) { + if (System.currentTimeMillis() < timeToResumeFiltering) { + return false; + } + + timeToResumeFiltering = 0; + filteredVideosPercentage = 0; + Logger.printDebug(() -> "Resuming keyword filtering"); + } + + // Must check player type first, as search bar can be active behind the player. + if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { + // For now, consider the under video results the same as the home feed. + return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); + } + + // Must check second, as search can be from any tab. + if (NavigationBar.isSearchBarActive()) { + return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get(); + } + + // Avoid checking navigation button status if all other settings are off. + final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get(); + final boolean hideSubscriptions = Settings.HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS.get(); + if (!hideHome && !hideSubscriptions) { + return false; + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; + } + // User is in the Library or Notifications tab. + return false; + } + + private void updateStats(boolean videoWasHidden, @Nullable String keyword) { + float updatedAverage = filteredVideosPercentage + * ((ALL_VIDEOS_FILTERED_SAMPLE_SIZE - 1) / ALL_VIDEOS_FILTERED_SAMPLE_SIZE); + if (videoWasHidden) { + updatedAverage += 1 / ALL_VIDEOS_FILTERED_SAMPLE_SIZE; + } + + if (updatedAverage <= ALL_VIDEOS_FILTERED_THRESHOLD) { + filteredVideosPercentage = updatedAverage; + return; + } + + // A keyword is hiding everything. + // Inform the user, and temporarily turn off filtering. + timeToResumeFiltering = System.currentTimeMillis() + ALL_VIDEOS_FILTERED_TIMEOUT_MILLISECONDS; + + Logger.printDebug(() -> "Temporarily turning off filtering due to excessively broad filter: " + keyword); + Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_broad", keyword)); + } + @Override boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { @@ -267,8 +352,6 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff return false; } - if (!hideKeywordSettingIsActive()) return false; - // Field is intentionally compared using reference equality. //noinspection StringEquality if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) { @@ -276,11 +359,22 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff parseKeywords(); } - if (!bufferSearch.matches(protobufBufferArray)) { - return false; + if (!hideKeywordSettingIsActive()) return false; + + MutableReference matchRef = new MutableReference<>(); + if (bufferSearch.matches(protobufBufferArray, matchRef)) { + updateStats(true, matchRef.value); + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } - return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + updateStats(false, null); + return false; } +} +/** + * Simple non-atomic wrapper since {@link AtomicReference#setPlain(Object)} is not available with Android 8.0. + */ +final class MutableReference { + T value; } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java index e1bb9d6780..a7c9972004 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java @@ -96,6 +96,7 @@ public boolean isEnabled() { * @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; } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java index 1f3bd6c012..58fd974633 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/ReVancedPreferenceFragment.java @@ -3,13 +3,11 @@ import android.os.Build; import android.preference.ListPreference; import android.preference.Preference; -import android.preference.PreferenceGroup; import androidx.annotation.RequiresApi; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.settings.preference.AbstractPreferenceFragment; -import app.revanced.integrations.youtube.patches.DownloadsPatch; import app.revanced.integrations.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; import app.revanced.integrations.youtube.settings.Settings;