diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java index a9c02b8a5b..b0411267c5 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java @@ -36,13 +36,13 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter { @GuardedBy("itself") private static final Map lastVideoIds = new LinkedHashMap<>() { /** - * Number of video id's to keep track of for searching through the buffer. + * 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) { + protected boolean removeEldestEntry(Map.Entry eldest) { return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK; } }; @@ -64,7 +64,6 @@ public ReturnYouTubeDislikeFilterPatch() { /** * Injection point. */ - @SuppressWarnings("unused") public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) { try { if (!isShortAndOpeningOrPlaying || !SettingsEnum.RYD_SHORTS.getBoolean()) { @@ -111,7 +110,7 @@ boolean isFiltered(String path, @Nullable String identifier, String allValue, by 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, + // 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); } @@ -130,4 +129,4 @@ private String findVideoId(byte[] protobufBufferArray) { return null; } } -} +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/ReturnYouTubeDislikePatch.java index fc747f6396..70ae6bbd7e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/ReturnYouTubeDislikePatch.java @@ -6,7 +6,6 @@ import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; -import android.os.Build; import android.text.Editable; import android.text.Spannable; import android.text.SpannableString; @@ -26,7 +25,6 @@ import app.revanced.integrations.youtube.patches.components.ReturnYouTubeDislikeFilterPatch; import app.revanced.integrations.youtube.patches.video.VideoInformation; import app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike; -import app.revanced.integrations.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.youtube.settings.SettingsEnum; import app.revanced.integrations.youtube.shared.PlayerType; import app.revanced.integrations.youtube.utils.LogHelper; @@ -43,7 +41,7 @@ *

* A (yet to be implemented) solution that fixes this problem. Any one of: * - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously. - * - Find a way to force Litho to rebuild its component tree, + * - Find a way to force Litho to rebuild it's component tree, * and use that hook to force the shorts dislikes to update after the fetch is completed. * - Hook into the dislikes button image view, and replace the dislikes thumb down image with a * generated image of the number of dislikes, then update the image asynchronously. This Could @@ -56,6 +54,11 @@ public class ReturnYouTubeDislikePatch { public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER = isSpoofingToLessThan("18.34.00"); + /** + * Injection point. + * Whether to use incognito mode. + */ + public static volatile boolean isIncognito; /** * RYD data for the current video on screen. */ @@ -78,16 +81,17 @@ public class ReturnYouTubeDislikePatch { private static volatile boolean lithoShortsShouldUseCurrentData; /** - * Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row. + * Last video id prefetched. Field is prevent prefetching the same video id multiple times in a row. */ @Nullable private static volatile String lastPrefetchedVideoId; public static void onRYDStatusChange(boolean rydEnabled) { - ReturnYouTubeDislikeApi.resetRateLimits(); - // Must remove all values to protect against using stale data - // if the user enables RYD while a video is on screen. - clearData(); + if (!rydEnabled) { + // Must remove all values to protect against using stale data + // if the user enables RYD while a video is on screen. + clearData(); + } } private static void clearData() { @@ -162,7 +166,7 @@ private static void updateOldUIDislikesTextView() { /** * Injection point. Called on main thread. - * + *

* Used when spoofing to 16.x and 17.x versions. */ public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable TextView textView) { @@ -198,7 +202,7 @@ public static void setOldUILayoutDislikes(int buttonViewResourceId, @Nullable Te /** * Injection point. - * + *

* For Litho segmented buttons and Litho Shorts player. */ @NonNull @@ -208,13 +212,15 @@ public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, } /** + * Injection point. + *

* 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 original Original char sequence was created or reused by Litho. + * @param original Original char sequence was created or reused by Litho. * @param isRollingNumber If the span is for a Rolling Number. * @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes. */ @@ -241,6 +247,10 @@ private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, } replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original, true, isRollingNumber); + + // When spoofing between 17.09.xx and 17.30.xx the UI is the old layout + // but uses litho and the dislikes is "|dislike_button.eml|". + // But spoofing to that range gives a broken UI layout so no point checking for that. } else if (!isRollingNumber && conversionContextString.contains("|shorts_dislike_button.eml|")) { // Litho Shorts player. if (!SettingsEnum.RYD_SHORTS.getBoolean()) { @@ -279,6 +289,63 @@ private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, return original; } + // + // Litho Shorts player in the incognito mode / live stream. + // + + /** + * Injection point. + *

+ * This method is used in the following situations. + *

+ * 1. When the dislike counts are fetched in the Incognito mode. + * 2. When the dislike counts are fetched in the live stream. + * + * @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 CharSequence onCharSequenceLoaded(@NonNull Object conversionContext, + @NonNull CharSequence original) { + try { + String conversionContextString = conversionContext.toString(); + if (!SettingsEnum.RYD_ENABLED.getBoolean()) { + return original; + } + if (!SettingsEnum.RYD_SHORTS.getBoolean()) { + // Must clear the current video here, otherwise if the user opens a regular video + // then opens a litho short (while keeping the regular video on screen), then closes the short, + // the original video may show the incorrect dislike value. + clearData(); + return original; + } + + final boolean fetchDislikeIncognito = + conversionContextString.contains("|shorts_dislike_button.eml|") + && isIncognito; + final boolean fetchDislikeLiveStream = + conversionContextString.contains("immersive_live_video_action_bar.eml") + && conversionContextString.contains("|dislike_button.eml|"); + + if (fetchDislikeIncognito) { + LogHelper.printDebug(() -> "setShortsDislikes in Incognito mode"); + } else if (fetchDislikeLiveStream) { + LogHelper.printDebug(() -> "setShortsDislikes in LiveStream"); + } else { + return original; + } + + ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(VideoInformation.getVideoId()); + videoData.setVideoIdIsShort(true); + lastLithoShortsVideoData = videoData; + lithoShortsShouldUseCurrentData = false; + + return videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN); + } catch (Exception ex) { + LogHelper.printException(() -> "onCharSequenceLoaded failure", ex); + } + return original; + } + // // Rolling Number // @@ -297,10 +364,9 @@ public static String onRollingNumberLoaded(@NonNull Object conversionContext, @NonNull String original) { try { CharSequence replacement = onLithoTextLoaded(conversionContext, original, true); - String replacementString = replacement.toString(); - if (!replacementString.equals(original)) { + if (!replacement.toString().equals(original)) { rollingNumberSpan = replacement; - return replacementString; + return replacement.toString(); } // Else, the text was not a likes count but instead the view count or something else. } catch (Exception ex) { LogHelper.printException(() -> "onRollingNumberLoaded failure", ex); @@ -334,7 +400,6 @@ public static float onRollingNumberMeasured(String text, float measuredTextWidth } catch (Exception ex) { LogHelper.printException(() -> "onRollingNumberMeasured failure", ex); } - return measuredTextWidth; } @@ -352,12 +417,11 @@ private static void addRollingNumberPatchChanges(TextView view) { } else { view.setCompoundDrawables(separator, null, null, null); } - - // Disliking can cause the span to grow in size, which is ok and is laid out correctly, - // but if the user then removes their dislike the layout will not adjust to the new shorter width. + // Liking/disliking can cause the span to grow in size, + // which is ok and is laid out correctly, + // but if the user then undoes their action the layout will not remove the extra padding. // Use a center alignment to take up any extra space. view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); - // Single line mode does not clip words if the span is larger than the view bounds. // The styled span applied to the view should always have the same bounds, // but use this feature just in case the measurements are somehow off by a few pixels. @@ -426,7 +490,7 @@ public static CharSequence updateRollingNumber(TextView view, CharSequence origi } // - // Non-litho Shorts player. + // Non litho Shorts player. // /** @@ -436,16 +500,14 @@ public static CharSequence updateRollingNumber(TextView view, CharSequence origi /** * Dislikes TextViews used by Shorts. - * + *

* Multiple TextViews are loaded at once (for the prior and next videos to swipe to). * Keep track of all of them, and later pick out the correct one based on their on screen position. */ private static final List> shortsTextViewRefs = new ArrayList<>(); private static void clearRemovedShortsTextViews() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // YouTube requires Android N or greater - shortsTextViewRefs.removeIf(ref -> ref.get() == null); - } + shortsTextViewRefs.removeIf(ref -> ref.get() == null); } /** @@ -559,7 +621,7 @@ private static boolean isShortTextViewOnScreen(@NonNull View view) { // - // Video ID and voting hooks (all players). + // Video Id and voting hooks (all players). // private static volatile boolean lastPlayerResponseWasShort; @@ -572,6 +634,10 @@ public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpe if (!SettingsEnum.RYD_ENABLED.getBoolean()) { return; } + if (ReVancedUtils.isNetworkNotConnected()) { + LogHelper.printDebug(() -> "Network not connected, ignoring video"); + return; + } if (videoId.equals(lastPrefetchedVideoId)) { return; } @@ -613,10 +679,17 @@ public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpe */ public static void newVideoLoaded(@NonNull String videoId) { try { - if (!SettingsEnum.RYD_ENABLED.getBoolean()) return; + if (!SettingsEnum.RYD_ENABLED.getBoolean()) { + return; + } Objects.requireNonNull(videoId); - PlayerType currentPlayerType = PlayerType.getCurrent(); + if (ReVancedUtils.isNetworkNotConnected()) { + LogHelper.printDebug(() -> "Network not connected, ignoring video"); + return; + } + + final PlayerType currentPlayerType = PlayerType.getCurrent(); 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. @@ -674,10 +747,10 @@ private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Null /** * Injection point. - * + *

* Called when the user likes or dislikes. * - * @param vote int that matches {@link Vote#value} + * @param vote int that matches {@link ReturnYouTubeDislike.Vote#value} */ public static void sendVote(int vote) { try { diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java index 89e729b681..ee9374b322 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -10,7 +10,6 @@ import android.graphics.drawable.shapes.OvalShape; import android.graphics.drawable.shapes.RectShape; import android.icu.text.CompactDecimalFormat; -import android.os.Build; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -43,7 +42,6 @@ import app.revanced.integrations.youtube.shared.PlayerType; import app.revanced.integrations.youtube.utils.LogHelper; import app.revanced.integrations.youtube.utils.ReVancedUtils; - import app.revanced.integrations.youtube.utils.ThemeHelper; import app.revanced.integrations.youtube.patches.misc.SpoofAppVersionPatch; @@ -152,7 +150,7 @@ public enum Vote { private final Future future; /** - * Time this instance and the fetch future was created. + * Time this instance and the future was created. */ private final long timeFetched; @@ -186,12 +184,12 @@ public enum Vote { /** * Color of the left and middle separator, based on the color of the right separator. - * It's unknown where YT gets the color from, and the values here are approximated by hand. - * Ideally, this would be the actual color YT uses at runtime. + * It's unknown where YT gets the color from, and the colors here are approximated by hand. + * Ideally, the color here would be the actual color YT uses at runtime. * * Older versions before the 'Me' library tab use a slightly different color. * If spoofing was previously used and is now turned off, - * or an old version was recently upgraded then the old colors are sometimes still used. + * or an old version was recently upgraded then the old colors are sometimes used. */ private static int getSeparatorColor() { if (IS_SPOOFING_TO_OLD_SEPARATOR_COLOR) { @@ -200,8 +198,8 @@ private static int getSeparatorColor() { : 0xFFD9D9D9; // light gray } return ThemeHelper.getDayNightTheme() - ? 0x33FFFFFF - : 0xFFD9D9D9; + ? 0x33FFFFFF // transparent dark gray + : 0xFFD9D9D9; // light gray } public static ShapeDrawable getLeftSeparatorDrawable() { @@ -252,7 +250,7 @@ private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, : "\u200E"; // u200E = left to right character final Spannable leftSeparatorSpan; if (isRollingNumber) { - leftSeparatorSpan = new SpannableString(leftSeparatorString); + leftSeparatorSpan = new SpannableString(leftSeparatorString); } else { leftSeparatorString += " "; leftSeparatorSpan = new SpannableString(leftSeparatorString); @@ -352,23 +350,18 @@ private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned } private static String formatDislikeCount(long dislikeCount) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - 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.234" into "۱,۲۳٤" - // But YouTube disregards locale specific number characters - // and instead shows english number characters everywhere. - Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale; - LogHelper.printDebug(() -> "Locale: " + locale); - dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT); - } - return dislikeCountFormatter.format(dislikeCount); + 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.234" into "۱,۲۳٤" + // But YouTube disregards locale specific number characters + // and instead shows english number characters everywhere. + Locale locale = Objects.requireNonNull(ReVancedUtils.getContext()).getResources().getConfiguration().locale; + LogHelper.printDebug(() -> "Locale: " + locale); + dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT); } + return dislikeCountFormatter.format(dislikeCount); } - - // Will never be reached, as the oldest supported YouTube app requires Android N or greater. - return String.valueOf(dislikeCount); } private static String formatDislikePercentage(float dislikePercentage) { @@ -392,15 +385,13 @@ public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) Objects.requireNonNull(videoId); synchronized (fetchCache) { // Remove any expired entries. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - final long now = System.currentTimeMillis(); - fetchCache.values().removeIf(value -> { - final boolean expired = value.isExpired(now); - if (expired) - LogHelper.printDebug(() -> "Removing expired fetch: " + value.videoId); - return expired; - }); - } + final long now = System.currentTimeMillis(); + fetchCache.values().removeIf(value -> { + final boolean expired = value.isExpired(now); + if (expired) + LogHelper.printDebug(() -> "Removing expired fetch: " + value.videoId); + return expired; + }); ReturnYouTubeDislike fetch = fetchCache.get(videoId); if (fetch == null) { @@ -412,7 +403,7 @@ public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) } /** - * Should be called if the user changes dislikes appearance settings. + * Should be called if the user changes settings for dislikes appearance. */ public static void clearAllUICaches() { synchronized (fetchCache) { @@ -592,7 +583,7 @@ public void sendVote(@NonNull Vote vote) { /** * Sets the current user vote value, and does not send the vote to the RYD API. - * + *

* Only used to set value if thumbs up/down is already selected on video load. */ public void setUserVote(@NonNull Vote vote) { @@ -654,8 +645,8 @@ class VerticallyCenteredImageSpan extends ImageSpan { /** * @param useOriginalWidth Use the original layout width of the text this span is applied to, - * and not the bounds of the Drawable. Drawable is always displayed using it's own bounds, - * and this setting only affects the layout width of the entire span. + * and not the bounds of the Drawable. Drawable is always displayed using it's own bounds, + * and this setting only affects the layout width of the entire span. */ public VerticallyCenteredImageSpan(Drawable drawable, boolean useOriginalWidth) { super(drawable); diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index 7d490a98ce..cd77276c55 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -42,7 +42,7 @@ public class ReturnYouTubeDislikeApi { /** * Default connection and response timeout for voting and registration. - * + *

* Voting and user registration runs in the background and has has no urgency * so this can be a larger value. */ @@ -58,114 +58,14 @@ public class ReturnYouTubeDislikeApi { */ private static final int HTTP_STATUS_CODE_RATE_LIMIT = 429; - /** - * How long to wait until API calls are resumed, if the API requested a back off. - * No clear guideline of how long to wait until resuming. - */ - private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 10 * 60 * 1000; // 10 Minutes. - - /** - * How long to wait until API calls are resumed, if any connection error occurs. - */ - private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes. - /** * If non zero, then the system time of when API calls can resume. */ - private static volatile long timeToResumeAPICalls; - - /** - * If the last API getVotes call failed for any reason (including server requested rate limit). - * Used to prevent showing repeat connection toasts when the API is down. - */ - private static volatile boolean lastApiCallFailed; - - /** - * Number of times {@link #HTTP_STATUS_CODE_RATE_LIMIT} was requested by RYD api. - * Does not include network calls attempted while rate limit is in effect, - * and does not include rate limit imposed if a fetch fails. - */ - private static volatile int numberOfRateLimitRequestsEncountered; - - /** - * Number of network calls made in {@link #fetchVotes(String)} - */ - private static volatile int fetchCallCount; - - /** - * Number of times {@link #fetchVotes(String)} failed due to timeout or any other error. - * This does not include when rate limit requests are encountered. - */ - private static volatile int fetchCallNumberOfFailures; - - /** - * Total time spent waiting for {@link #fetchVotes(String)} network call to complete. - * Value does does not persist on app shut down. - */ - private static volatile long fetchCallResponseTimeTotal; - - /** - * Round trip network time for the most recent call to {@link #fetchVotes(String)} - */ - private static volatile long fetchCallResponseTimeLast; - private static volatile long fetchCallResponseTimeMin; - private static volatile long fetchCallResponseTimeMax; - - public static final int FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT = -1; - - /** - * If rate limit was hit, this returns {@link #FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT} - */ - public static long getFetchCallResponseTimeLast() { - return fetchCallResponseTimeLast; - } - public static long getFetchCallResponseTimeMin() { - return fetchCallResponseTimeMin; - } - public static long getFetchCallResponseTimeMax() { - return fetchCallResponseTimeMax; - } - public static long getFetchCallResponseTimeAverage() { - return fetchCallCount == 0 ? 0 : (fetchCallResponseTimeTotal / fetchCallCount); - } - public static int getFetchCallCount() { - return fetchCallCount; - } - public static int getFetchCallNumberOfFailures() { - return fetchCallNumberOfFailures; - } - public static int getNumberOfRateLimitRequestsEncountered() { - return numberOfRateLimitRequestsEncountered; - } + private static volatile long timeToResumeAPICalls; // must be volatile, since different threads read/write to this private ReturnYouTubeDislikeApi() { } // utility class - /** - * Simulates a slow response by doing meaningless calculations. - * Used to debug the app UI and verify UI timeout logic works - */ - private static void randomlyWaitIfLocallyDebugging() { - final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI - if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) { - final long amountOfTimeToWaste = (long) (Math.random() - * (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS)); - ReVancedUtils.doNothingForDuration(amountOfTimeToWaste); - } - } - - /** - * Clears any backoff rate limits in effect. - * Should be called if RYD is turned on/off. - */ - public static void resetRateLimits() { - if (lastApiCallFailed || timeToResumeAPICalls != 0) { - LogHelper.printDebug(() -> "Reset rate limit"); - } - lastApiCallFailed = false; - timeToResumeAPICalls = 0; - } - /** * @return True, if api rate limit is in effect. */ @@ -186,61 +86,25 @@ private static boolean checkIfRateLimitInEffect(String apiEndPointName) { * @return True, if a client rate limit was requested */ private static boolean checkIfRateLimitWasHit(int httpResponseCode) { - final boolean DEBUG_RATE_LIMIT = false; // set to true, to verify rate limit works - if (DEBUG_RATE_LIMIT) { - final double RANDOM_RATE_LIMIT_PERCENTAGE = 0.2; // 20% chance of a triggering a rate limit - if (Math.random() < RANDOM_RATE_LIMIT_PERCENTAGE) { - LogHelper.printDebug(() -> "Artificially triggering rate limit for debug purposes"); - httpResponseCode = HTTP_STATUS_CODE_RATE_LIMIT; - } - } return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT; } - @SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are estimates. - private static void updateRateLimitAndStats(long timeNetworkCallStarted, boolean connectionError, boolean rateLimitHit) { - if (connectionError && rateLimitHit) { - throw new IllegalArgumentException(); - } - final long responseTimeOfFetchCall = System.currentTimeMillis() - timeNetworkCallStarted; - fetchCallResponseTimeTotal += responseTimeOfFetchCall; - fetchCallResponseTimeMin = (fetchCallResponseTimeMin == 0) ? responseTimeOfFetchCall : Math.min(responseTimeOfFetchCall, fetchCallResponseTimeMin); - fetchCallResponseTimeMax = Math.max(responseTimeOfFetchCall, fetchCallResponseTimeMax); - fetchCallCount++; - if (connectionError) { - timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS; - fetchCallResponseTimeLast = responseTimeOfFetchCall; - fetchCallNumberOfFailures++; - lastApiCallFailed = true; - } else if (rateLimitHit) { - LogHelper.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " - + BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds"); - timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS; - numberOfRateLimitRequestsEncountered++; - fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT; - if (!lastApiCallFailed && SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean()) { + private static void updateRateLimitAndStats(boolean connectionError, boolean rateLimitHit) { + if (rateLimitHit) { + if (connectionError) + throw new IllegalArgumentException(); + else ReVancedUtils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); - } - lastApiCallFailed = true; - } else { - fetchCallResponseTimeLast = responseTimeOfFetchCall; - lastApiCallFailed = false; } } - private static void handleConnectionError(@NonNull String toastMessage, - @Nullable Exception ex, - boolean showLongToast) { - if (!lastApiCallFailed && SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean()) { - if (showLongToast) { - ReVancedUtils.showToastLong(toastMessage); - } else { - ReVancedUtils.showToastShort(toastMessage); - } + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + if (SettingsEnum.RYD_TOAST_ON_CONNECTION_ERROR.getBoolean()) { + ReVancedUtils.showToastShort(toastMessage); + } + if (ex != null) { + LogHelper.printInfo(() -> toastMessage, ex); } - lastApiCallFailed = true; - - LogHelper.printInfo(() -> toastMessage, ex); } /** @@ -255,7 +119,6 @@ public static RYDVoteData fetchVotes(String videoId) { return null; } LogHelper.printDebug(() -> "Fetching votes for: " + videoId); - final long timeNetworkCallStarted = System.currentTimeMillis(); try { HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId); @@ -269,21 +132,19 @@ public static RYDVoteData fetchVotes(String videoId) { connection.setConnectTimeout(API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server connection.setReadTimeout(API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS); // timeout for server response - randomlyWaitIfLocallyDebugging(); - final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { connection.disconnect(); // rate limit hit, should disconnect - updateRateLimitAndStats(timeNetworkCallStarted, false, true); + updateRateLimitAndStats(false, true); return null; } if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - // do not disconnect, the same server connection will likely be used again soon + // Do not disconnect, the same server connection will likely be used again soon JSONObject json = Requester.parseJSONObject(connection); try { RYDVoteData votingData = new RYDVoteData(json); - updateRateLimitAndStats(timeNetworkCallStarted, false, false); + updateRateLimitAndStats(false, false); LogHelper.printDebug(() -> "Voting data fetched: " + votingData); return votingData; } catch (JSONException ex) { @@ -291,21 +152,19 @@ public static RYDVoteData fetchVotes(String videoId) { // fall thru to update statistics } } else { - // Unexpected response code. Most likely RYD is temporarily broken. - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), - null, true); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); } - connection.disconnect(); // Something went wrong, might as well disconnect. - } catch (SocketTimeoutException ex) { - handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false); + connection.disconnect(); // something went wrong, might as well disconnect + } catch (SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error + handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex); } catch (IOException ex) { - handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex, true); + handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex); } catch (Exception ex) { // should never happen LogHelper.printException(() -> "Failed to fetch votes", ex); } - updateRateLimitAndStats(timeNetworkCallStarted, true, false); + updateRateLimitAndStats(true, false); return null; } @@ -319,7 +178,7 @@ public static String registerAsNewUser() { if (checkIfRateLimitInEffect("registerAsNewUser")) { return null; } - String userId = randomString(36); + String userId = randomString(); LogHelper.printDebug(() -> "Trying to register new user"); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); @@ -340,13 +199,12 @@ public static String registerAsNewUser() { String solution = solvePuzzle(challenge, difficulty); return confirmRegistration(userId, solution); } - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), - null, true); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); connection.disconnect(); } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex, true); + handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex); } catch (Exception ex) { LogHelper.printException(() -> "Failed to register user", ex); // should never happen } @@ -377,25 +235,20 @@ private static String confirmRegistration(String userId, String solution) { connection.disconnect(); // disconnect, as no more connections will be made for a little while return null; } - String result = null; if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - result = Requester.parseJson(connection); - if (result.equalsIgnoreCase("true")) { - LogHelper.printDebug(() -> "Registration confirmation successful"); - return userId; - } + LogHelper.printDebug(() -> "Registration confirmation successful"); + return userId; } - final String resultLog = result == null ? "(no response)" : result; + + // Something went wrong, might as well disconnect. + String response = Requester.parseJsonAndDisconnect(connection); LogHelper.printInfo(() -> "Failed to confirm registration for user: " + userId - + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), - null, true); - connection.disconnect(); // something went wrong, might as well disconnect + + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'"); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), - ex, true); + handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), ex); } catch (Exception ex) { LogHelper.printException(() -> "Failed to confirm registration for user: " + userId + "solution: " + solution, ex); @@ -425,17 +278,17 @@ private static String getUserId() { return userId; } - public static boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { + public static void sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { ReVancedUtils.verifyOffMainThread(); Objects.requireNonNull(videoId); Objects.requireNonNull(vote); try { String userId = getUserId(); - if (userId == null) return false; + if (userId == null) return; if (checkIfRateLimitInEffect("sendVote")) { - return false; + return; } LogHelper.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote); @@ -451,7 +304,7 @@ public static boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { connection.disconnect(); // disconnect, as no more connections will be made for a little while - return false; + return; } if (responseCode == HTTP_STATUS_CODE_SUCCESS) { JSONObject json = Requester.parseJSONObject(connection); @@ -459,25 +312,24 @@ public static boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { int difficulty = json.getInt("difficulty"); String solution = solvePuzzle(challenge, difficulty); - return confirmVote(videoId, userId, solution); + confirmVote(videoId, userId, solution); + return; } LogHelper.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote + " response code was: " + responseCode); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), - null, true); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex, true); + handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex); } catch (Exception ex) { // should never happen LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); } - return false; } - private static boolean confirmVote(String videoId, String userId, String solution) { + private static void confirmVote(String videoId, String userId, String solution) { ReVancedUtils.verifyOffMainThread(); Objects.requireNonNull(videoId); Objects.requireNonNull(userId); @@ -485,7 +337,7 @@ private static boolean confirmVote(String videoId, String userId, String solutio try { if (checkIfRateLimitInEffect("confirmVote")) { - return false; + return; } LogHelper.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); @@ -499,32 +351,26 @@ private static boolean confirmVote(String videoId, String userId, String solutio final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { connection.disconnect(); // disconnect, as no more connections will be made for a little while - return false; + return; } - String result = null; if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - result = Requester.parseJson(connection); - if (result.equalsIgnoreCase("true")) { LogHelper.printDebug(() -> "Vote confirm successful for video: " + videoId); - return true; - } + return; } - final String resultLog = result == null ? "(no response)" : result; + + // Something went wrong, might as well disconnect. + String response = Requester.parseJsonAndDisconnect(connection); LogHelper.printInfo(() -> "Failed to confirm vote for video: " + videoId - + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), - null, true); - connection.disconnect(); // something went wrong, might as well disconnect + + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'"); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), - ex, true); + handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), ex); } catch (Exception ex) { LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId + " solution: " + solution, ex); // should never happen } - return false; } private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException { @@ -575,12 +421,12 @@ private static String solvePuzzle(String challenge, int difficulty) { } // https://stackoverflow.com/a/157202 - private static String randomString(int len) { + private static String randomString() { String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; SecureRandom rnd = new SecureRandom(); - StringBuilder sb = new StringBuilder(len); - for (int i = 0; i < len; i++) + StringBuilder sb = new StringBuilder(36); + for (int i = 0; i < 36; i++) sb.append(AB.charAt(rnd.nextInt(AB.length()))); return sb.toString(); } diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java index 44385a9e2e..20a544883a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java @@ -24,4 +24,5 @@ private ReturnYouTubeDislikeRoutes() { static HttpURLConnection getRYDConnectionFromRoute(Route route, String... params) throws IOException { return Requester.getConnectionFromRoute(RYD_API_URL, route, params); } + } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/youtube/utils/ReVancedUtils.java index 386485d7a0..8f4664dba2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/youtube/utils/ReVancedUtils.java @@ -255,25 +255,6 @@ public static boolean isNetworkNotConnected() { return networkType == NetworkType.NONE; } - /** - * Simulates a delay by doing meaningless calculations. - * Used for debugging to verify UI timeout logic. - */ - @SuppressWarnings("UnusedReturnValue") - public static long doNothingForDuration(long amountOfTimeToWaste) { - final long timeCalculationStarted = System.currentTimeMillis(); - LogHelper.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms"); - - long meaninglessValue = 0; - while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) { - // could do a thread sleep, but that will trigger an exception if the thread is interrupted - meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random())); - } - // return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work, - // leaving an empty loop that hammers on the System.currentTimeMillis native call - return meaninglessValue; - } - @SuppressLint("MissingPermission") // permission already included in YouTube public static NetworkType getNetworkType() { if (context == null || !(context.getSystemService(Context.CONNECTIVITY_SERVICE) instanceof ConnectivityManager cm))