From 693ef08c6c2ebb2a4dde9194583ac77ae3af9fc6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 22 Dec 2022 01:19:34 +0400 Subject: [PATCH] feat(youtube/return-youtube-dislike): debug connection statistics, toast on error, high priority background threads (#236) Co-authored-by: oSumAtrIX --- .../integrations/requests/Requester.java | 94 +++++--- .../ReturnYouTubeDislike.java | 37 ++- .../requests/ReturnYouTubeDislikeApi.java | 227 +++++++++++++----- .../requests/ReturnYouTubeDislikeRoutes.java | 23 +- .../ReturnYouTubeDislikeSettingsFragment.java | 80 ++++++ .../sponsorblock/requests/SBRequester.java | 14 +- .../integrations/utils/LogHelper.java | 8 +- .../integrations/utils/ReVancedUtils.java | 31 ++- 8 files changed, 400 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/requests/Requester.java b/app/src/main/java/app/revanced/integrations/requests/Requester.java index 6837ce76..37cc6e50 100644 --- a/app/src/main/java/app/revanced/integrations/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/requests/Requester.java @@ -26,28 +26,37 @@ public class Requester { } /** - * Parse, and then disconnect the {@link HttpURLConnection} - * - * TODO: rename this to #parseJsonAndDisconnect + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. */ public static String parseJson(HttpURLConnection connection) throws IOException { - String result = parseJson(connection.getInputStream(), false); + return parseInputStreamAndClose(connection.getInputStream(), true); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * + * Should only be used if other requests to the server are unlikely in the near future + * + * @see #parseJson(HttpURLConnection) + */ + public static String parseJsonAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseJson(connection); connection.disconnect(); return result; } /** - * Parse, and then close the {@link InputStream} + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. * - * TODO: rename this to #parseJsonAndCloseStream + * @param stripNewLineCharacters if newline (\n) characters should be stripped from the InputStream */ - public static String parseJson(InputStream inputStream, boolean isError) throws IOException { + public static String parseInputStreamAndClose(InputStream inputStream, boolean stripNewLineCharacters) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { StringBuilder jsonBuilder = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { jsonBuilder.append(line); - if (isError) + if (!stripNewLineCharacters) jsonBuilder.append("\n"); } return jsonBuilder.toString(); @@ -55,34 +64,63 @@ public class Requester { } /** - * Parse, and then do NOT disconnect the {@link HttpURLConnection} + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. */ public static String parseErrorJson(HttpURLConnection connection) throws IOException { - // TODO: make this also disconnect, and rename method to #parseErrorJsonAndDisconnect - return parseJson(connection.getErrorStream(), true); + return parseInputStreamAndClose(connection.getErrorStream(), false); } /** - * Parse, and then disconnect the {@link HttpURLConnection} + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. * - * TODO: rename this to #getJSONObjectAndDisconnect - */ - public static JSONObject getJSONObject(HttpURLConnection connection) throws JSONException, IOException { - return new JSONObject(parseJsonAndDisconnect(connection)); - } - - /** - * Parse, and then disconnect the {@link HttpURLConnection} + * Should only be used if other requests to the server are unlikely in the near future * - * TODO: rename this to #getJSONArrayAndDisconnect + * @see #parseErrorJson(HttpURLConnection) */ - public static JSONArray getJSONArray(HttpURLConnection connection) throws JSONException, IOException { - return new JSONArray(parseJsonAndDisconnect(connection)); - } - - private static String parseJsonAndDisconnect(HttpURLConnection connection) throws IOException { - String json = parseJson(connection); + public static String parseErrorJsonAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseErrorJson(connection); connection.disconnect(); - return json; + return result; } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + */ + public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException { + return new JSONObject(parseJson(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * + * Should only be used if other requests to the server are unlikely in the near future + * + * @see #parseJSONObject(HttpURLConnection) + */ + public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONObject object = parseJSONObject(connection); + connection.disconnect(); + return object; + } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + */ + public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException { + return new JSONArray(parseJson(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * + * Should only be used if other requests to the server are unlikely in the near future + * + * @see #parseJSONArray(HttpURLConnection) + */ + public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONArray array = parseJSONArray(connection); + connection.disconnect(); + return array; + } + } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index 4f19fb47..d2276b07 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -40,7 +40,7 @@ public class ReturnYouTubeDislike { */ private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor(); - // Must be volatile, since non-main threads read this field. + // Must be volatile, since this is read/write from different threads private static volatile boolean isEnabled = SettingsEnum.RYD_ENABLED.getBoolean(); /** @@ -138,11 +138,18 @@ public class ReturnYouTubeDislike { // Have to block the current thread until fetching is done // There's no known way to edit the text after creation yet RYDVoteData votingData; + long fetchStartTime = 0; try { - votingData = getVoteFetchFuture().get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS); + Future fetchFuture = getVoteFetchFuture(); + if (SettingsEnum.DEBUG.getBoolean() && !fetchFuture.isDone()) { + fetchStartTime = System.currentTimeMillis(); + } + votingData = fetchFuture.get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { LogHelper.printDebug(() -> "UI timed out waiting for dislike fetch to complete"); return; + } finally { + recordTimeUISpentWaitingForNetworkCall(fetchStartTime); } if (votingData == null) { LogHelper.printDebug(() -> "Cannot add dislike count to UI (RYD data not available)"); @@ -293,4 +300,30 @@ public class ReturnYouTubeDislike { // never will be reached, as the oldest supported YouTube app requires Android N or greater return (int) (100 * dislikePercentage) + "%"; } + + + /** + * Number of times the UI was forced to wait on a network fetch to complete + */ + private static volatile int numberOfTimesUIWaitedOnNetworkCalls; + + /** + * Total time the UI waited, of all times it was forced to wait. + */ + private static volatile long totalTimeUIWaitedOnNetworkCalls; + + private static void recordTimeUISpentWaitingForNetworkCall(long timeUIWaitStarted) { + if (timeUIWaitStarted == 0 || !SettingsEnum.DEBUG.getBoolean()) { + return; + } + final long timeUIWaitingTotal = System.currentTimeMillis() - timeUIWaitStarted; + LogHelper.printDebug(() -> "UI thread waited for: " + timeUIWaitingTotal + "ms for vote fetch to complete"); + + totalTimeUIWaitedOnNetworkCalls += timeUIWaitingTotal; + numberOfTimesUIWaitedOnNetworkCalls++; + final long averageTimeForcedToWait = totalTimeUIWaitedOnNetworkCalls / numberOfTimesUIWaitedOnNetworkCalls; + LogHelper.printDebug(() -> "UI thread forced to wait: " + numberOfTimesUIWaitedOnNetworkCalls + " times, " + + "total wait time: " + totalTimeUIWaitedOnNetworkCalls + "ms, " + + "average wait time: " + averageTimeForcedToWait + "ms") ; + } } diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index e85f3c47..73a5711c 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -1,13 +1,16 @@ package app.revanced.integrations.returnyoutubedislike.requests; +import static app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; +import static app.revanced.integrations.sponsorblock.StringRef.str; + import android.util.Base64; +import android.widget.Toast; import androidx.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; -import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.ProtocolException; @@ -18,18 +21,21 @@ import java.security.SecureRandom; import java.util.Objects; import app.revanced.integrations.requests.Requester; -import app.revanced.integrations.requests.Route; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.integrations.utils.LogHelper; import app.revanced.integrations.utils.ReVancedUtils; public class ReturnYouTubeDislikeApi { - private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/"; + /** + * {@link #fetchVotes(String)} TCP connection timeout + */ + private static final int API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS = 2000; /** - * Default connection and response timeout for {@link #fetchVotes(String)} + * {@link #fetchVotes(String)} HTTP read timeout + * To locally debug and force timeouts, change this to a very small number (ie: 100) */ - private static final int API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS = 5000; + private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 4000; /** * Default connection and response timeout for voting and registration. @@ -37,7 +43,7 @@ public class ReturnYouTubeDislikeApi { * Voting and user registration runs in the background and has has no urgency * so this can be a larger value. */ - private static final int API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS = 90000; + private static final int API_REGISTER_VOTE_TIMEOUT_MILLISECONDS = 90000; /** * Response code of a successful API call @@ -59,19 +65,76 @@ public class ReturnYouTubeDislikeApi { * Last time a {@link #RATE_LIMIT_HTTP_STATUS_CODE} was reached. * zero if has not been reached. */ - private static volatile long lastTimeLimitWasHit; // must be volatile, since different threads read/write to this + private static volatile long lastTimeRateLimitWasHit; // must be volatile, since different threads read/write to this + + /** + * Number of times {@link #RATE_LIMIT_HTTP_STATUS_CODE} was requested by RYD api. + * Does not include network calls attempted while rate limit is in effect + */ + 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 = -2; + + /** + * 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 /** - * Only for local debugging to simulate a slow api call. - * Does this by doing meaningless calculations. + * Only to simulate a slow api call, for debugging the app UI with slow url calls. + * Simulates a slow response by doing meaningless calculations. * * @param maximumTimeToWait maximum time to wait */ private static long randomlyWaitIfLocallyDebugging(long maximumTimeToWait) { - final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; + 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() * maximumTimeToWait); final long timeCalculationStarted = System.currentTimeMillis(); @@ -93,10 +156,10 @@ public class ReturnYouTubeDislikeApi { * @return True, if api rate limit is in effect. */ private static boolean checkIfRateLimitInEffect(String apiEndPointName) { - if (lastTimeLimitWasHit == 0) { + if (lastTimeRateLimitWasHit == 0) { return false; } - final long numberOfSecondsSinceLastRateLimit = (System.currentTimeMillis() - lastTimeLimitWasHit) / 1000; + final long numberOfSecondsSinceLastRateLimit = (System.currentTimeMillis() - lastTimeRateLimitWasHit) / 1000; if (numberOfSecondsSinceLastRateLimit < RATE_LIMIT_BACKOFF_SECONDS) { LogHelper.printDebug(() -> "Ignoring api call " + apiEndPointName + " as only " + numberOfSecondsSinceLastRateLimit + " seconds has passed since last rate limit."); @@ -106,13 +169,12 @@ public class ReturnYouTubeDislikeApi { } /** - * @return True, if the rate limit was reached. + * @return True, if a client rate limit was requested */ private static boolean checkIfRateLimitWasHit(int httpResponseCode) { - // set to true, to verify rate limit works - final boolean DEBUG_RATE_LIMIT = false; + 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.1; // 10% chance of a triggering a 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 = RATE_LIMIT_HTTP_STATUS_CODE; @@ -120,7 +182,7 @@ public class ReturnYouTubeDislikeApi { } if (httpResponseCode == RATE_LIMIT_HTTP_STATUS_CODE) { - lastTimeLimitWasHit = System.currentTimeMillis(); + lastTimeRateLimitWasHit = System.currentTimeMillis(); LogHelper.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " + RATE_LIMIT_BACKOFF_SECONDS + " seconds"); return true; @@ -128,6 +190,28 @@ public class ReturnYouTubeDislikeApi { return false; } + private static void updateStatistics(long timeNetworkCallStarted, long timeNetworkCallEnded, boolean connectionError, boolean rateLimitHit) { + if (connectionError && rateLimitHit) { + throw new IllegalArgumentException("both connection error and rate limit parameter were true"); + } + final long responseTimeOfFetchCall = timeNetworkCallEnded - timeNetworkCallStarted; + fetchCallResponseTimeTotal += responseTimeOfFetchCall; + fetchCallResponseTimeMin = (fetchCallResponseTimeMin == 0) ? responseTimeOfFetchCall : Math.min(responseTimeOfFetchCall, fetchCallResponseTimeMin); + fetchCallResponseTimeMax = Math.max(responseTimeOfFetchCall, fetchCallResponseTimeMax); + fetchCallCount++; + if (connectionError) { + fetchCallResponseTimeLast = responseTimeOfFetchCall; + fetchCallNumberOfFailures++; + showToast("revanced_ryd_failure_connection_timeout"); + } else if (rateLimitHit) { + fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT; + numberOfRateLimitRequestsEncountered++; + showToast("revanced_ryd_failure_client_rate_limit_requested"); + } else { + fetchCallResponseTimeLast = responseTimeOfFetchCall; + } + } + /** * @return NULL if fetch failed, or if a rate limit is in effect. */ @@ -135,13 +219,15 @@ public class ReturnYouTubeDislikeApi { public static RYDVoteData fetchVotes(String videoId) { ReVancedUtils.verifyOffMainThread(); Objects.requireNonNull(videoId); - try { - if (checkIfRateLimitInEffect("fetchDislikes")) { - return null; - } - LogHelper.printDebug(() -> "Fetching dislikes for: " + videoId); - HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId); + if (checkIfRateLimitInEffect("fetchVotes")) { + return null; + } + LogHelper.printDebug(() -> "Fetching votes for: " + videoId); + final long timeNetworkCallStarted = System.currentTimeMillis(); + + try { + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId); // request headers, as per https://returnyoutubedislike.com/docs/fetching // the documentation says to use 'Accept:text/html', but the RYD browser plugin uses 'Accept:application/json' connection.setRequestProperty("Accept", "application/json"); @@ -149,33 +235,41 @@ public class ReturnYouTubeDislikeApi { connection.setRequestProperty("Pragma", "no-cache"); connection.setRequestProperty("Cache-Control", "no-cache"); connection.setUseCaches(false); - connection.setConnectTimeout(API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server - connection.setReadTimeout(API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for server response + 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(2 * API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS); + randomlyWaitIfLocallyDebugging(2*(API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS)); final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { - connection.disconnect(); + connection.disconnect(); // rate limit hit, should disconnect + updateStatistics(timeNetworkCallStarted, System.currentTimeMillis(),false, true); return null; } + if (responseCode == SUCCESS_HTTP_STATUS_CODE) { - JSONObject json = Requester.getJSONObject(connection); // also disconnects + final long timeNetworkCallEnded = System.currentTimeMillis(); // record end time before parsing + // do not disconnect, the same server connection will likely be used again soon + JSONObject json = Requester.parseJSONObject(connection); try { RYDVoteData votingData = new RYDVoteData(json); + updateStatistics(timeNetworkCallStarted, timeNetworkCallEnded, false, false); LogHelper.printDebug(() -> "Voting data fetched: " + votingData); return votingData; } catch (JSONException ex) { LogHelper.printException(() -> "Failed to parse video: " + videoId + " json: " + json, ex); - return null; + // fall thru to update statistics } + } else { + LogHelper.printDebug(() -> "Failed to fetch votes for video: " + videoId + + " response code was: " + responseCode); + connection.disconnect(); // something went wrong, might as well disconnect } - LogHelper.printDebug(() -> "Failed to fetch dislikes for video: " + videoId - + " response code was: " + responseCode); - connection.disconnect(); - } catch (Exception ex) { - LogHelper.printException(() -> "Failed to fetch dislikes", ex); + } catch (Exception ex) { // connection timed out, response timeout, or some other network error + LogHelper.printException(() -> "Failed to fetch votes", ex); } + + updateStatistics(timeNetworkCallStarted, System.currentTimeMillis(), true, false); return null; } @@ -192,18 +286,18 @@ public class ReturnYouTubeDislikeApi { String userId = randomString(36); LogHelper.printDebug(() -> "Trying to register new user: " + userId); - HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); connection.setRequestProperty("Accept", "application/json"); - connection.setConnectTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS); - connection.setReadTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS); + connection.setConnectTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { - connection.disconnect(); + connection.disconnect(); // disconnect, as no more connections will be made for a little while return null; } if (responseCode == SUCCESS_HTTP_STATUS_CODE) { - JSONObject json = Requester.getJSONObject(connection); // also disconnects + JSONObject json = Requester.parseJSONObject(connection); String challenge = json.getString("challenge"); int difficulty = json.getInt("difficulty"); @@ -216,6 +310,7 @@ public class ReturnYouTubeDislikeApi { } catch (Exception ex) { LogHelper.printException(() -> "Failed to register user", ex); } + showToast("revanced_ryd_failure_register_user"); return null; } @@ -230,7 +325,7 @@ public class ReturnYouTubeDislikeApi { } LogHelper.printDebug(() -> "Trying to confirm registration for user: " + userId + " with solution: " + solution); - HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId); + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId); applyCommonPostRequestSettings(connection); String jsonInputString = "{\"solution\": \"" + solution + "\"}"; @@ -240,26 +335,27 @@ public class ReturnYouTubeDislikeApi { } final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { - connection.disconnect(); + connection.disconnect(); // disconnect, as no more connections will be made for a little while return null; } if (responseCode == SUCCESS_HTTP_STATUS_CODE) { - String result = Requester.parseJson(connection); // also disconnects + String result = Requester.parseJson(connection); if (result.equalsIgnoreCase("true")) { LogHelper.printDebug(() -> "Registration confirmation successful for user: " + userId); return userId; } LogHelper.printDebug(() -> "Failed to confirm registration for user: " + userId + " solution: " + solution + " response string was: " + result); - return null; + } else { + LogHelper.printDebug(() -> "Failed to confirm registration for user: " + userId + + " solution: " + solution + " response code was: " + responseCode); } - LogHelper.printDebug(() -> "Failed to confirm registration for user: " + userId - + " solution: " + solution + " response code was: " + responseCode); - connection.disconnect(); + connection.disconnect(); // something went wrong, might as well disconnect } catch (Exception ex) { LogHelper.printException(() -> "Failed to confirm registration for user: " + userId + "solution: " + solution, ex); } + showToast("revanced_ryd_failure_confirm_user"); return null; } @@ -277,7 +373,7 @@ public class ReturnYouTubeDislikeApi { LogHelper.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote + " user: " + userId); - HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE); + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE); applyCommonPostRequestSettings(connection); String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}"; @@ -288,11 +384,11 @@ public class ReturnYouTubeDislikeApi { final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { - connection.disconnect(); + connection.disconnect(); // disconnect, as no more connections will be made for a little while return false; } if (responseCode == SUCCESS_HTTP_STATUS_CODE) { - JSONObject json = Requester.getJSONObject(connection); // also disconnects + JSONObject json = Requester.parseJSONObject(connection); String challenge = json.getString("challenge"); int difficulty = json.getInt("difficulty"); @@ -301,11 +397,12 @@ public class ReturnYouTubeDislikeApi { } LogHelper.printDebug(() -> "Failed to send vote for video: " + videoId + " userId: " + userId + " vote: " + vote + " response code was: " + responseCode); - connection.disconnect(); + connection.disconnect(); // something went wrong, might as well disconnect } catch (Exception ex) { LogHelper.printException(() -> "Failed to send vote for video: " + videoId + " user: " + userId + " vote: " + vote, ex); } + showToast("revanced_ryd_failure_send_vote_failed"); return false; } @@ -321,7 +418,7 @@ public class ReturnYouTubeDislikeApi { } LogHelper.printDebug(() -> "Trying to confirm vote for video: " + videoId + " user: " + userId + " solution: " + solution); - HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); applyCommonPostRequestSettings(connection); String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}"; @@ -331,31 +428,36 @@ public class ReturnYouTubeDislikeApi { } final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { - connection.disconnect(); + connection.disconnect(); // disconnect, as no more connections will be made for a little while return false; } if (responseCode == SUCCESS_HTTP_STATUS_CODE) { - String result = Requester.parseJson(connection); // also disconnects + String result = Requester.parseJson(connection); if (result.equalsIgnoreCase("true")) { LogHelper.printDebug(() -> "Vote confirm successful for video: " + videoId); return true; } LogHelper.printDebug(() -> "Failed to confirm vote for video: " + videoId + " user: " + userId + " solution: " + solution + " response string was: " + result); - return false; + } else { + LogHelper.printDebug(() -> "Failed to confirm vote for video: " + videoId + + " user: " + userId + " solution: " + solution + " response code was: " + responseCode); } - LogHelper.printDebug(() -> "Failed to confirm vote for video: " + videoId - + " user: " + userId + " solution: " + solution + " response code was: " + responseCode); - connection.disconnect(); + connection.disconnect(); // something went wrong, might as well disconnect } catch (Exception ex) { LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId + " user: " + userId + " solution: " + solution, ex); } + showToast("revanced_ryd_failure_confirm_vote_failed"); return false; } - // utils + private static void showToast(String toastTextStringKey) { + ReVancedUtils.runOnMainThread(() -> { // must show toasts on main thread + Toast.makeText(ReVancedUtils.getContext(), str(toastTextStringKey), Toast.LENGTH_LONG).show(); + }); + } private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException { connection.setRequestMethod("POST"); @@ -365,15 +467,10 @@ public class ReturnYouTubeDislikeApi { connection.setRequestProperty("Cache-Control", "no-cache"); connection.setUseCaches(false); connection.setDoOutput(true); - connection.setConnectTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server - connection.setReadTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for server response + connection.setConnectTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server + connection.setReadTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); // timeout for server response } - // helpers - - private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException { - return Requester.getConnectionFromRoute(RYD_API_URL, route, params); - } private static String solvePuzzle(String challenge, int difficulty) { final long timeSolveStarted = System.currentTimeMillis(); diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java index 0a26134a..dba0196b 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java @@ -3,15 +3,26 @@ package app.revanced.integrations.returnyoutubedislike.requests; import static app.revanced.integrations.requests.Route.Method.GET; import static app.revanced.integrations.requests.Route.Method.POST; +import java.io.IOException; +import java.net.HttpURLConnection; + +import app.revanced.integrations.requests.Requester; import app.revanced.integrations.requests.Route; -public class ReturnYouTubeDislikeRoutes { - public static final Route SEND_VOTE = new Route(POST, "interact/vote"); - public static final Route CONFIRM_VOTE = new Route(POST, "interact/confirmVote"); - public static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}"); - public static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}"); - public static final Route CONFIRM_REGISTRATION = new Route(POST, "puzzle/registration?userId={user_id}"); +class ReturnYouTubeDislikeRoutes { + static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/"; + + static final Route SEND_VOTE = new Route(POST, "interact/vote"); + static final Route CONFIRM_VOTE = new Route(POST, "interact/confirmVote"); + static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}"); + static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}"); + static final Route CONFIRM_REGISTRATION = new Route(POST, "puzzle/registration?userId={user_id}"); 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/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java index 435b555d..1211a678 100644 --- a/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java +++ b/app/src/main/java/app/revanced/integrations/settingsmenu/ReturnYouTubeDislikeSettingsFragment.java @@ -13,6 +13,7 @@ import android.preference.PreferenceScreen; import android.preference.SwitchPreference; import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.settings.SettingsEnum; import app.revanced.integrations.utils.SharedPrefHelper; @@ -100,6 +101,85 @@ public class ReturnYouTubeDislikeSettingsFragment extends PreferenceFragment { return false; }); preferenceScreen.addPreference(aboutWebsitePreference); + + // RYD API connection statistics + + if (SettingsEnum.DEBUG.getBoolean()) { + PreferenceCategory emptyCategory = new PreferenceCategory(context); // vertical padding + preferenceScreen.addPreference(emptyCategory); + + PreferenceCategory statisticsCategory = new PreferenceCategory(context); + statisticsCategory.setTitle(str("revanced_ryd_statistics_category_title")); + preferenceScreen.addPreference(statisticsCategory); + + Preference statisticPreference; + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeAverage_title")); + statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage())); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMin_title")); + statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin())); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMax_title")); + statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax())); + preferenceScreen.addPreference(statisticPreference); + + String fetchCallTimeWaitingLastSummary; + final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast(); + if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) { + fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary"); + } else { + fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast); + } + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeLast_title")); + statisticPreference.setSummary(fetchCallTimeWaitingLastSummary); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallCount_title")); + statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(), + "revanced_ryd_statistics_getFetchCallCount_zero_summary", + "revanced_ryd_statistics_getFetchCallCount_non_zero_summary")); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallNumberOfFailures_title")); + statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(), + "revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary", + "revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary")); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title")); + statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(), + "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary", + "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary")); + preferenceScreen.addPreference(statisticPreference); + } + } + + private String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) { + if (value == 0) { + return str(summaryStringZeroKey); + } + return String.format(str(summaryStringOneOrMoreKey), value); + } + + private static String createMillisecondStringFromNumber(long number) { + return String.format(str("revanced_ryd_statistics_millisecond_text"), number); } } diff --git a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java index e825d468..babdd1e6 100644 --- a/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java +++ b/app/src/main/java/app/revanced/integrations/sponsorblock/requests/SBRequester.java @@ -46,7 +46,11 @@ public class SBRequester { runVipCheck(); if (responseCode == 200) { - JSONArray responseArray = Requester.getJSONArray(connection); + // FIXME? should this use Requester#getJSONArray and not disconnect? + // HTTPURLConnection#disconnect says: + // disconnect if other requests to the server + // are unlikely in the near future. + JSONArray responseArray = Requester.parseJSONArrayAndDisconnect(connection); int length = responseArray.length(); for (int i = 0; i < length; i++) { JSONObject obj = (JSONObject) responseArray.get(i); @@ -96,13 +100,13 @@ public class SBRequester { SponsorBlockUtils.messageToToast = str("submit_failed_duplicate"); break; case 403: - SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseErrorJson(connection)); + SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)); break; case 429: SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit"); break; case 400: - SponsorBlockUtils.messageToToast = str("submit_failed_invalid", Requester.parseErrorJson(connection)); + SponsorBlockUtils.messageToToast = str("submit_failed_invalid", Requester.parseErrorJsonAndDisconnect(connection)); break; default: SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage()); @@ -143,7 +147,7 @@ public class SBRequester { SponsorBlockUtils.messageToToast = str("vote_succeeded"); break; case 403: - SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseErrorJson(connection)); + SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseErrorJsonAndDisconnect(connection)); break; default: SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage()); @@ -220,6 +224,6 @@ public class SBRequester { } private static JSONObject getJSONObject(Route route, String... params) throws Exception { - return Requester.getJSONObject(getConnectionFromRoute(route, params)); + return Requester.parseJSONObjectAndDisconnect(getConnectionFromRoute(route, params)); } } diff --git a/app/src/main/java/app/revanced/integrations/utils/LogHelper.java b/app/src/main/java/app/revanced/integrations/utils/LogHelper.java index b995eeac..70a73438 100644 --- a/app/src/main/java/app/revanced/integrations/utils/LogHelper.java +++ b/app/src/main/java/app/revanced/integrations/utils/LogHelper.java @@ -53,16 +53,18 @@ public class LogHelper { */ public static void printDebug(LogMessage message) { if (SettingsEnum.DEBUG.getBoolean()) { - var log = new StringBuilder(message.buildMessageString()); + var messageString = message.buildMessageString(); if (SettingsEnum.DEBUG_STACKTRACE.getBoolean()) { + var builder = new StringBuilder(messageString); var sw = new StringWriter(); new Throwable().printStackTrace(new PrintWriter(sw)); - log.append(String.format("\n%s", sw)); + builder.append(String.format("\n%s", sw)); + messageString = builder.toString(); } - Log.d("revanced: " + message.findOuterClassSimpleName(), log.toString()); + Log.d("revanced: " + message.findOuterClassSimpleName(), messageString); } } diff --git a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java index 193e3e50..4dd98bbc 100644 --- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java +++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java @@ -9,6 +9,7 @@ import android.os.Looper; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @@ -32,13 +33,22 @@ public class ReVancedUtils { /** * General purpose pool for network calls and other background tasks. + * All tasks run at max thread priority. */ private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor( - 2, // minimum 2 threads always ready to be used + 1, // minimum 1 thread always ready to be used 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS, TimeUnit.SECONDS, - new LinkedBlockingQueue()); + new LinkedBlockingQueue(), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MAX_PRIORITY); // run at max priority + return t; + } + }); private static void checkIfPoolHasReachedLimit() { if (backgroundThreadPool.getActiveCount() >= SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS) { @@ -54,13 +64,14 @@ public class ReVancedUtils { } public static void runOnBackgroundThread(Runnable task) { - checkIfPoolHasReachedLimit(); backgroundThreadPool.execute(task); + checkIfPoolHasReachedLimit(); } public static Future submitOnBackgroundThread(Callable call) { + Future future = backgroundThreadPool.submit(call); checkIfPoolHasReachedLimit(); - return backgroundThreadPool.submit(call); + return future; } public static boolean containsAny(final String value, final String... targets) { @@ -114,8 +125,18 @@ public class ReVancedUtils { return context.getResources().getConfiguration().smallestScreenWidthDp >= 600; } + /** + * Automatically logs any exceptions the runnable throws + */ public static void runOnMainThread(Runnable runnable) { - new Handler(Looper.getMainLooper()).post(runnable); + Runnable exceptLoggingRunnable = () -> { + try { + runnable.run(); + } catch (Exception ex) { + LogHelper.printException(() -> "Exception on main thread from runnable: " + runnable.toString(), ex); + } + }; + new Handler(Looper.getMainLooper()).post(exceptLoggingRunnable); } /**