mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-05 17:45:49 +01:00
fix(youtube/return-youtube-dislike): render dislikes when scrolling into the screen (#350)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
48050c1c50
commit
41c07f77f4
@ -1,48 +1,108 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* TODO: delete this empty class, and point the patch to {@link ReturnYouTubeDislike}
|
||||
*/
|
||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.integrations.settings.SettingsEnum;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoLoaded(String videoId) {
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
ReturnYouTubeDislike.newVideoLoaded(videoId);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "newVideoLoaded failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*
|
||||
* Called when a litho text component is created
|
||||
* Called when a litho text component is initially created,
|
||||
* and also when a Span is later reused again (such as scrolling off/on screen).
|
||||
*
|
||||
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added).
|
||||
*
|
||||
* @param textRef Cache reference to the like/dislike char sequence,
|
||||
* which may or may not be the same as the original span parameter.
|
||||
* If dislikes are added, the atomic reference must be set to the replacement span.
|
||||
* @param original Original span that was created or reused by Litho.
|
||||
* @return The original span (if nothing should change), or a replacement span that contains dislikes.
|
||||
*/
|
||||
public static void onComponentCreated(Object conversionContext, AtomicReference<Object> textRef) {
|
||||
ReturnYouTubeDislike.onComponentCreated(conversionContext, textRef);
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull AtomicReference<CharSequence> textRef,
|
||||
@NonNull CharSequence original) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return original;
|
||||
}
|
||||
SpannableString replacement = ReturnYouTubeDislike.getDislikeSpanForContext(conversionContext, original);
|
||||
if (replacement != null) {
|
||||
textRef.set(replacement);
|
||||
return replacement;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onComponentCreated AtomicReference failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*
|
||||
* Called when a Shorts dislike Spannable is created
|
||||
* Called when a Shorts dislike Spanned is created.
|
||||
*/
|
||||
public static Spanned onShortsComponentCreated(Spanned dislike) {
|
||||
return ReturnYouTubeDislike.onShortsComponentCreated(dislike);
|
||||
public static Spanned onShortsComponentCreated(Spanned original) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return original;
|
||||
}
|
||||
SpannableString replacement = ReturnYouTubeDislike.getDislikeSpanForShort(original);
|
||||
if (replacement != null) {
|
||||
return replacement;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onShortsComponentCreated failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* Injection point.
|
||||
*
|
||||
* Called when the like/dislike button is clicked
|
||||
* Called when the user likes or dislikes.
|
||||
*
|
||||
* @param vote -1 (dislike), 0 (none) or 1 (like)
|
||||
* @param vote int that matches {@link ReturnYouTubeDislike.Vote#value}
|
||||
*/
|
||||
public static void sendVote(int vote) {
|
||||
ReturnYouTubeDislike.sendVote(vote);
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
ReturnYouTubeDislike.sendVote(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
LogHelper.printException(() -> "Unknown vote type: " + vote);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "sendVote failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.RYDVoteData;
|
||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
@ -41,10 +40,13 @@ import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.ReVancedUtils;
|
||||
import app.revanced.integrations.utils.ThemeHelper;
|
||||
|
||||
/**
|
||||
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
|
||||
*/
|
||||
public class ReturnYouTubeDislike {
|
||||
/**
|
||||
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
|
||||
* <p>
|
||||
*
|
||||
* Must be less than 5 seconds, as per:
|
||||
* https://developer.android.com/topic/performance/vitals/anr
|
||||
*/
|
||||
@ -52,18 +54,17 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
/**
|
||||
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
|
||||
* Can be any almost any non-visible character
|
||||
* Can be any almost any non-visible character.
|
||||
*/
|
||||
private static final char MIDDLE_SEPARATOR_CHARACTER = '\u2009'; // 'narrow space' character
|
||||
|
||||
/**
|
||||
* Used to send votes, one by one, in the same order the user created them
|
||||
* Used to send votes, one by one, in the same order the user created them.
|
||||
*/
|
||||
private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Used to guard {@link #currentVideoId} and {@link #voteFetchFuture},
|
||||
* as multiple threads access this class.
|
||||
* Used to guard {@link #currentVideoId} and {@link #voteFetchFuture}.
|
||||
*/
|
||||
private static final Object videoIdLockObject = new Object();
|
||||
|
||||
@ -71,14 +72,13 @@ public class ReturnYouTubeDislike {
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static String currentVideoId;
|
||||
|
||||
|
||||
/**
|
||||
* If {@link #currentVideoId} and the RYD data is for the last shorts loaded
|
||||
* If {@link #currentVideoId} and the RYD data is for the last shorts loaded.
|
||||
*/
|
||||
private static volatile boolean lastVideoLoadedWasShort;
|
||||
|
||||
/**
|
||||
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes
|
||||
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("videoIdLockObject")
|
||||
@ -86,19 +86,31 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
/**
|
||||
* Original dislike span, before modifications.
|
||||
* Required for segmented layout
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static Spanned originalDislikeSpan;
|
||||
|
||||
/**
|
||||
* Replacement like/dislike span that includes formatted dislikes and is ready to display
|
||||
* Replacement like/dislike span that includes formatted dislikes.
|
||||
* Used to prevent recreating the same span multiple times.
|
||||
*/
|
||||
@Nullable
|
||||
@GuardedBy("videoIdLockObject")
|
||||
private static SpannableString replacementLikeDislikeSpan;
|
||||
|
||||
/**
|
||||
* For formatting dislikes as number.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
||||
private static CompactDecimalFormat dislikeCountFormatter;
|
||||
|
||||
/**
|
||||
* For formatting dislikes as percentage.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class")
|
||||
private static NumberFormat dislikePercentageFormatter;
|
||||
|
||||
public enum Vote {
|
||||
LIKE(1),
|
||||
DISLIKE(-1),
|
||||
@ -114,18 +126,6 @@ public class ReturnYouTubeDislike {
|
||||
private ReturnYouTubeDislike() {
|
||||
} // only static methods
|
||||
|
||||
/**
|
||||
* Used to format like/dislike count.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
||||
private static CompactDecimalFormat dislikeCountFormatter;
|
||||
|
||||
/**
|
||||
* Used to format like/dislike count.
|
||||
*/
|
||||
@GuardedBy("ReturnYouTubeDislike.class")
|
||||
private static NumberFormat dislikePercentageFormatter;
|
||||
|
||||
public static void onEnabledChange(boolean enabled) {
|
||||
if (!enabled) {
|
||||
// Must clear old values, to protect against using stale data
|
||||
@ -148,7 +148,7 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called if user changes settings for dislikes appearance.
|
||||
* Should be called after a user dislikes, or if the user changes settings for dislikes appearance.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
synchronized (videoIdLockObject) {
|
||||
@ -174,146 +174,113 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
public static void newVideoLoaded(@NonNull String videoId) {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
try {
|
||||
Objects.requireNonNull(videoId);
|
||||
|
||||
PlayerType currentPlayerType = PlayerType.getCurrent();
|
||||
if (currentPlayerType == PlayerType.INLINE_MINIMAL) {
|
||||
LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId);
|
||||
PlayerType currentPlayerType = PlayerType.getCurrent();
|
||||
if (currentPlayerType == PlayerType.INLINE_MINIMAL) {
|
||||
LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId);
|
||||
setCurrentVideoId(null);
|
||||
return;
|
||||
}
|
||||
synchronized (videoIdLockObject) {
|
||||
if (videoId.equals(currentVideoId)) {
|
||||
return; // already loaded
|
||||
}
|
||||
if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id
|
||||
LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId);
|
||||
setCurrentVideoId(null);
|
||||
return;
|
||||
}
|
||||
synchronized (videoIdLockObject) {
|
||||
if (videoId.equals(currentVideoId)) {
|
||||
return; // already loaded
|
||||
}
|
||||
if (!ReVancedUtils.isNetworkConnected()) { // must do network check after verifying it's a new video id
|
||||
LogHelper.printDebug(() -> "Network not connected, ignoring video: " + videoId);
|
||||
setCurrentVideoId(null);
|
||||
return;
|
||||
}
|
||||
LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType);
|
||||
setCurrentVideoId(videoId);
|
||||
LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType);
|
||||
setCurrentVideoId(videoId);
|
||||
|
||||
// If a Short is opened while a regular video is on screen, this will incorrectly set this as false.
|
||||
// But this check is needed to fix unusual situations of opening/closing the app
|
||||
// while both a regular video and a short are on screen.
|
||||
lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden();
|
||||
// If a Short is opened while a regular video is on screen, this will incorrectly set this as false.
|
||||
// But this check is needed to fix unusual situations of opening/closing the app
|
||||
// while both a regular video and a short are on screen.
|
||||
lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden();
|
||||
|
||||
// no need to wrap the call in a try/catch,
|
||||
// as any exceptions are propagated out in the later Future#Get call
|
||||
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to load new video: " + videoId, ex);
|
||||
// No need to wrap the call in a try/catch,
|
||||
// as any exceptions are propagated out in the later Future#Get call.
|
||||
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a litho text component is created.
|
||||
*
|
||||
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added)
|
||||
*
|
||||
* @param textRef atomic reference should always be non null, but the spanned reference inside can be null.
|
||||
* @return NULL if the span does not need changing or if RYD is not available.
|
||||
*/
|
||||
public static void onComponentCreated(@NonNull Object conversionContext, @NonNull AtomicReference<Object> textRef) {
|
||||
try {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
|
||||
if (PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
final boolean isSegmentedButton;
|
||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||
isSegmentedButton = true;
|
||||
} else if (conversionContextString.contains("|dislike_button.eml|")) {
|
||||
isSegmentedButton = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastVideoLoadedWasShort) {
|
||||
// user:
|
||||
// 1, opened a video
|
||||
// 2. opened a short (without closing the regular video)
|
||||
// 3. closed the short
|
||||
// 4. regular video is now present, but the videoId and RYD data is still for the short
|
||||
LogHelper.printDebug(() -> "Ignoring onComponentCreated(), as data loaded is is for prior short");
|
||||
return;
|
||||
}
|
||||
|
||||
Spanned replacement = waitForFetchAndUpdateReplacementSpan((Spanned) textRef.get(), isSegmentedButton);
|
||||
if (replacement != null) {
|
||||
textRef.set(replacement);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onComponentCreated failure", ex);
|
||||
@Nullable
|
||||
public static SpannableString getDislikeSpanForContext(@NonNull Object conversionContext, @NonNull CharSequence original) {
|
||||
if (PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
return null;
|
||||
}
|
||||
String conversionContextString = conversionContext.toString();
|
||||
final boolean isSegmentedButton;
|
||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||
isSegmentedButton = true;
|
||||
} else if (conversionContextString.contains("|dislike_button.eml|")) {
|
||||
isSegmentedButton = false;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lastVideoLoadedWasShort) {
|
||||
// user:
|
||||
// 1, opened a video
|
||||
// 2. opened a short (without closing the regular video)
|
||||
// 3. closed the short
|
||||
// 4. regular video is now present, but the videoId and RYD data is still for the short
|
||||
LogHelper.printDebug(() -> "Ignoring getDislikeSpanForContext(), as data loaded is for prior short");
|
||||
return null;
|
||||
}
|
||||
|
||||
return waitForFetchAndUpdateReplacementSpan((Spannable) original, isSegmentedButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a Shorts dislike Spannable is created.
|
||||
*/
|
||||
public static Spanned onShortsComponentCreated(Spanned original) {
|
||||
try {
|
||||
if (SettingsEnum.RYD_ENABLED.getBoolean()) {
|
||||
lastVideoLoadedWasShort = true; // it's now certain the video and data are a short
|
||||
Spanned replacement = waitForFetchAndUpdateReplacementSpan(original, false);
|
||||
if (replacement != null) {
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "onShortsComponentCreated failure", ex);
|
||||
}
|
||||
return original;
|
||||
public static SpannableString getDislikeSpanForShort(@NonNull Spanned original) {
|
||||
lastVideoLoadedWasShort = true; // it's now certain the video and data are a short
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false);
|
||||
}
|
||||
|
||||
// alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick
|
||||
// Alternatively, this could check if the span contains one of the custom created spans, but this is simple and quick.
|
||||
private static boolean isPreviouslyCreatedSegmentedSpan(@NonNull Spanned span) {
|
||||
return span.toString().indexOf(MIDDLE_SEPARATOR_CHARACTER) != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NULL if the span does not need changing or if RYD is not available
|
||||
* @return NULL if the span does not need changing or if RYD is not available.
|
||||
*/
|
||||
@Nullable
|
||||
private static SpannableString waitForFetchAndUpdateReplacementSpan(@Nullable Spanned oldSpannable, boolean isSegmentedButton) {
|
||||
if (oldSpannable == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislikes (injection code was called with null Span)");
|
||||
return null;
|
||||
}
|
||||
private static SpannableString waitForFetchAndUpdateReplacementSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton) {
|
||||
try {
|
||||
synchronized (videoIdLockObject) {
|
||||
if (oldSpannable.equals(replacementLikeDislikeSpan)) {
|
||||
LogHelper.printDebug(() -> "Ignoring span that already contains dislikes");
|
||||
return null;
|
||||
}
|
||||
if (replacementLikeDislikeSpan != null) {
|
||||
LogHelper.printDebug(() -> "Using previously created dislike span");
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
if (isSegmentedButton) {
|
||||
if (isPreviouslyCreatedSegmentedSpan(oldSpannable)) {
|
||||
// need to recreate using original, as oldSpannable has prior outdated dislike values
|
||||
oldSpannable = originalDislikeSpan;
|
||||
if (oldSpannable == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislikes - original span is null"); // should never happen
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
originalDislikeSpan = oldSpannable; // most up to date original
|
||||
String oldSpannableString = oldSpannable.toString();
|
||||
if (replacementLikeDislikeSpan.toString().equals(oldSpannableString)) {
|
||||
LogHelper.printDebug(() -> "Ignoring previously created dislikes span");
|
||||
return null;
|
||||
}
|
||||
if (originalDislikeSpan.toString().equals(oldSpannableString)) {
|
||||
LogHelper.printDebug(() -> "Replacing span with previously created dislike span");
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
}
|
||||
if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(oldSpannable)) {
|
||||
// need to recreate using original, as oldSpannable has prior outdated dislike values
|
||||
oldSpannable = originalDislikeSpan;
|
||||
if (oldSpannable == null) {
|
||||
LogHelper.printDebug(() -> "Cannot add dislikes - original span is null"); // should never happen
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
originalDislikeSpan = oldSpannable; // most up to date original
|
||||
}
|
||||
}
|
||||
|
||||
// Must block the current thread until fetching is done
|
||||
// There's no known way to edit the text after creation yet
|
||||
// Must block the current thread until fetching is done.
|
||||
// There's no known way to edit the text after creation yet.
|
||||
Future<RYDVoteData> fetchFuture = getVoteFetchFuture();
|
||||
if (fetchFuture == null) {
|
||||
LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)");
|
||||
@ -340,37 +307,16 @@ public class ReturnYouTubeDislike {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the like/dislike button is clicked.
|
||||
*
|
||||
* @param vote int that matches {@link Vote#value}
|
||||
*/
|
||||
public static void sendVote(int vote) {
|
||||
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return;
|
||||
|
||||
try {
|
||||
for (Vote v : Vote.values()) {
|
||||
if (v.value == vote) {
|
||||
sendVote(v);
|
||||
return;
|
||||
}
|
||||
}
|
||||
LogHelper.printException(() -> "Unknown vote type: " + vote);
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "sendVote failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendVote(@NonNull Vote vote) {
|
||||
public static void sendVote(@NonNull Vote vote) {
|
||||
ReVancedUtils.verifyOnMainThread();
|
||||
Objects.requireNonNull(vote);
|
||||
try {
|
||||
// Must make a local copy of videoId, since it may change between now and when the vote thread runs
|
||||
// Must make a local copy of videoId, since it may change between now and when the vote thread runs.
|
||||
String videoIdToVoteFor = getCurrentVideoId();
|
||||
if (videoIdToVoteFor == null || lastVideoLoadedWasShort != PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
// User enabled RYD after starting playback of a video.
|
||||
// Or shorts was loaded with regular video present, then shorts was closed,
|
||||
// and then user voted on the now visible original video
|
||||
// and then user voted on the now visible original video.
|
||||
// Cannot send a vote, because the loaded videoId is for the wrong video.
|
||||
ReVancedUtils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted"));
|
||||
return;
|
||||
@ -387,15 +333,15 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
});
|
||||
|
||||
clearCache(); // ui values need updating
|
||||
clearCache(); // UI needs updating
|
||||
|
||||
// update the downloaded vote data
|
||||
// Update the downloaded vote data.
|
||||
Future<RYDVoteData> future = getVoteFetchFuture();
|
||||
if (future == null) {
|
||||
LogHelper.printException(() -> "Cannot update UI dislike count - vote fetch is null");
|
||||
return;
|
||||
}
|
||||
// the future should always be completed before user can like/dislike, but use a timeout just in case
|
||||
// The future should always be completed before user can like/dislike, but use a timeout just in case.
|
||||
RYDVoteData voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
|
||||
if (voteData == null) {
|
||||
// RYD fetch failed
|
||||
@ -409,10 +355,10 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
/**
|
||||
* Must call off main thread, as this will make a network call if user is not yet registered
|
||||
* Must call off main thread, as this will make a network call if user is not yet registered.
|
||||
*
|
||||
* @return ReturnYouTubeDislike user ID. If user registration has never happened
|
||||
* and the network call fails, this returns NULL
|
||||
* and the network call fails, this returns NULL.
|
||||
*/
|
||||
@Nullable
|
||||
private static String getUserId() {
|
||||
@ -423,7 +369,7 @@ public class ReturnYouTubeDislike {
|
||||
return userId;
|
||||
}
|
||||
|
||||
userId = ReturnYouTubeDislikeApi.registerAsNewUser(); // blocks until network call is completed
|
||||
userId = ReturnYouTubeDislikeApi.registerAsNewUser();
|
||||
if (userId != null) {
|
||||
SettingsEnum.RYD_USER_ID.saveValue(userId);
|
||||
}
|
||||
@ -431,25 +377,25 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isSegmentedButton if UI is using the segmented single UI component for both like and dislike
|
||||
* @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike.
|
||||
*/
|
||||
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) {
|
||||
if (!isSegmentedButton) {
|
||||
// simple replacement of 'dislike' with a number/percentage
|
||||
// Simple replacement of 'dislike' with a number/percentage.
|
||||
return newSpannableWithDislikes(oldSpannable, voteData);
|
||||
}
|
||||
|
||||
// note: some locales use right to left layout (arabic, hebrew, etc),
|
||||
// and care must be taken to retain the existing RTL encoding character on the likes string
|
||||
// otherwise text will incorrectly show as left to right
|
||||
// if making changes to this code, change device settings to a RTL language and verify layout is correct
|
||||
// Note: Some locales use right to left layout (arabic, hebrew, etc),
|
||||
// and care must be taken to retain the existing RTL encoding character on the likes string,
|
||||
// otherwise text will incorrectly show as left to right.
|
||||
// If making changes to this code, change device settings to a RTL language and verify layout is correct.
|
||||
String oldLikesString = oldSpannable.toString();
|
||||
|
||||
// YouTube creators can hide the like count on a video,
|
||||
// and the like count appears as a device language specific string that says 'Like'
|
||||
// check if the string contains any numbers
|
||||
// and the like count appears as a device language specific string that says 'Like'.
|
||||
// Check if the string contains any numbers.
|
||||
if (!stringContainsNumber(oldLikesString)) {
|
||||
// likes are hidden.
|
||||
// Likes are hidden.
|
||||
// RYD does not provide usable data for these types of videos,
|
||||
// and the API returns bogus data (zero likes and zero dislikes)
|
||||
// discussion about this: https://github.com/Anarios/return-youtube-dislike/discussions/530
|
||||
@ -457,7 +403,7 @@ public class ReturnYouTubeDislike {
|
||||
// example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
|
||||
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
|
||||
//
|
||||
// Change the "Likes" string to show that likes and dislikes are hidden
|
||||
// Change the "Likes" string to show that likes and dislikes are hidden.
|
||||
String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
|
||||
return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
|
||||
}
|
||||
@ -467,7 +413,7 @@ public class ReturnYouTubeDislike {
|
||||
final int separatorColor = ThemeHelper.isDarkTheme()
|
||||
? 0x29AAAAAA // transparent dark gray
|
||||
: 0xFFD9D9D9; // light gray
|
||||
DisplayMetrics dp = ReVancedUtils.getContext().getResources().getDisplayMetrics();
|
||||
DisplayMetrics dp = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics();
|
||||
|
||||
if (!compactLayout) {
|
||||
// left separator
|
||||
@ -511,9 +457,9 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly handles any unicode numbers (such as Arabic numbers)
|
||||
* Correctly handles any unicode numbers (such as Arabic numbers).
|
||||
*
|
||||
* @return if the string contains at least 1 number
|
||||
* @return if the string contains at least 1 number.
|
||||
*/
|
||||
private static boolean stringContainsNumber(@NonNull String text) {
|
||||
for (int index = 0, length = text.length(); index < length; index++) {
|
||||
@ -545,10 +491,10 @@ public class ReturnYouTubeDislike {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikeCountFormatter == null) {
|
||||
// Note: Java number formatters will use the locale specific number characters.
|
||||
// such as Arabic which formats "1.2" into "١٫٢"
|
||||
// such as Arabic which formats "1.234" into "۱,۲۳٤"
|
||||
// But YouTube disregards locale specific number characters
|
||||
// and instead shows english number characters everywhere.
|
||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||
Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale;
|
||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||
}
|
||||
@ -563,7 +509,7 @@ public class ReturnYouTubeDislike {
|
||||
private static String formatDislikePercentage(float dislikePercentage) {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikePercentageFormatter == null) {
|
||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||
Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale;
|
||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
@SuppressWarnings("NonAtomicOperationOnVolatileField") // do not want to pay performance cost of full synchronization for debug fields that are only estimates anyways
|
||||
private static void updateStatistics(long timeNetworkCallStarted, long timeNetworkCallEnded, boolean connectionError, boolean rateLimitHit) {
|
||||
if (connectionError && rateLimitHit) {
|
||||
throw new IllegalArgumentException("both connection error and rate limit parameter were true");
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
final long responseTimeOfFetchCall = timeNetworkCallEnded - timeNetworkCallStarted;
|
||||
fetchCallResponseTimeTotal += responseTimeOfFetchCall;
|
||||
@ -320,7 +320,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
return confirmRegistration(userId, solution);
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to register new user: " + userId
|
||||
+ " response code was: " + responseCode);
|
||||
+ " response code was: " + responseCode); // failed attempt, and ok to log userId
|
||||
connection.disconnect();
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to register user", ex);
|
||||
@ -337,7 +337,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (checkIfRateLimitInEffect("confirmRegistration")) {
|
||||
return null;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Trying to confirm registration for user: " + userId + " with solution: " + solution);
|
||||
LogHelper.printDebug(() -> "Trying to confirm registration with solution: " + solution);
|
||||
|
||||
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId);
|
||||
applyCommonPostRequestSettings(connection);
|
||||
@ -355,7 +355,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
|
||||
String result = Requester.parseJson(connection);
|
||||
if (result.equalsIgnoreCase("true")) {
|
||||
LogHelper.printDebug(() -> "Registration confirmation successful for user: " + userId);
|
||||
LogHelper.printDebug(() -> "Registration confirmation successful");
|
||||
return userId;
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to confirm registration for user: " + userId
|
||||
@ -382,8 +382,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (checkIfRateLimitInEffect("sendVote")) {
|
||||
return false;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Trying to vote for video: "
|
||||
+ videoId + " with vote: " + vote + " user: " + userId);
|
||||
LogHelper.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote);
|
||||
|
||||
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE);
|
||||
applyCommonPostRequestSettings(connection);
|
||||
@ -408,11 +407,10 @@ public class ReturnYouTubeDislikeApi {
|
||||
return confirmVote(videoId, userId, solution);
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to send vote for video: " + videoId
|
||||
+ " userId: " + userId + " vote: " + vote + " response code was: " + responseCode);
|
||||
+ " vote: " + vote + " response code was: " + responseCode);
|
||||
connection.disconnect(); // something went wrong, might as well disconnect
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to send vote for video: " + videoId
|
||||
+ " user: " + userId + " vote: " + vote, ex);
|
||||
LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -427,8 +425,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
if (checkIfRateLimitInEffect("confirmVote")) {
|
||||
return false;
|
||||
}
|
||||
LogHelper.printDebug(() -> "Trying to confirm vote for video: "
|
||||
+ videoId + " user: " + userId + " solution: " + solution);
|
||||
LogHelper.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution);
|
||||
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
|
||||
applyCommonPostRequestSettings(connection);
|
||||
|
||||
@ -450,15 +447,15 @@ public class ReturnYouTubeDislikeApi {
|
||||
return true;
|
||||
}
|
||||
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
|
||||
+ " user: " + userId + " solution: " + solution + " response string was: " + result);
|
||||
+ " solution: " + solution + " response string was: " + result);
|
||||
} else {
|
||||
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
|
||||
+ " user: " + userId + " solution: " + solution + " response code was: " + responseCode);
|
||||
+ " solution: " + solution + " response code was: " + responseCode);
|
||||
}
|
||||
connection.disconnect(); // something went wrong, might as well disconnect
|
||||
} catch (Exception ex) {
|
||||
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
|
||||
+ " user: " + userId + " solution: " + solution, ex);
|
||||
+ " solution: " + solution, ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user