revanced-integrations/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterP...

133 lines
5.3 KiB
Java

package app.revanced.integrations.youtube.patches.components;
import android.os.Build;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import app.revanced.integrations.youtube.patches.ReturnYouTubeDislikePatch;
import app.revanced.integrations.youtube.patches.VideoInformation;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.TrieSearch;
/**
* Searches for video id's in the proto buffer of Shorts dislike.
*
* Because multiple litho dislike spans are created in the background
* (and also anytime litho refreshes the components, which is somewhat arbitrary),
* that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()}
* unreliable to determine which video id a Shorts litho span belongs to.
*
* But the correct video id does appear in the protobuffer just before a Shorts litho span is created.
*
* Once a way to asynchronously update litho text is found, this strategy will no longer be needed.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public final class ReturnYouTubeDislikeFilterPatch extends Filter {
/**
* Last unique video id's loaded. Value is ignored and Map is treated as a Set.
* Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry().
*/
@GuardedBy("itself")
private static final Map<String, Boolean> lastVideoIds = new LinkedHashMap<>() {
/**
* Number of video id's to keep track of for searching thru the buffer.
* A minimum value of 3 should be sufficient, but check a few more just in case.
*/
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
}
};
/**
* Injection point.
*/
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
try {
if (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get()) {
return;
}
synchronized (lastVideoIds) {
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
Logger.printDebug(() -> "New Short video id: " + videoId);
}
}
} catch (Exception ex) {
Logger.printException(() -> "newPlayerResponseVideoId failure", ex);
}
}
private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();
public ReturnYouTubeDislikeFilterPatch() {
addPathCallbacks(
new StringFilterGroup(Settings.RYD_SHORTS, "|shorts_dislike_button.eml|")
);
// After the dislikes icon name is some binary data and then the video id for that specific short.
videoIdFilterGroup.addAll(
// Video was previously disliked before video was opened.
new ByteArrayFilterGroup(null, "ic_right_dislike_on_shadowed"),
// Video was not already disliked.
new ByteArrayFilterGroup(null, "ic_right_dislike_off_shadowed")
);
}
@Override
public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
if (result.isFiltered()) {
String matchedVideoId = findVideoId(protobufBufferArray);
// Matched video will be null if in incognito mode.
// Must pass a null id to correctly clear out the current video data.
// Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened,
// the new incognito Short will show the old prior data.
ReturnYouTubeDislikePatch.setLastLithoShortsVideoId(matchedVideoId);
}
return false;
}
@Nullable
private String findVideoId(byte[] protobufBufferArray) {
synchronized (lastVideoIds) {
for (String videoId : lastVideoIds.keySet()) {
if (byteArrayContainsString(protobufBufferArray, videoId)) {
return videoId;
}
}
return null;
}
}
/**
* This could use {@link TrieSearch}, but since the patterns are constantly changing
* the overhead of updating the Trie might negate the search performance gain.
*/
private static boolean byteArrayContainsString(@NonNull byte[] array, @NonNull String text) {
for (int i = 0, lastArrayStartIndex = array.length - text.length(); i <= lastArrayStartIndex; i++) {
boolean found = true;
for (int j = 0, textLength = text.length(); j < textLength; j++) {
if (array[i + j] != (byte) text.charAt(j)) {
found = false;
break;
}
}
if (found) {
return true;
}
}
return false;
}
}