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

Commit

Permalink
fix(YouTube - ReturnYouTubeDislike): Fix dislikes not showing on Shor…
Browse files Browse the repository at this point in the history
…ts (#495)
  • Loading branch information
LisoUseInAIKyrios authored Oct 12, 2023
1 parent dceaf7a commit 9b2add7
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
package app.revanced.integrations.patches;

import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;

import android.graphics.Rect;
import android.os.Build;
import android.text.*;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextWatcher;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;

import app.revanced.integrations.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.integrations.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.shared.PlayerType;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.ReVancedUtils;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;

/**
* Handles all interaction of UI patch components.
*
Expand Down Expand Up @@ -108,7 +113,7 @@ public static void onRYDStatusChange(boolean rydEnabled) {

/**
* Old UI dislikes can be set multiple times by YouTube.
* To prevent it from reverting changes made here, this listener overrides any future changes YouTube makes.
* To prevent reverting changes made here, this listener overrides any future changes YouTube makes.
*/
private static final TextWatcher oldUiTextWatcher = new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
Expand Down Expand Up @@ -141,7 +146,7 @@ private static void updateOldUIDislikesTextView() {
/**
* Injection point. Called on main thread.
*
* Used when spoofing the older app versions of {@link SpoofAppVersionPatch}.
* Used when spoofing to 16.x and 17.x versions.
*/
public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable TextView textView) {
try {
Expand Down Expand Up @@ -230,9 +235,9 @@ public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
}
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
if (videoData == null) {
// Should not happen, as user cannot turn on RYD while leaving a short on screen.
// If this does happen, then the litho video id filter did not detect the video id.
LogHelper.printDebug(() -> "Error: Litho video data is null, but it should not be");
// The Shorts litho video id filter did not detect the video id.
// This is normal if in incognito mode, but otherwise is not normal.
LogHelper.printDebug(() -> "Cannot modify Shorts litho span, data is null");
return original;
}
// Use the correct dislikes data after voting.
Expand Down Expand Up @@ -425,14 +430,18 @@ public static void newVideoLoaded(@NonNull String videoId) {
* Called both on and off main thread.
*
* @param isShortsLithoVideoId If the video id is from {@link ReturnYouTubeDislikeFilterPatch}.
* if true, then the video id can be null indicating the filter did
* not find any video id.
*/
public static void newVideoLoaded(@NonNull String videoId, boolean isShortsLithoVideoId) {
public static void newVideoLoaded(@Nullable String videoId, boolean isShortsLithoVideoId) {
try {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;

PlayerType currentPlayerType = PlayerType.getCurrent();
final boolean isNoneOrHidden = currentPlayerType.isNoneOrHidden();
if (isNoneOrHidden && !SettingsEnum.RYD_SHORTS.getBoolean()) {
final boolean isNoneHiddenOrSlidingMinimized = currentPlayerType.isNoneHiddenOrSlidingMinimized();
if (isNoneHiddenOrSlidingMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) {
// Must clear here, otherwise the wrong data can be used for a minimized regular video.
currentVideoData = null;
return;
}

Expand All @@ -441,33 +450,51 @@ public static void newVideoLoaded(@NonNull String videoId, boolean isShortsLitho
if (videoIdIsSame(lastLithoShortsVideoData, videoId)) {
return;
}
if (videoId == null) {
// Litho filter did not detect the video id. App is in incognito mode,
// or the proto buffer structure was changed and the video id is no longer present.
// Must clear both currently playing and last litho data otherwise the
// next regular video may use the wrong data.
LogHelper.printDebug(() -> "Litho filter did not find any video ids");
currentVideoData = null;
lastLithoShortsVideoData = null;
lithoShortsShouldUseCurrentData = false;
return;
}
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
videoData.setVideoIdIsShort(true);
lastLithoShortsVideoData = videoData;
lithoShortsShouldUseCurrentData = false;
} else {
Objects.requireNonNull(videoId);
// All other playback (including non-litho Shorts).
if (videoIdIsSame(currentVideoData, videoId)) {
return;
}
currentVideoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
// Pre-emptively set the data to short status.
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
if (isNoneHiddenOrSlidingMinimized) {
currentVideoData.setVideoIdIsShort(true);
}
}

LogHelper.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType
+ " isShortsLithoHook: " + isShortsLithoVideoId);

if (isNoneOrHidden) {
// Current video id hook can be called out of order with the non litho Shorts text view hook.
// Must manually update again here.
// Current video id hook can be called out of order with the non litho Shorts text view hook.
// Must manually update again here.
if (!isShortsLithoVideoId && isNoneHiddenOrSlidingMinimized) {
updateOnScreenShortsTextViews(true);
}
} catch (Exception ex) {
LogHelper.printException(() -> "newVideoLoaded failure", ex);
}
}

private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, String videoId) {
return fetch != null && fetch.getVideoId().equals(videoId);
private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) {
return (fetch == null && videoId == null)
|| (fetch != null && fetch.getVideoId().equals(videoId));
}

/**
Expand All @@ -482,22 +509,27 @@ public static void sendVote(int vote) {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
return;
}
if (!SettingsEnum.RYD_SHORTS.getBoolean() && PlayerType.getCurrent().isNoneHiddenOrMinimized()) {
final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized();
if (isNoneHiddenOrMinimized && !SettingsEnum.RYD_SHORTS.getBoolean()) {
return;
}
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
LogHelper.printDebug(() -> "Cannot send vote, as current video data is null");
return; // User enabled RYD while a regular video was minimized.
}

for (Vote v : Vote.values()) {
if (v.value == vote) {
videoData.sendVote(v);

if (lastLithoShortsVideoData != null) {
lithoShortsShouldUseCurrentData = true;
if (isNoneHiddenOrMinimized) {
if (lastLithoShortsVideoData != null) {
lithoShortsShouldUseCurrentData = true;
}
updateOldUIDislikesTextView();
}
updateOldUIDislikesTextView();

return;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,72 @@

import android.os.Build;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;

import app.revanced.integrations.patches.ReturnYouTubeDislikePatch;
import app.revanced.integrations.patches.VideoInformation;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
import app.revanced.integrations.utils.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(Map.Entry eldest) {
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
}
};

/**
* Injection point.
*/
public static void newPlayerResponseVideoId(String videoId) {
try {
if (!SettingsEnum.RYD_SHORTS.getBoolean()) {
return;
}
synchronized (lastVideoIds) {
if (lastVideoIds.put(videoId, Boolean.TRUE) == null) {
LogHelper.printDebug(() -> "New video id: " + videoId);
}
}
} catch (Exception ex) {
LogHelper.printException(() -> "newPlayerResponseVideoId failure", ex);
}
}

private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();

public ReturnYouTubeDislikeFilterPatch() {
Expand All @@ -33,44 +88,46 @@ public boolean isFiltered(@Nullable String identifier, String path, byte[] proto
FilterGroupList matchedList, FilterGroup matchedGroup, int matchedIndex) {
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
if (result.isFiltered()) {
// The video length must be hard coded to 11, as there is additional ASCII text that
// appears immediately after the id if the dislike button is already selected.
final int videoIdLength = 11;
final int subStringSearchStartIndex = result.getMatchedIndex() + result.getMatchedLength();
String videoId = findSubString(protobufBufferArray, subStringSearchStartIndex, videoIdLength);
if (videoId != null) {
ReturnYouTubeDislikePatch.newVideoLoaded(videoId, true);
}
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.newVideoLoaded(matchedVideoId, true);
}

return false;
}

/**
* Find an exact length ASCII substring starting from a given index.
*
* Similar to the String finding code in {@link LithoFilterPatch},
* but refactoring it to also handle this use case became messy and overly complicated.
*/
@Nullable
private static String findSubString(byte[] buffer, int bufferStartIndex, int subStringLength) {
// Valid ASCII values (ignore control characters).
final int minimumAscii = 32; // 32 = space character
final int maximumAscii = 126; // 127 = delete character

final int bufferLength = buffer.length;
int start = bufferStartIndex;
int end = bufferStartIndex;
do {
final int value = buffer[end];
if (value < minimumAscii || value > maximumAscii) {
start = end + 1;
} else if (end - start == subStringLength) {
return new String(buffer, start, subStringLength, StandardCharsets.US_ASCII);
private String findVideoId(byte[] protobufBufferArray) {
synchronized (lastVideoIds) {
for (String videoId : lastVideoIds.keySet()) {
if (byteArrayContainsString(protobufBufferArray, videoId)) {
return videoId;
}
}
end++;
} while (end < bufferLength);
return null;
}
}

return null;
/**
* This could use {@link TrieSearch}, but since the video ids 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;
}
}
}
Loading

0 comments on commit 9b2add7

Please sign in to comment.