From 6f0958b3285cd7456803a6222de8f246ba91c6b5 Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Tue, 31 Dec 2024 21:35:40 +0900 Subject: [PATCH] fix(YouTube - Return YouTube Dislike): Match with ReVanced (Close https://github.com/inotia00/ReVanced_Extended/issues/2611) --- .../requests/RYDVoteData.java | 25 ++- .../requests/ReturnYouTubeDislikeApi.java | 197 ++++++++++++++---- .../extension/shared/utils/Utils.java | 18 ++ .../utils/ReturnYouTubeDislikePatch.java | 4 +- .../ReturnYouTubeDislike.java | 2 - 5 files changed, 185 insertions(+), 61 deletions(-) diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java index a4a56de04..984fa5042 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java @@ -111,22 +111,21 @@ public final class RYDVoteData { public void updateUsingVote(Vote vote) { final int likesToAdd, dislikesToAdd; - switch (vote) { - case LIKE: + dislikesToAdd = switch (vote) { + case LIKE -> { likesToAdd = 1; - dislikesToAdd = 0; - break; - case DISLIKE: + yield 0; + } + case DISLIKE -> { likesToAdd = 0; - dislikesToAdd = 1; - break; - case LIKE_REMOVE: + yield 1; + } + case LIKE_REMOVE -> { likesToAdd = 0; - dislikesToAdd = 0; - break; - default: - throw new IllegalStateException(); - } + yield 0; + } + default -> throw new IllegalStateException(); + }; // If a video has no public likes but RYD has raw like data, // then use the raw data instead. diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index df1e503b5..0f8623fd7 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -27,6 +27,7 @@ import app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Utils; +@SuppressWarnings("All") public class ReturnYouTubeDislikeApi { /** * {@link #fetchVotes(String)} TCP connection timeout @@ -81,9 +82,81 @@ public class ReturnYouTubeDislikeApi { public static boolean toastOnConnectionError = false; + /** + * 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 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)); + Utils.doNothingForDuration(amountOfTimeToWaste); + } + } + /** * Clears any backoff rate limits in effect. * Should be called if RYD is turned on/off. @@ -116,41 +189,64 @@ public class ReturnYouTubeDislikeApi { * @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) { + Logger.printDebug(() -> "Artificially triggering rate limit for debug purposes"); + httpResponseCode = HTTP_STATUS_CODE_RATE_LIMIT; + } + } return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT; } - private static void updateRateLimitAndStats(boolean connectionError, boolean rateLimitHit) { + @SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are only 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) { Logger.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 && toastOnConnectionError) { Utils.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) { + private static void handleConnectionError(@NonNull String toastMessage, + @Nullable Exception ex, + boolean showLongToast) { if (!lastApiCallFailed && toastOnConnectionError) { - Utils.showToastShort(toastMessage); - } - if (ex != null) { - Logger.printInfo(() -> toastMessage, ex); + if (showLongToast) { + Utils.showToastLong(toastMessage); + } else { + Utils.showToastShort(toastMessage); + } } + lastApiCallFailed = true; + + Logger.printInfo(() -> toastMessage, ex); } - /** - * @return NULL if fetch failed, or if a rate limit is in effect. - */ @Nullable public static RYDVoteData fetchVotes(String videoId) { Utils.verifyOffMainThread(); @@ -160,6 +256,7 @@ public class ReturnYouTubeDislikeApi { return null; } Logger.printDebug(() -> "Fetching votes for: " + videoId); + final long timeNetworkCallStarted = System.currentTimeMillis(); try { HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId); @@ -173,10 +270,12 @@ public class ReturnYouTubeDislikeApi { 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(false, true); + updateRateLimitAndStats(timeNetworkCallStarted, false, true); return null; } @@ -185,7 +284,7 @@ public class ReturnYouTubeDislikeApi { JSONObject json = Requester.parseJSONObject(connection); try { RYDVoteData votingData = new RYDVoteData(json); - updateRateLimitAndStats(false, false); + updateRateLimitAndStats(timeNetworkCallStarted, false, false); Logger.printDebug(() -> "Voting data fetched: " + votingData); return votingData; } catch (JSONException ex) { @@ -193,20 +292,21 @@ public class ReturnYouTubeDislikeApi { // fall thru to update statistics } } else { - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + // Unexpected response code. Most likely RYD is temporarily broken. + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); } - 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); + connection.disconnect(); // Something went wrong, might as well disconnect. + } catch (SocketTimeoutException ex) { + handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false); } catch (IOException ex) { - handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex); + handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex, true); } catch (Exception ex) { // should never happen Logger.printException(() -> "fetchVotes failure", ex); } - updateRateLimitAndStats(true, false); + updateRateLimitAndStats(timeNetworkCallStarted, true, false); return null; } @@ -220,7 +320,7 @@ public class ReturnYouTubeDislikeApi { if (checkIfRateLimitInEffect("registerAsNewUser")) { return null; } - String userId = randomString(); + String userId = randomString(36); Logger.printDebug(() -> "Trying to register new user"); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); @@ -241,12 +341,13 @@ public class ReturnYouTubeDislikeApi { String solution = solvePuzzle(challenge, difficulty); return confirmRegistration(userId, solution); } - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); connection.disconnect(); } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex, true); } catch (Exception ex) { Logger.printException(() -> "Failed to register user", ex); // should never happen } @@ -283,15 +384,18 @@ public class ReturnYouTubeDislikeApi { Logger.printDebug(() -> "Registration confirmation successful"); return userId; } + // Something went wrong, might as well disconnect. String response = Requester.parseStringAndDisconnect(connection); Logger.printInfo(() -> "Failed to confirm registration for user: " + userId + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''"); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), + ex, true); } catch (Exception ex) { Logger.printException(() -> "Failed to confirm registration for user: " + userId + "solution: " + solution, ex); @@ -299,16 +403,16 @@ public class ReturnYouTubeDislikeApi { return null; } - public static void sendVote(String userId, String videoId, ReturnYouTubeDislike.Vote vote) { + public static boolean sendVote(String userId, String videoId, ReturnYouTubeDislike.Vote vote) { Utils.verifyOffMainThread(); Objects.requireNonNull(videoId); Objects.requireNonNull(vote); try { - if (userId == null) return; + if (userId == null) return false; if (checkIfRateLimitInEffect("sendVote")) { - return; + return false; } Logger.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote); @@ -325,7 +429,7 @@ public class ReturnYouTubeDislikeApi { final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { connection.disconnect(); // disconnect, as no more connections will be made for a little while - return; + return false; } if (responseCode == HTTP_STATUS_CODE_SUCCESS) { JSONObject json = Requester.parseJSONObject(connection); @@ -333,25 +437,26 @@ public class ReturnYouTubeDislikeApi { int difficulty = json.getInt("difficulty"); String solution = solvePuzzle(challenge, difficulty); - confirmVote(videoId, userId, solution); - return; + return confirmVote(videoId, userId, solution); } Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote + " response code was: " + responseCode); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex, true); } catch (Exception ex) { // should never happen Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); } + return false; } - private static void confirmVote(String videoId, String userId, String solution) { + private static boolean confirmVote(String videoId, String userId, String solution) { Utils.verifyOffMainThread(); Objects.requireNonNull(videoId); Objects.requireNonNull(userId); @@ -359,7 +464,7 @@ public class ReturnYouTubeDislikeApi { try { if (checkIfRateLimitInEffect("confirmVote")) { - return; + return false; } Logger.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); @@ -375,25 +480,29 @@ public class ReturnYouTubeDislikeApi { final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { connection.disconnect(); // disconnect, as no more connections will be made for a little while - return; + return false; } if (responseCode == HTTP_STATUS_CODE_SUCCESS) { Logger.printDebug(() -> "Vote confirm successful for video: " + videoId); - return; + return true; } + // Something went wrong, might as well disconnect. String response = Requester.parseStringAndDisconnect(connection); Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'"); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), + ex, true); } catch (Exception ex) { Logger.printException(() -> "Failed to confirm vote for video: " + videoId + " solution: " + solution, ex); // should never happen } + return false; } private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException { @@ -440,12 +549,12 @@ public class ReturnYouTubeDislikeApi { } // https://stackoverflow.com/a/157202 - private static String randomString() { + private static String randomString(int len) { String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; SecureRandom rnd = new SecureRandom(); - StringBuilder sb = new StringBuilder(36); - for (int i = 0; i < 36; i++) + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) sb.append(AB.charAt(rnd.nextInt(AB.length()))); return sb.toString(); } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java index b0fbf2a62..68077e339 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java @@ -142,6 +142,24 @@ public class Utils { return backgroundThreadPool.submit(call); } + /** + * 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(); + Logger.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; + } public static boolean containsAny(@NonNull String value, @NonNull String... targets) { return indexOfFirstFound(value, targets) >= 0; diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java index 8e29174e5..4c94eb6a4 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java @@ -360,8 +360,8 @@ public class ReturnYouTubeDislikePatch { removeRollingNumberPatchChanges(view); return original; } - final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent - && viewGroupParent.getChildCount() < 2; + final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent && + viewGroupParent.getChildCount() < 2; // Called for all instances of RollingNumber, so must check if text is for a dislikes. // Text will already have the correct content but it's missing the drawable separators. if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString()) || isDescriptionPanel) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java index dd478f4f0..c2d727885 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -118,7 +118,6 @@ public class ReturnYouTubeDislike { */ public static final int leftSeparatorShapePaddingPixels; private static final ShapeDrawable leftSeparatorShape; - public static final Locale locale; static { final Resources resources = Utils.getResources(); @@ -135,7 +134,6 @@ public class ReturnYouTubeDislike { leftSeparatorShape = new ShapeDrawable(new RectShape()); leftSeparatorShape.setBounds(leftSeparatorBounds); - locale = resources.getConfiguration().getLocales().get(0); ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get(); }