Skip to content
This repository has been archived by the owner on Oct 26, 2024. It is now read-only.

Commit

Permalink
fix(YouTube - Hide layout components): Detect if a keyword filter hid…
Browse files Browse the repository at this point in the history
…es all videos (#657)
  • Loading branch information
LisoUseInAIKyrios authored Jun 23, 2024
1 parent 8fe73b2 commit 3a3ceec
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 52 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import android.view.ViewGroup;

import androidx.annotation.Nullable;

import app.revanced.integrations.youtube.shared.PlayerOverlays;

@SuppressWarnings("unused")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ public static long getVideoTime() {
*
* @see VideoState
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean isAtEndOfVideo() {
return videoTime >= videoLength && videoLength > 0;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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",
Expand All @@ -97,13 +104,46 @@ 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",
"shorts_lockup_cell.eml", // Part of 'shorts_shelf_carousel.eml'
"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}.
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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<byte[]> callback =
(textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
// noinspection unchecked
((MutableReference<String>) 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.
}

Expand All @@ -260,27 +282,99 @@ 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) {
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
return false;
}

if (!hideKeywordSettingIsActive()) return false;

// Field is intentionally compared using reference equality.
//noinspection StringEquality
if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) {
// User changed the keywords.
parseKeywords();
}

if (!bufferSearch.matches(protobufBufferArray)) {
return false;
if (!hideKeywordSettingIsActive()) return false;

MutableReference<String> 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> {
T value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

0 comments on commit 3a3ceec

Please sign in to comment.