mirror of
https://github.com/revanced/revanced-integrations.git
synced 2024-11-30 15:52:55 +01:00
fix(YouTube - Spoof client): Fix frozen video on playback start (#520)
This commit is contained in:
parent
7ae7e8d3db
commit
91399540ba
@ -1,6 +1,5 @@
|
|||||||
package app.revanced.integrations.patches.spoof;
|
package app.revanced.integrations.patches.spoof;
|
||||||
|
|
||||||
import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
|
||||||
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -9,16 +8,11 @@ import android.widget.ImageView;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
import app.revanced.integrations.patches.VideoInformation;
|
import app.revanced.integrations.patches.VideoInformation;
|
||||||
|
import app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester;
|
||||||
import app.revanced.integrations.settings.SettingsEnum;
|
import app.revanced.integrations.settings.SettingsEnum;
|
||||||
import app.revanced.integrations.shared.PlayerType;
|
import app.revanced.integrations.shared.PlayerType;
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
import app.revanced.integrations.utils.LogHelper;
|
||||||
import app.revanced.integrations.utils.ReVancedUtils;
|
|
||||||
|
|
||||||
/** @noinspection unused*/
|
/** @noinspection unused*/
|
||||||
public class SpoofSignaturePatch {
|
public class SpoofSignaturePatch {
|
||||||
@ -51,29 +45,16 @@ public class SpoofSignaturePatch {
|
|||||||
/**
|
/**
|
||||||
* Last video id loaded. Used to prevent reloading the same spec multiple times.
|
* Last video id loaded. Used to prevent reloading the same spec multiple times.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
private static volatile String lastPlayerResponseVideoId;
|
private static volatile String lastPlayerResponseVideoId;
|
||||||
|
|
||||||
private static volatile Future<StoryboardRenderer> rendererFuture;
|
@Nullable
|
||||||
|
private static volatile StoryboardRenderer videoRenderer;
|
||||||
|
|
||||||
private static volatile boolean useOriginalStoryboardRenderer;
|
private static volatile boolean useOriginalStoryboardRenderer;
|
||||||
|
|
||||||
private static volatile boolean isPlayingShorts;
|
private static volatile boolean isPlayingShorts;
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static StoryboardRenderer getRenderer() {
|
|
||||||
if (rendererFuture != null) {
|
|
||||||
try {
|
|
||||||
return rendererFuture.get(2000, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (TimeoutException ex) {
|
|
||||||
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
|
|
||||||
} catch (ExecutionException | InterruptedException ex) {
|
|
||||||
// Should never happen.
|
|
||||||
LogHelper.printException(() -> "Could not get renderer", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
*
|
||||||
@ -82,62 +63,64 @@ public class SpoofSignaturePatch {
|
|||||||
* @param parameters Original protobuf parameter value.
|
* @param parameters Original protobuf parameter value.
|
||||||
*/
|
*/
|
||||||
public static String spoofParameter(String parameters) {
|
public static String spoofParameter(String parameters) {
|
||||||
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
try {
|
||||||
|
LogHelper.printDebug(() -> "Original protobuf parameter value: " + parameters);
|
||||||
|
|
||||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters;
|
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()) return parameters;
|
||||||
|
|
||||||
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
|
// Clip's player parameters contain a lot of information (e.g. video start and end time or whether it loops)
|
||||||
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
|
// For this reason, the player parameters of a clip are usually very long (150~300 characters).
|
||||||
// Clips are 60 seconds or less in length, so no spoofing.
|
// Clips are 60 seconds or less in length, so no spoofing.
|
||||||
if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters;
|
if (useOriginalStoryboardRenderer = parameters.length() > 150) return parameters;
|
||||||
|
|
||||||
// Shorts do not need to be spoofed.
|
// Shorts do not need to be spoofed.
|
||||||
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
|
if (useOriginalStoryboardRenderer = parameters.startsWith(SHORTS_PLAYER_PARAMETERS)) {
|
||||||
isPlayingShorts = true;
|
isPlayingShorts = true;
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
isPlayingShorts = false;
|
|
||||||
|
|
||||||
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
|
|
||||||
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
|
|
||||||
if (isPlayingFeed) {
|
|
||||||
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
|
|
||||||
// Don't spoof the feed video playback. This will cause video playback issues,
|
|
||||||
// but only if user continues watching for more than 1 minute.
|
|
||||||
return parameters;
|
return parameters;
|
||||||
}
|
}
|
||||||
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
|
isPlayingShorts = false;
|
||||||
fetchStoryboardRenderer();
|
|
||||||
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchStoryboardRenderer();
|
boolean isPlayingFeed = PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL
|
||||||
|
&& containsAny(parameters, AUTOPLAY_PARAMETERS);
|
||||||
|
if (isPlayingFeed) {
|
||||||
|
if (useOriginalStoryboardRenderer = !SettingsEnum.SPOOF_SIGNATURE_IN_FEED.getBoolean()) {
|
||||||
|
// Don't spoof the feed video playback. This will cause video playback issues,
|
||||||
|
// but only if user continues watching for more than 1 minute.
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
// Spoof the feed video. Video will show up in watch history and video subtitles are missing.
|
||||||
|
fetchStoryboardRenderer();
|
||||||
|
return SCRIM_PARAMETER + INCOGNITO_PARAMETERS;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchStoryboardRenderer();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(() -> "spoofParameter failure", ex);
|
||||||
|
}
|
||||||
return INCOGNITO_PARAMETERS;
|
return INCOGNITO_PARAMETERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void fetchStoryboardRenderer() {
|
private static void fetchStoryboardRenderer() {
|
||||||
if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
|
if (!SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
|
||||||
lastPlayerResponseVideoId = null;
|
lastPlayerResponseVideoId = null;
|
||||||
rendererFuture = null;
|
videoRenderer = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String videoId = VideoInformation.getPlayerResponseVideoId();
|
String videoId = VideoInformation.getPlayerResponseVideoId();
|
||||||
if (!videoId.equals(lastPlayerResponseVideoId)) {
|
if (!videoId.equals(lastPlayerResponseVideoId)) {
|
||||||
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
|
|
||||||
lastPlayerResponseVideoId = videoId;
|
lastPlayerResponseVideoId = videoId;
|
||||||
|
// This will block starting video playback until the fetch completes.
|
||||||
|
// This is desired because if this returns without finishing the fetch,
|
||||||
|
// then video will start playback but the image will be frozen
|
||||||
|
// while the main thread call for the renderer waits for the fetch to complete.
|
||||||
|
videoRenderer = StoryboardRendererRequester.getStoryboardRenderer(videoId);
|
||||||
}
|
}
|
||||||
// Block until the fetch is completed. Without this, occasionally when a new video is opened
|
|
||||||
// the video will be frozen a few seconds while the audio plays.
|
|
||||||
// This is because the main thread is calling to get the storyboard but the fetch is not completed.
|
|
||||||
// To prevent this, call get() here and block until the fetch is completed.
|
|
||||||
// So later when the main thread calls to get the renderer it will never block as the future is done.
|
|
||||||
getRenderer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
|
private static String getStoryboardRendererSpec(String originalStoryboardRendererSpec,
|
||||||
boolean returnNullIfLiveStream) {
|
boolean returnNullIfLiveStream) {
|
||||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
||||||
StoryboardRenderer renderer = getRenderer();
|
StoryboardRenderer renderer = videoRenderer;
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
if (returnNullIfLiveStream && renderer.isLiveStream()) return null;
|
if (returnNullIfLiveStream && renderer.isLiveStream()) return null;
|
||||||
return renderer.getSpec();
|
return renderer.getSpec();
|
||||||
@ -171,7 +154,7 @@ public class SpoofSignaturePatch {
|
|||||||
*/
|
*/
|
||||||
public static int getRecommendedLevel(int originalLevel) {
|
public static int getRecommendedLevel(int originalLevel) {
|
||||||
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
if (SettingsEnum.SPOOF_SIGNATURE.getBoolean() && !useOriginalStoryboardRenderer) {
|
||||||
StoryboardRenderer renderer = getRenderer();
|
StoryboardRenderer renderer = videoRenderer;
|
||||||
if (renderer != null) {
|
if (renderer != null) {
|
||||||
Integer recommendedLevel = renderer.getRecommendedLevel();
|
Integer recommendedLevel = renderer.getRecommendedLevel();
|
||||||
if (recommendedLevel != null) return recommendedLevel;
|
if (recommendedLevel != null) return recommendedLevel;
|
||||||
@ -195,15 +178,19 @@ public class SpoofSignaturePatch {
|
|||||||
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
|
* @param view seekbar thumbnail view. Includes both shorts and regular videos.
|
||||||
*/
|
*/
|
||||||
public static void seekbarImageViewCreated(ImageView view) {
|
public static void seekbarImageViewCreated(ImageView view) {
|
||||||
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()
|
try {
|
||||||
|| SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
|
if (!SettingsEnum.SPOOF_SIGNATURE.getBoolean()
|
||||||
return;
|
|| SettingsEnum.SPOOF_STORYBOARD_RENDERER.getBoolean()) {
|
||||||
}
|
return;
|
||||||
if (isPlayingShorts) return;
|
}
|
||||||
|
if (isPlayingShorts) return;
|
||||||
|
|
||||||
view.setVisibility(View.GONE);
|
view.setVisibility(View.GONE);
|
||||||
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
|
// Also hide the border around the thumbnail (otherwise a 1 pixel wide bordered frame is visible).
|
||||||
ViewGroup parentLayout = (ViewGroup) view.getParent();
|
ViewGroup parentLayout = (ViewGroup) view.getParent();
|
||||||
parentLayout.setPadding(0, 0, 0, 0);
|
parentLayout.setPadding(0, 0, 0, 0);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(() -> "seekbarImageViewCreated failure", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,11 @@ final class PlayerRoutes {
|
|||||||
static final String ANDROID_INNER_TUBE_BODY;
|
static final String ANDROID_INNER_TUBE_BODY;
|
||||||
static final String TV_EMBED_INNER_TUBE_BODY;
|
static final String TV_EMBED_INNER_TUBE_BODY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TCP connection and HTTP read timeout
|
||||||
|
*/
|
||||||
|
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 4 * 1000; // 4 Seconds.
|
||||||
|
|
||||||
static {
|
static {
|
||||||
JSONObject innerTubeBody = new JSONObject();
|
JSONObject innerTubeBody = new JSONObject();
|
||||||
|
|
||||||
@ -88,8 +93,8 @@ final class PlayerRoutes {
|
|||||||
connection.setUseCaches(false);
|
connection.setUseCaches(false);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
|
|
||||||
connection.setConnectTimeout(5000);
|
connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
||||||
connection.setReadTimeout(5000);
|
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,27 +1,48 @@
|
|||||||
package app.revanced.integrations.patches.spoof.requests;
|
package app.revanced.integrations.patches.spoof.requests;
|
||||||
|
|
||||||
|
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.ANDROID_INNER_TUBE_BODY;
|
||||||
|
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.GET_STORYBOARD_SPEC_RENDERER;
|
||||||
|
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.TV_EMBED_INNER_TUBE_BODY;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
|
|
||||||
import app.revanced.integrations.requests.Requester;
|
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
|
||||||
import app.revanced.integrations.utils.ReVancedUtils;
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.*;
|
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
|
||||||
|
import app.revanced.integrations.requests.Requester;
|
||||||
|
import app.revanced.integrations.settings.SettingsEnum;
|
||||||
|
import app.revanced.integrations.utils.LogHelper;
|
||||||
|
import app.revanced.integrations.utils.ReVancedUtils;
|
||||||
|
|
||||||
public class StoryboardRendererRequester {
|
public class StoryboardRendererRequester {
|
||||||
|
|
||||||
private StoryboardRendererRequester() {
|
private StoryboardRendererRequester() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void randomlyWaitIfLocallyDebugging() {
|
||||||
|
final boolean randomlyWait = false; // Enable to simulate slow connection responses.
|
||||||
|
if (randomlyWait) {
|
||||||
|
final long maximumTimeToRandomlyWait = 10000;
|
||||||
|
ReVancedUtils.doNothingForDuration(maximumTimeToRandomlyWait);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex,
|
||||||
|
boolean showToastOnIOException) {
|
||||||
|
if (showToastOnIOException) ReVancedUtils.showToastShort(toastMessage);
|
||||||
|
LogHelper.printInfo(() -> toastMessage, ex);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static JSONObject fetchPlayerResponse(@NonNull String requestBody) {
|
private static JSONObject fetchPlayerResponse(@NonNull String requestBody, boolean showToastOnIOException) {
|
||||||
final long startTime = System.currentTimeMillis();
|
final long startTime = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
ReVancedUtils.verifyOffMainThread();
|
ReVancedUtils.verifyOffMainThread();
|
||||||
@ -33,14 +54,21 @@ public class StoryboardRendererRequester {
|
|||||||
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
|
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
|
||||||
|
|
||||||
final int responseCode = connection.getResponseCode();
|
final int responseCode = connection.getResponseCode();
|
||||||
|
randomlyWaitIfLocallyDebugging();
|
||||||
if (responseCode == 200) return Requester.parseJSONObject(connection);
|
if (responseCode == 200) return Requester.parseJSONObject(connection);
|
||||||
|
|
||||||
LogHelper.printException(() -> "API not available: " + responseCode);
|
// Always show a toast for this, as a non 200 response means something is broken.
|
||||||
|
handleConnectionError("Spoof storyboard not available: " + responseCode,
|
||||||
|
null, showToastOnIOException || SettingsEnum.DEBUG_TOAST_ON_ERROR.getBoolean());
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
} catch (SocketTimeoutException ex) {
|
} catch (SocketTimeoutException ex) {
|
||||||
LogHelper.printException(() -> "API timed out", ex);
|
handleConnectionError("Spoof storyboard temporarily not available (API timed out)",
|
||||||
|
ex, showToastOnIOException);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
handleConnectionError("Spoof storyboard temporarily not available: " + ex.getMessage(),
|
||||||
|
ex, showToastOnIOException);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
LogHelper.printException(() -> "Spoof storyboard fetch failed", ex); // Should never happen.
|
||||||
} finally {
|
} finally {
|
||||||
LogHelper.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
|
LogHelper.printDebug(() -> "Request took: " + (System.currentTimeMillis() - startTime) + "ms");
|
||||||
}
|
}
|
||||||
@ -64,8 +92,9 @@ public class StoryboardRendererRequester {
|
|||||||
* @return StoryboardRenderer or null if playabilityStatus is not OK.
|
* @return StoryboardRenderer or null if playabilityStatus is not OK.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody) {
|
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody,
|
||||||
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody);
|
boolean showToastOnIOException) {
|
||||||
|
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody, showToastOnIOException);
|
||||||
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
|
if (playerResponse != null && isPlayabilityStatusOk(playerResponse))
|
||||||
return getStoryboardRendererUsingResponse(playerResponse);
|
return getStoryboardRendererUsingResponse(playerResponse);
|
||||||
|
|
||||||
@ -103,23 +132,19 @@ public class StoryboardRendererRequester {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
|
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
|
||||||
try {
|
Objects.requireNonNull(videoId);
|
||||||
Objects.requireNonNull(videoId);
|
|
||||||
|
|
||||||
var renderer = getStoryboardRendererUsingBody(String.format(ANDROID_INNER_TUBE_BODY, videoId));
|
var renderer = getStoryboardRendererUsingBody(
|
||||||
|
String.format(ANDROID_INNER_TUBE_BODY, videoId), false);
|
||||||
|
if (renderer == null) {
|
||||||
|
LogHelper.printDebug(() -> videoId + " not available using Android client");
|
||||||
|
renderer = getStoryboardRendererUsingBody(
|
||||||
|
String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId), true);
|
||||||
if (renderer == null) {
|
if (renderer == null) {
|
||||||
LogHelper.printDebug(() -> videoId + " not available using Android client");
|
LogHelper.printDebug(() -> videoId + " not available using TV embedded client");
|
||||||
renderer = getStoryboardRendererUsingBody(String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId));
|
|
||||||
if (renderer == null) {
|
|
||||||
LogHelper.printDebug(() -> videoId + " not available using TV embedded client");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderer;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return renderer;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -139,25 +139,13 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
* Simulates a slow response by doing meaningless calculations.
|
* Simulates a slow response by doing meaningless calculations.
|
||||||
* Used to debug the app UI and verify UI timeout logic works
|
* Used to debug the app UI and verify UI timeout logic works
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("UnusedReturnValue")
|
private static void randomlyWaitIfLocallyDebugging() {
|
||||||
private static long randomlyWaitIfLocallyDebugging() {
|
|
||||||
final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI
|
final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI
|
||||||
if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) {
|
if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) {
|
||||||
final long amountOfTimeToWaste = (long) (Math.random()
|
final long amountOfTimeToWaste = (long) (Math.random()
|
||||||
* (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS));
|
* (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS));
|
||||||
final long timeCalculationStarted = System.currentTimeMillis();
|
ReVancedUtils.doNothingForDuration(amountOfTimeToWaste);
|
||||||
LogHelper.printDebug(() -> "Artificially creating network 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;
|
|
||||||
}
|
}
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,6 +112,26 @@ public class ReVancedUtils {
|
|||||||
return backgroundThreadPool.submit(call);
|
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();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
|
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
|
||||||
return indexOfFirstFound(value, targets) >= 0;
|
return indexOfFirstFound(value, targets) >= 0;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user