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:
LisoUseInAIKyrios 2023-04-19 10:36:10 +04:00 committed by GitHub
parent 48050c1c50
commit 41c07f77f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 212 additions and 209 deletions

View File

@ -1,48 +1,108 @@
package app.revanced.integrations.patches; package app.revanced.integrations.patches;
import static app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike.Vote;
import android.text.SpannableString;
import android.text.Spanned; import android.text.Spanned;
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
import androidx.annotation.NonNull;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
/** import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
* TODO: delete this empty class, and point the patch to {@link ReturnYouTubeDislike} import app.revanced.integrations.settings.SettingsEnum;
*/ import app.revanced.integrations.utils.LogHelper;
public class ReturnYouTubeDislikePatch { public class ReturnYouTubeDislikePatch {
/** /**
* Injection point * Injection point.
*/ */
public static void newVideoLoaded(String videoId) { 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) { @NonNull
ReturnYouTubeDislike.onComponentCreated(conversionContext, textRef); 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) { public static Spanned onShortsComponentCreated(Spanned original) {
return ReturnYouTubeDislike.onShortsComponentCreated(dislike); 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) { 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);
}
} }
} }

View File

@ -31,7 +31,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import app.revanced.integrations.returnyoutubedislike.requests.RYDVoteData; import app.revanced.integrations.returnyoutubedislike.requests.RYDVoteData;
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; 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.ReVancedUtils;
import app.revanced.integrations.utils.ThemeHelper; import app.revanced.integrations.utils.ThemeHelper;
/**
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
*/
public class ReturnYouTubeDislike { public class ReturnYouTubeDislike {
/** /**
* Maximum amount of time to block the UI from updates while waiting for network call to complete. * 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: * Must be less than 5 seconds, as per:
* https://developer.android.com/topic/performance/vitals/anr * 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. * 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 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(); private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor();
/** /**
* Used to guard {@link #currentVideoId} and {@link #voteFetchFuture}, * Used to guard {@link #currentVideoId} and {@link #voteFetchFuture}.
* as multiple threads access this class.
*/ */
private static final Object videoIdLockObject = new Object(); private static final Object videoIdLockObject = new Object();
@ -71,14 +72,13 @@ public class ReturnYouTubeDislike {
@GuardedBy("videoIdLockObject") @GuardedBy("videoIdLockObject")
private static String currentVideoId; 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; 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 @Nullable
@GuardedBy("videoIdLockObject") @GuardedBy("videoIdLockObject")
@ -86,19 +86,31 @@ public class ReturnYouTubeDislike {
/** /**
* Original dislike span, before modifications. * Original dislike span, before modifications.
* Required for segmented layout
*/ */
@Nullable @Nullable
@GuardedBy("videoIdLockObject") @GuardedBy("videoIdLockObject")
private static Spanned originalDislikeSpan; 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 @Nullable
@GuardedBy("videoIdLockObject") @GuardedBy("videoIdLockObject")
private static SpannableString replacementLikeDislikeSpan; 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 { public enum Vote {
LIKE(1), LIKE(1),
DISLIKE(-1), DISLIKE(-1),
@ -114,18 +126,6 @@ public class ReturnYouTubeDislike {
private ReturnYouTubeDislike() { private ReturnYouTubeDislike() {
} // only static methods } // 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) { public static void onEnabledChange(boolean enabled) {
if (!enabled) { if (!enabled) {
// Must clear old values, to protect against using stale data // 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() { public static void clearCache() {
synchronized (videoIdLockObject) { synchronized (videoIdLockObject) {
@ -174,146 +174,113 @@ public class ReturnYouTubeDislike {
} }
public static void newVideoLoaded(@NonNull String videoId) { public static void newVideoLoaded(@NonNull String videoId) {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; Objects.requireNonNull(videoId);
try { PlayerType currentPlayerType = PlayerType.getCurrent();
Objects.requireNonNull(videoId); if (currentPlayerType == PlayerType.INLINE_MINIMAL) {
LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId);
PlayerType currentPlayerType = PlayerType.getCurrent(); setCurrentVideoId(null);
if (currentPlayerType == PlayerType.INLINE_MINIMAL) { return;
LogHelper.printDebug(() -> "Ignoring inline playback of video: " + videoId); }
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); setCurrentVideoId(null);
return; return;
} }
synchronized (videoIdLockObject) { LogHelper.printDebug(() -> "New video loaded: " + videoId + " playerType: " + currentPlayerType);
if (videoId.equals(currentVideoId)) { setCurrentVideoId(videoId);
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);
// If a Short is opened while a regular video is on screen, this will incorrectly set this as false. // 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 // 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. // while both a regular video and a short are on screen.
lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden(); lastVideoLoadedWasShort = PlayerType.getCurrent().isNoneOrHidden();
// no need to wrap the call in a try/catch, // No need to wrap the call in a try/catch,
// as any exceptions are propagated out in the later Future#Get call // as any exceptions are propagated out in the later Future#Get call.
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
}
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to load new video: " + videoId, ex);
} }
} }
/** /**
* Called when a litho text component is created. * @return NULL if the span does not need changing or if RYD is not available.
*
* 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.
*/ */
public static void onComponentCreated(@NonNull Object conversionContext, @NonNull AtomicReference<Object> textRef) { @Nullable
try { public static SpannableString getDislikeSpanForContext(@NonNull Object conversionContext, @NonNull CharSequence original) {
if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; if (PlayerType.getCurrent().isNoneOrHidden()) {
return null;
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);
} }
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. * Called when a Shorts dislike Spannable is created.
*/ */
public static Spanned onShortsComponentCreated(Spanned original) { public static SpannableString getDislikeSpanForShort(@NonNull Spanned original) {
try { lastVideoLoadedWasShort = true; // it's now certain the video and data are a short
if (SettingsEnum.RYD_ENABLED.getBoolean()) { return waitForFetchAndUpdateReplacementSpan(original, false);
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;
} }
// 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) { private static boolean isPreviouslyCreatedSegmentedSpan(@NonNull Spanned span) {
return span.toString().indexOf(MIDDLE_SEPARATOR_CHARACTER) != -1; 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 @Nullable
private static SpannableString waitForFetchAndUpdateReplacementSpan(@Nullable Spanned oldSpannable, boolean isSegmentedButton) { private static SpannableString waitForFetchAndUpdateReplacementSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton) {
if (oldSpannable == null) {
LogHelper.printDebug(() -> "Cannot add dislikes (injection code was called with null Span)");
return null;
}
try { try {
synchronized (videoIdLockObject) { synchronized (videoIdLockObject) {
if (oldSpannable.equals(replacementLikeDislikeSpan)) {
LogHelper.printDebug(() -> "Ignoring span that already contains dislikes");
return null;
}
if (replacementLikeDislikeSpan != null) { if (replacementLikeDislikeSpan != null) {
LogHelper.printDebug(() -> "Using previously created dislike span"); String oldSpannableString = oldSpannable.toString();
return replacementLikeDislikeSpan; if (replacementLikeDislikeSpan.toString().equals(oldSpannableString)) {
} LogHelper.printDebug(() -> "Ignoring previously created dislikes span");
if (isSegmentedButton) { return null;
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
} }
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 // Must block the current thread until fetching is done.
// There's no known way to edit the text after creation yet // There's no known way to edit the text after creation yet.
Future<RYDVoteData> fetchFuture = getVoteFetchFuture(); Future<RYDVoteData> fetchFuture = getVoteFetchFuture();
if (fetchFuture == null) { if (fetchFuture == null) {
LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)"); LogHelper.printDebug(() -> "fetch future not available (user enabled RYD while video was playing?)");
@ -340,37 +307,16 @@ public class ReturnYouTubeDislike {
return null; return null;
} }
/** public static void sendVote(@NonNull Vote vote) {
* 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) {
ReVancedUtils.verifyOnMainThread(); ReVancedUtils.verifyOnMainThread();
Objects.requireNonNull(vote); Objects.requireNonNull(vote);
try { 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(); String videoIdToVoteFor = getCurrentVideoId();
if (videoIdToVoteFor == null || lastVideoLoadedWasShort != PlayerType.getCurrent().isNoneOrHidden()) { if (videoIdToVoteFor == null || lastVideoLoadedWasShort != PlayerType.getCurrent().isNoneOrHidden()) {
// User enabled RYD after starting playback of a video. // User enabled RYD after starting playback of a video.
// Or shorts was loaded with regular video present, then shorts was closed, // 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. // 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")); ReVancedUtils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted"));
return; 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(); Future<RYDVoteData> future = getVoteFetchFuture();
if (future == null) { if (future == null) {
LogHelper.printException(() -> "Cannot update UI dislike count - vote fetch is null"); LogHelper.printException(() -> "Cannot update UI dislike count - vote fetch is null");
return; 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); RYDVoteData voteData = future.get(MAX_MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
if (voteData == null) { if (voteData == null) {
// RYD fetch failed // 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 * @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 @Nullable
private static String getUserId() { private static String getUserId() {
@ -423,7 +369,7 @@ public class ReturnYouTubeDislike {
return userId; return userId;
} }
userId = ReturnYouTubeDislikeApi.registerAsNewUser(); // blocks until network call is completed userId = ReturnYouTubeDislikeApi.registerAsNewUser();
if (userId != null) { if (userId != null) {
SettingsEnum.RYD_USER_ID.saveValue(userId); 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) { private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, boolean isSegmentedButton, @NonNull RYDVoteData voteData) {
if (!isSegmentedButton) { if (!isSegmentedButton) {
// simple replacement of 'dislike' with a number/percentage // Simple replacement of 'dislike' with a number/percentage.
return newSpannableWithDislikes(oldSpannable, voteData); return newSpannableWithDislikes(oldSpannable, voteData);
} }
// note: some locales use right to left layout (arabic, hebrew, etc), // 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 // 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 // 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 // If making changes to this code, change device settings to a RTL language and verify layout is correct.
String oldLikesString = oldSpannable.toString(); String oldLikesString = oldSpannable.toString();
// YouTube creators can hide the like count on a video, // YouTube creators can hide the like count on a video,
// and the like count appears as a device language specific string that says 'Like' // and the like count appears as a device language specific string that says 'Like'.
// check if the string contains any numbers // Check if the string contains any numbers.
if (!stringContainsNumber(oldLikesString)) { if (!stringContainsNumber(oldLikesString)) {
// likes are hidden. // Likes are hidden.
// RYD does not provide usable data for these types of videos, // RYD does not provide usable data for these types of videos,
// and the API returns bogus data (zero likes and zero dislikes) // and the API returns bogus data (zero likes and zero dislikes)
// discussion about this: https://github.com/Anarios/return-youtube-dislike/discussions/530 // 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 // example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=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"); String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString); return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
} }
@ -467,7 +413,7 @@ public class ReturnYouTubeDislike {
final int separatorColor = ThemeHelper.isDarkTheme() final int separatorColor = ThemeHelper.isDarkTheme()
? 0x29AAAAAA // transparent dark gray ? 0x29AAAAAA // transparent dark gray
: 0xFFD9D9D9; // light gray : 0xFFD9D9D9; // light gray
DisplayMetrics dp = ReVancedUtils.getContext().getResources().getDisplayMetrics(); DisplayMetrics dp = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getDisplayMetrics();
if (!compactLayout) { if (!compactLayout) {
// left separator // 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) { private static boolean stringContainsNumber(@NonNull String text) {
for (int index = 0, length = text.length(); index < length; index++) { 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 synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
if (dislikeCountFormatter == null) { if (dislikeCountFormatter == null) {
// Note: Java number formatters will use the locale specific number characters. // 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 // But YouTube disregards locale specific number characters
// and instead shows english number characters everywhere. // 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); LogHelper.printDebug(() -> "Locale: " + locale);
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT); dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
} }
@ -563,7 +509,7 @@ public class ReturnYouTubeDislike {
private static String formatDislikePercentage(float dislikePercentage) { private static String formatDislikePercentage(float dislikePercentage) {
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
if (dislikePercentageFormatter == null) { if (dislikePercentageFormatter == null) {
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale; Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale;
LogHelper.printDebug(() -> "Locale: " + locale); LogHelper.printDebug(() -> "Locale: " + locale);
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale); dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
} }

View File

@ -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 @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) { private static void updateStatistics(long timeNetworkCallStarted, long timeNetworkCallEnded, boolean connectionError, boolean rateLimitHit) {
if (connectionError && rateLimitHit) { if (connectionError && rateLimitHit) {
throw new IllegalArgumentException("both connection error and rate limit parameter were true"); throw new IllegalArgumentException();
} }
final long responseTimeOfFetchCall = timeNetworkCallEnded - timeNetworkCallStarted; final long responseTimeOfFetchCall = timeNetworkCallEnded - timeNetworkCallStarted;
fetchCallResponseTimeTotal += responseTimeOfFetchCall; fetchCallResponseTimeTotal += responseTimeOfFetchCall;
@ -320,7 +320,7 @@ public class ReturnYouTubeDislikeApi {
return confirmRegistration(userId, solution); return confirmRegistration(userId, solution);
} }
LogHelper.printException(() -> "Failed to register new user: " + userId 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(); connection.disconnect();
} catch (Exception ex) { } catch (Exception ex) {
LogHelper.printException(() -> "Failed to register user", ex); LogHelper.printException(() -> "Failed to register user", ex);
@ -337,7 +337,7 @@ public class ReturnYouTubeDislikeApi {
if (checkIfRateLimitInEffect("confirmRegistration")) { if (checkIfRateLimitInEffect("confirmRegistration")) {
return null; 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); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId);
applyCommonPostRequestSettings(connection); applyCommonPostRequestSettings(connection);
@ -355,7 +355,7 @@ public class ReturnYouTubeDislikeApi {
if (responseCode == HTTP_STATUS_CODE_SUCCESS) { if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
String result = Requester.parseJson(connection); String result = Requester.parseJson(connection);
if (result.equalsIgnoreCase("true")) { if (result.equalsIgnoreCase("true")) {
LogHelper.printDebug(() -> "Registration confirmation successful for user: " + userId); LogHelper.printDebug(() -> "Registration confirmation successful");
return userId; return userId;
} }
LogHelper.printException(() -> "Failed to confirm registration for user: " + userId LogHelper.printException(() -> "Failed to confirm registration for user: " + userId
@ -382,8 +382,7 @@ public class ReturnYouTubeDislikeApi {
if (checkIfRateLimitInEffect("sendVote")) { if (checkIfRateLimitInEffect("sendVote")) {
return false; return false;
} }
LogHelper.printDebug(() -> "Trying to vote for video: " LogHelper.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote);
+ videoId + " with vote: " + vote + " user: " + userId);
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE);
applyCommonPostRequestSettings(connection); applyCommonPostRequestSettings(connection);
@ -408,11 +407,10 @@ public class ReturnYouTubeDislikeApi {
return confirmVote(videoId, userId, solution); return confirmVote(videoId, userId, solution);
} }
LogHelper.printException(() -> "Failed to send vote for video: " + videoId 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 connection.disconnect(); // something went wrong, might as well disconnect
} catch (Exception ex) { } catch (Exception ex) {
LogHelper.printException(() -> "Failed to send vote for video: " + videoId LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex);
+ " user: " + userId + " vote: " + vote, ex);
} }
return false; return false;
} }
@ -427,8 +425,7 @@ public class ReturnYouTubeDislikeApi {
if (checkIfRateLimitInEffect("confirmVote")) { if (checkIfRateLimitInEffect("confirmVote")) {
return false; return false;
} }
LogHelper.printDebug(() -> "Trying to confirm vote for video: " LogHelper.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution);
+ videoId + " user: " + userId + " solution: " + solution);
HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
applyCommonPostRequestSettings(connection); applyCommonPostRequestSettings(connection);
@ -450,15 +447,15 @@ public class ReturnYouTubeDislikeApi {
return true; return true;
} }
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
+ " user: " + userId + " solution: " + solution + " response string was: " + result); + " solution: " + solution + " response string was: " + result);
} else { } else {
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId 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 connection.disconnect(); // something went wrong, might as well disconnect
} catch (Exception ex) { } catch (Exception ex) {
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
+ " user: " + userId + " solution: " + solution, ex); + " solution: " + solution, ex);
} }
return false; return false;
} }