Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(YouTube - Hide layout components): Detect if a keyword filter hides all videos #657

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading