mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-06 18:15:51 +01:00
fix(youtube/return-youtube-dislike): don't block the UI thread (#221)
Co-authored-by: LisousEinaiKyrios <user@host> Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
a87e366035
commit
b2eab33e02
@ -1,103 +0,0 @@
|
|||||||
package app.revanced.integrations.returnyoutubedislike;
|
|
||||||
|
|
||||||
|
|
||||||
import android.util.Base64;
|
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import app.revanced.integrations.settings.SettingsEnum;
|
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
|
||||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
|
||||||
|
|
||||||
public class Registration {
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/157202
|
|
||||||
private final String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
private SecureRandom rnd = new SecureRandom();
|
|
||||||
private String userId;
|
|
||||||
|
|
||||||
public String getUserId() {
|
|
||||||
return userId != null ? userId : fetchUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveUserId(String userId) {
|
|
||||||
SettingsEnum.RYD_USER_ID.saveValue(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String solvePuzzle(String challenge, int difficulty) {
|
|
||||||
byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP);
|
|
||||||
|
|
||||||
byte[] buffer = new byte[20];
|
|
||||||
for (int i = 4; i < 20; i++) {
|
|
||||||
buffer[i] = decodedChallenge[i - 4];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
int maxCount = (int) (Math.pow(2, difficulty + 1) * 5);
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-512");
|
|
||||||
for (int i = 0; i < maxCount; i++) {
|
|
||||||
buffer[0] = (byte) i;
|
|
||||||
buffer[1] = (byte) (i >> 8);
|
|
||||||
buffer[2] = (byte) (i >> 16);
|
|
||||||
buffer[3] = (byte) (i >> 24);
|
|
||||||
byte[] messageDigest = md.digest(buffer);
|
|
||||||
|
|
||||||
if (countLeadingZeroes(messageDigest) >= difficulty) {
|
|
||||||
String encode = Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP);
|
|
||||||
return encode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(Registration.class, "Failed to solve puzzle", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String register() {
|
|
||||||
String userId = randomString(36);
|
|
||||||
LogHelper.debug(Registration.class, "Trying to register the following userId: " + userId);
|
|
||||||
return ReturnYouTubeDislikeApi.register(userId, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String randomString(int len) {
|
|
||||||
StringBuilder sb = new StringBuilder(len);
|
|
||||||
for (int i = 0; i < len; i++)
|
|
||||||
sb.append(AB.charAt(rnd.nextInt(AB.length())));
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String fetchUserId() {
|
|
||||||
this.userId = SettingsEnum.RYD_USER_ID.getString();
|
|
||||||
if (this.userId == null) {
|
|
||||||
this.userId = register();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int countLeadingZeroes(byte[] uInt8View) {
|
|
||||||
int zeroes = 0;
|
|
||||||
int value = 0;
|
|
||||||
for (int i = 0; i < uInt8View.length; i++) {
|
|
||||||
value = uInt8View[i] & 0xFF;
|
|
||||||
if (value == 0) {
|
|
||||||
zeroes += 8;
|
|
||||||
} else {
|
|
||||||
int count = 1;
|
|
||||||
if (value >>> 4 == 0) {
|
|
||||||
count += 4;
|
|
||||||
value <<= 4;
|
|
||||||
}
|
|
||||||
if (value >>> 6 == 0) {
|
|
||||||
count += 2;
|
|
||||||
value <<= 2;
|
|
||||||
}
|
|
||||||
zeroes += count - (value >>> 7);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return zeroes;
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,9 @@ import android.icu.text.CompactDecimalFormat;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@ -16,8 +19,16 @@ import app.revanced.integrations.utils.ReVancedUtils;
|
|||||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||||
|
|
||||||
public class ReturnYouTubeDislike {
|
public class ReturnYouTubeDislike {
|
||||||
|
/**
|
||||||
|
* maximum amount of time to block the UI from updates, while waiting for dislike network call to complete
|
||||||
|
*/
|
||||||
|
private static final long MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE = 5000;
|
||||||
|
|
||||||
|
// Different threads read and write these fields. access to them must be synchronized
|
||||||
|
@GuardedBy("this")
|
||||||
private static String currentVideoId;
|
private static String currentVideoId;
|
||||||
public static Integer dislikeCount;
|
@GuardedBy("this")
|
||||||
|
private static Integer dislikeCount;
|
||||||
|
|
||||||
private static boolean isEnabled;
|
private static boolean isEnabled;
|
||||||
private static boolean segmentedButton;
|
private static boolean segmentedButton;
|
||||||
@ -27,7 +38,7 @@ public class ReturnYouTubeDislike {
|
|||||||
DISLIKE(-1),
|
DISLIKE(-1),
|
||||||
LIKE_REMOVE(0);
|
LIKE_REMOVE(0);
|
||||||
|
|
||||||
public int value;
|
public final int value;
|
||||||
|
|
||||||
Vote(int value) {
|
Vote(int value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
@ -36,21 +47,14 @@ public class ReturnYouTubeDislike {
|
|||||||
|
|
||||||
private static Thread _dislikeFetchThread = null;
|
private static Thread _dislikeFetchThread = null;
|
||||||
private static Thread _votingThread = null;
|
private static Thread _votingThread = null;
|
||||||
private static Registration registration;
|
|
||||||
private static Voting voting;
|
|
||||||
private static CompactDecimalFormat compactNumberFormatter;
|
private static CompactDecimalFormat compactNumberFormatter;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
Context context = ReVancedUtils.getContext();
|
|
||||||
isEnabled = SettingsEnum.RYD_ENABLED.getBoolean();
|
isEnabled = SettingsEnum.RYD_ENABLED.getBoolean();
|
||||||
if (isEnabled) {
|
|
||||||
registration = new Registration();
|
|
||||||
voting = new Voting(registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
Locale locale = context.getResources().getConfiguration().locale;
|
|
||||||
LogHelper.debug(ReturnYouTubeDislike.class, "locale - " + locale);
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
Context context = ReVancedUtils.getContext();
|
||||||
|
Locale locale = context.getResources().getConfiguration().locale;
|
||||||
|
LogHelper.debug(ReturnYouTubeDislike.class, "Locale: " + locale);
|
||||||
compactNumberFormatter = CompactDecimalFormat.getInstance(
|
compactNumberFormatter = CompactDecimalFormat.getInstance(
|
||||||
locale,
|
locale,
|
||||||
CompactDecimalFormat.CompactStyle.SHORT
|
CompactDecimalFormat.CompactStyle.SHORT
|
||||||
@ -58,34 +62,90 @@ public class ReturnYouTubeDislike {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ReturnYouTubeDislike() {
|
||||||
|
} // only static methods
|
||||||
|
|
||||||
public static void onEnabledChange(boolean enabled) {
|
public static void onEnabledChange(boolean enabled) {
|
||||||
isEnabled = enabled;
|
isEnabled = enabled;
|
||||||
if (registration == null) {
|
}
|
||||||
registration = new Registration();
|
|
||||||
|
public static synchronized String getCurrentVideoId() {
|
||||||
|
return currentVideoId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void setCurrentVideoId(String videoId) {
|
||||||
|
Objects.requireNonNull(videoId);
|
||||||
|
currentVideoId = videoId;
|
||||||
|
dislikeCount = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the dislikeCount for {@link #getCurrentVideoId()}.
|
||||||
|
* Returns NULL if the dislike is not yet loaded, or if the dislike network fetch failed.
|
||||||
|
*/
|
||||||
|
public static synchronized Integer getDislikeCount() {
|
||||||
|
return dislikeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the videoId parameter matches the current dislike request, and the set value was successful.
|
||||||
|
* If videoID parameter does not match currentVideoId, then this call does nothing
|
||||||
|
*/
|
||||||
|
private static synchronized boolean setCurrentDislikeCount(String videoId, Integer videoIdDislikeCount) {
|
||||||
|
if (!videoId.equals(currentVideoId)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (voting == null) {
|
dislikeCount = videoIdDislikeCount;
|
||||||
voting = new Voting(registration);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void interruptDislikeFetchThreadIfRunning() {
|
||||||
|
if (_dislikeFetchThread == null) return;
|
||||||
|
try {
|
||||||
|
Thread.State dislikeFetchThreadState = _dislikeFetchThread.getState();
|
||||||
|
if (dislikeFetchThreadState != Thread.State.TERMINATED) {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislike.class, "Interrupting the fetch dislike thread of state: " + dislikeFetchThreadState);
|
||||||
|
_dislikeFetchThread.interrupt();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(ReturnYouTubeDislike.class, "Error in the fetch dislike thread", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void interruptVoteThreadIfRunning() {
|
||||||
|
if (_votingThread == null) return;
|
||||||
|
try {
|
||||||
|
Thread.State voteThreadState = _votingThread.getState();
|
||||||
|
if (voteThreadState != Thread.State.TERMINATED) {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislike.class, "Interrupting the voting thread of state: " + voteThreadState);
|
||||||
|
_votingThread.interrupt();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(ReturnYouTubeDislike.class, "Error in the voting thread", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void newVideoLoaded(String videoId) {
|
public static void newVideoLoaded(String videoId) {
|
||||||
LogHelper.debug(ReturnYouTubeDislike.class, "newVideoLoaded - " + videoId);
|
|
||||||
|
|
||||||
dislikeCount = null;
|
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
|
LogHelper.debug(ReturnYouTubeDislike.class, "New video loaded: " + videoId);
|
||||||
|
|
||||||
currentVideoId = videoId;
|
setCurrentVideoId(videoId);
|
||||||
|
interruptDislikeFetchThreadIfRunning();
|
||||||
|
|
||||||
try {
|
// TODO use a private fixed size thread pool
|
||||||
if (_dislikeFetchThread != null && _dislikeFetchThread.getState() != Thread.State.TERMINATED) {
|
_dislikeFetchThread = new Thread(() -> {
|
||||||
LogHelper.debug(ReturnYouTubeDislike.class, "Interrupting the thread. Current state " + _dislikeFetchThread.getState());
|
try {
|
||||||
_dislikeFetchThread.interrupt();
|
Integer fetchedDislikeCount = ReturnYouTubeDislikeApi.fetchDislikes(videoId);
|
||||||
|
if (fetchedDislikeCount == null) {
|
||||||
|
return; // fetch failed or thread was interrupted
|
||||||
|
}
|
||||||
|
if (!ReturnYouTubeDislike.setCurrentDislikeCount(videoId, fetchedDislikeCount)) {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislike.class, "Ignoring stale dislike fetched call for video " + videoId);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(ReturnYouTubeDislike.class, "Failed to fetch dislikes for videoId: " + videoId, ex);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
});
|
||||||
LogHelper.printException(ReturnYouTubeDislike.class, "Error in the dislike fetch thread", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
_dislikeFetchThread = new Thread(() -> ReturnYouTubeDislikeApi.fetchDislikes(videoId));
|
|
||||||
_dislikeFetchThread.start();
|
_dislikeFetchThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,17 +156,26 @@ public class ReturnYouTubeDislike {
|
|||||||
var conversionContextString = conversionContext.toString();
|
var conversionContextString = conversionContext.toString();
|
||||||
|
|
||||||
// Check for new component
|
// Check for new component
|
||||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|"))
|
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||||
segmentedButton = true;
|
segmentedButton = true;
|
||||||
else if (!conversionContextString.contains("|dislike_button.eml|"))
|
} else if (!conversionContextString.contains("|dislike_button.eml|")) {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislike.class, "could not find a dislike button in " + conversionContextString);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Have to block the current thread until fetching is done
|
// Have to block the current thread until fetching is done
|
||||||
// There's no known way to edit the text after creation yet
|
// There's no known way to edit the text after creation yet
|
||||||
if (_dislikeFetchThread != null) _dislikeFetchThread.join();
|
if (_dislikeFetchThread != null) {
|
||||||
|
_dislikeFetchThread.join(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE);
|
||||||
|
}
|
||||||
|
|
||||||
if (dislikeCount == null) return;
|
Integer fetchedDislikeCount = getDislikeCount();
|
||||||
|
if (fetchedDislikeCount == null) {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislike.class, "Cannot add dislike count to UI (dislike count not available)");
|
||||||
|
// There's no point letting the request continue, as there is not another chance to use the result
|
||||||
|
interruptDislikeFetchThreadIfRunning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updateDislike(textRef, dislikeCount);
|
updateDislike(textRef, dislikeCount);
|
||||||
LogHelper.debug(ReturnYouTubeDislike.class, "Updated text on component" + conversionContextString);
|
LogHelper.debug(ReturnYouTubeDislike.class, "Updated text on component" + conversionContextString);
|
||||||
@ -118,24 +187,18 @@ public class ReturnYouTubeDislike {
|
|||||||
public static void sendVote(Vote vote) {
|
public static void sendVote(Vote vote) {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
|
|
||||||
Context context = ReVancedUtils.getContext();
|
Context context = Objects.requireNonNull(ReVancedUtils.getContext());
|
||||||
if (SharedPrefHelper.getBoolean(Objects.requireNonNull(context), SharedPrefHelper.SharedPrefNames.YOUTUBE, "user_signed_out", true))
|
if (SharedPrefHelper.getBoolean(context, SharedPrefHelper.SharedPrefNames.YOUTUBE, "user_signed_out", true))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LogHelper.debug(ReturnYouTubeDislike.class, "sending vote - " + vote + " for video " + currentVideoId);
|
// Must make a local copy of videoId, since it may change between now and when the vote thread runs
|
||||||
try {
|
String videoIdToVoteFor = getCurrentVideoId();
|
||||||
if (_votingThread != null && _votingThread.getState() != Thread.State.TERMINATED) {
|
interruptVoteThreadIfRunning();
|
||||||
LogHelper.debug(ReturnYouTubeDislike.class, "Interrupting the thread. Current state " + _votingThread.getState());
|
|
||||||
_votingThread.interrupt();
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(ReturnYouTubeDislike.class, "Error in the voting thread", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO: use a private fixed sized thread pool
|
||||||
_votingThread = new Thread(() -> {
|
_votingThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
boolean result = voting.sendVote(currentVideoId, vote);
|
ReturnYouTubeDislikeApi.sendVote(videoIdToVoteFor, getUserId(), vote);
|
||||||
LogHelper.debug(ReturnYouTubeDislike.class, "sendVote status " + result);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(ReturnYouTubeDislike.class, "Failed to send vote", ex);
|
LogHelper.printException(ReturnYouTubeDislike.class, "Failed to send vote", ex);
|
||||||
}
|
}
|
||||||
@ -143,11 +206,40 @@ public class ReturnYouTubeDislike {
|
|||||||
_votingThread.start();
|
_votingThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock used exclusively by {@link #getUserId()}
|
||||||
|
*/
|
||||||
|
private static final Object rydUserIdLock = new Object();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must call off main thread, as this will make a network call if user has not yet been registered yet
|
||||||
|
*
|
||||||
|
* @return ReturnYouTubeDislike user ID. If user registration has never happened
|
||||||
|
* and the network call fails, this will return NULL
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static String getUserId() {
|
||||||
|
ReVancedUtils.verifyOffMainThread();
|
||||||
|
|
||||||
|
synchronized (rydUserIdLock) {
|
||||||
|
String userId = SettingsEnum.RYD_USER_ID.getString();
|
||||||
|
if (userId != null) {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
userId = ReturnYouTubeDislikeApi.registerAsNewUser(); // blocks until network call is completed
|
||||||
|
if (userId != null) {
|
||||||
|
SettingsEnum.RYD_USER_ID.saveValue(userId);
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void updateDislike(AtomicReference<Object> textRef, Integer dislikeCount) {
|
private static void updateDislike(AtomicReference<Object> textRef, Integer dislikeCount) {
|
||||||
SpannableString oldSpannableString = (SpannableString) textRef.get();
|
SpannableString oldSpannableString = (SpannableString) textRef.get();
|
||||||
|
|
||||||
// parse the buttons string
|
// Parse the buttons string.
|
||||||
// if the button is segmented, only get the like count as a string
|
// If the button is segmented, only get the like count as a string
|
||||||
var oldButtonString = oldSpannableString.toString();
|
var oldButtonString = oldSpannableString.toString();
|
||||||
if (segmentedButton) oldButtonString = oldButtonString.split(" \\| ")[0];
|
if (segmentedButton) oldButtonString = oldButtonString.split(" \\| ")[0];
|
||||||
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package app.revanced.integrations.returnyoutubedislike;
|
|
||||||
|
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
|
||||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
|
||||||
|
|
||||||
public class Voting {
|
|
||||||
private Registration registration;
|
|
||||||
|
|
||||||
public Voting(Registration registration) {
|
|
||||||
this.registration = registration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) {
|
|
||||||
String userId = registration.getUserId();
|
|
||||||
LogHelper.debug(Voting.class, "Trying to vote the following video: " + videoId + " with vote " + vote + " and userId: " + userId);
|
|
||||||
return ReturnYouTubeDislikeApi.sendVote(videoId, userId, vote.value);
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,71 +2,178 @@ package app.revanced.integrations.returnyoutubedislike.requests;
|
|||||||
|
|
||||||
import static app.revanced.integrations.requests.Requester.parseJson;
|
import static app.revanced.integrations.requests.Requester.parseJson;
|
||||||
|
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.ProtocolException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.integrations.returnyoutubedislike.ReturnYouTubeDislike;
|
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
|
||||||
import app.revanced.integrations.returnyoutubedislike.Registration;
|
|
||||||
import app.revanced.integrations.requests.Requester;
|
import app.revanced.integrations.requests.Requester;
|
||||||
import app.revanced.integrations.requests.Route;
|
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 {
|
public class ReturnYouTubeDislikeApi {
|
||||||
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";
|
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";
|
||||||
|
|
||||||
|
private static final int HTTP_CONNECTION_DEFAULT_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates a client rate limit has been reached
|
||||||
|
*/
|
||||||
|
private static final int RATE_LIMIT_HTTP_STATUS_CODE = 429;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long wait until API calls are resumed, if a rate limit is hit
|
||||||
|
* No clear guideline of how long to backoff. Using 60 seconds for now.
|
||||||
|
*/
|
||||||
|
private static final int RATE_LIMIT_BACKOFF_SECONDS = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 access this
|
||||||
|
|
||||||
private ReturnYouTubeDislikeApi() {
|
private ReturnYouTubeDislikeApi() {
|
||||||
|
} // utility class
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return True, if api rate limit is in effect.
|
||||||
|
*/
|
||||||
|
private static boolean checkIfRateLimitInEffect(String apiEndPointName) {
|
||||||
|
if (lastTimeLimitWasHit == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final long numberOfSecondsSinceLastRateLimit = (System.currentTimeMillis() - lastTimeLimitWasHit) / 1000;
|
||||||
|
if (numberOfSecondsSinceLastRateLimit < RATE_LIMIT_BACKOFF_SECONDS) {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Ignoring api call " + apiEndPointName + " as only "
|
||||||
|
+ numberOfSecondsSinceLastRateLimit + " seconds has passed since last rate limit.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void fetchDislikes(String videoId) {
|
/**
|
||||||
|
* @return True, if the rate limit was reached.
|
||||||
|
*/
|
||||||
|
private static boolean checkIfRateLimitWasHit(int httpResponseCode) {
|
||||||
|
// set to true, to verify rate limit works
|
||||||
|
final boolean DEBUG_RATE_LIMIT = false;
|
||||||
|
if (DEBUG_RATE_LIMIT) {
|
||||||
|
final double RANDOM_RATE_LIMIT_PERCENTAGE = 0.1; // 10% chance of a triggering a rate limit
|
||||||
|
if (Math.random() < RANDOM_RATE_LIMIT_PERCENTAGE) {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Artificially triggering rate limit for debug purposes");
|
||||||
|
httpResponseCode = RATE_LIMIT_HTTP_STATUS_CODE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpResponseCode == RATE_LIMIT_HTTP_STATUS_CODE) {
|
||||||
|
lastTimeLimitWasHit = System.currentTimeMillis();
|
||||||
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "API rate limit was hit. Stopping API calls for the next "
|
||||||
|
+ RATE_LIMIT_BACKOFF_SECONDS + " seconds");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The number of dislikes.
|
||||||
|
* Returns NULL if fetch failed, calling thread is interrupted, or rate limit is in effect.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Integer fetchDislikes(String videoId) {
|
||||||
|
ReVancedUtils.verifyOffMainThread();
|
||||||
|
Objects.requireNonNull(videoId);
|
||||||
try {
|
try {
|
||||||
|
if (checkIfRateLimitInEffect("fetchDislikes")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Fetching dislikes for " + videoId);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Fetching dislikes for " + videoId);
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId);
|
||||||
connection.setConnectTimeout(1000);
|
connection.setConnectTimeout(HTTP_CONNECTION_DEFAULT_TIMEOUT);
|
||||||
if (connection.getResponseCode() == 200) {
|
final int responseCode = connection.getResponseCode();
|
||||||
|
if (checkIfRateLimitWasHit(responseCode)) {
|
||||||
|
connection.disconnect();
|
||||||
|
return null;
|
||||||
|
} else if (responseCode == 200) {
|
||||||
JSONObject json = getJSONObject(connection);
|
JSONObject json = getJSONObject(connection);
|
||||||
ReturnYouTubeDislike.dislikeCount = json.getInt("dislikes");
|
Integer fetchedDislikeCount = json.getInt("dislikes");
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "dislikes fetched - " + ReturnYouTubeDislike.dislikeCount);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Dislikes fetched: " + fetchedDislikeCount);
|
||||||
|
connection.disconnect();
|
||||||
|
return fetchedDislikeCount;
|
||||||
} else {
|
} else {
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "dislikes fetch response was " + connection.getResponseCode());
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Dislikes fetch response was " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
}
|
}
|
||||||
connection.disconnect();
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to fetch dislikes", ex);
|
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to fetch dislikes", ex);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String register(String userId, Registration registration) {
|
/**
|
||||||
|
* @return The newly created and registered user id. Returns NULL if registration failed.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static String registerAsNewUser() {
|
||||||
|
ReVancedUtils.verifyOffMainThread();
|
||||||
try {
|
try {
|
||||||
|
if (checkIfRateLimitInEffect("registerAsNewUser")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String userId = randomString(36);
|
||||||
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Trying to register the following userId: " + userId);
|
||||||
|
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId);
|
||||||
connection.setConnectTimeout(5 * 1000);
|
connection.setConnectTimeout(HTTP_CONNECTION_DEFAULT_TIMEOUT);
|
||||||
if (connection.getResponseCode() == 200) {
|
final int responseCode = connection.getResponseCode();
|
||||||
|
if (checkIfRateLimitWasHit(responseCode)) {
|
||||||
|
connection.disconnect();
|
||||||
|
return null;
|
||||||
|
} else if (responseCode == 200) {
|
||||||
JSONObject json = getJSONObject(connection);
|
JSONObject json = getJSONObject(connection);
|
||||||
String challenge = json.getString("challenge");
|
String challenge = json.getString("challenge");
|
||||||
int difficulty = json.getInt("difficulty");
|
int difficulty = json.getInt("difficulty");
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration challenge - " + challenge + " with difficulty of " + difficulty);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration challenge - " + challenge + " with difficulty of " + difficulty);
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
// Solve the puzzle
|
// Solve the puzzle
|
||||||
String solution = Registration.solvePuzzle(challenge, difficulty);
|
String solution = solvePuzzle(challenge, difficulty);
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration confirmation solution is " + solution);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration confirmation solution is " + solution);
|
||||||
|
if (solution == null) {
|
||||||
return confirmRegistration(userId, solution, registration);
|
return null; // failed to solve puzzle
|
||||||
|
}
|
||||||
|
return confirmRegistration(userId, solution);
|
||||||
} else {
|
} else {
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration response was " + connection.getResponseCode());
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration response was " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
}
|
}
|
||||||
connection.disconnect();
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to register userId", ex);
|
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to register userId", ex);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String confirmRegistration(String userId, String solution, Registration registration) {
|
@Nullable
|
||||||
|
private static String confirmRegistration(String userId, String solution) {
|
||||||
|
ReVancedUtils.verifyOffMainThread();
|
||||||
|
Objects.requireNonNull(userId);
|
||||||
|
Objects.requireNonNull(solution);
|
||||||
try {
|
try {
|
||||||
|
if (checkIfRateLimitInEffect("confirmRegistration")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution);
|
||||||
|
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId);
|
||||||
@ -77,20 +184,25 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
||||||
os.write(input, 0, input.length);
|
os.write(input, 0, input.length);
|
||||||
}
|
}
|
||||||
if (connection.getResponseCode() == 200) {
|
final int responseCode = connection.getResponseCode();
|
||||||
|
if (checkIfRateLimitWasHit(responseCode)) {
|
||||||
|
connection.disconnect();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
String result = parseJson(connection);
|
String result = parseJson(connection);
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration confirmation result was " + result);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration confirmation result was " + result);
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
if (result.equalsIgnoreCase("true")) {
|
if (result.equalsIgnoreCase("true")) {
|
||||||
registration.saveUserId(userId);
|
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration was successful for user " + userId);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration was successful for user " + userId);
|
||||||
|
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration confirmation response was " + connection.getResponseCode());
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Registration confirmation response was " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
}
|
}
|
||||||
connection.disconnect();
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to confirm registration", ex);
|
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to confirm registration", ex);
|
||||||
}
|
}
|
||||||
@ -98,33 +210,50 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean sendVote(String videoId, String userId, int vote) {
|
public static boolean sendVote(String videoId, String userId, ReturnYouTubeDislike.Vote vote) {
|
||||||
|
ReVancedUtils.verifyOffMainThread();
|
||||||
|
Objects.requireNonNull(videoId);
|
||||||
|
Objects.requireNonNull(userId);
|
||||||
|
Objects.requireNonNull(vote);
|
||||||
|
|
||||||
|
if (checkIfRateLimitInEffect("sendVote")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Trying to vote the following video: "
|
||||||
|
+ videoId + " with vote " + vote + " and userId: " + userId);
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE);
|
||||||
applyCommonRequestSettings(connection);
|
applyCommonRequestSettings(connection);
|
||||||
|
|
||||||
String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote + "\"}";
|
String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}";
|
||||||
try (OutputStream os = connection.getOutputStream()) {
|
try (OutputStream os = connection.getOutputStream()) {
|
||||||
byte[] input = voteJsonString.getBytes(StandardCharsets.UTF_8);
|
byte[] input = voteJsonString.getBytes(StandardCharsets.UTF_8);
|
||||||
os.write(input, 0, input.length);
|
os.write(input, 0, input.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connection.getResponseCode() == 200) {
|
final int responseCode = connection.getResponseCode();
|
||||||
|
if (checkIfRateLimitWasHit(responseCode)) {
|
||||||
|
connection.disconnect();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
JSONObject json = getJSONObject(connection);
|
JSONObject json = getJSONObject(connection);
|
||||||
String challenge = json.getString("challenge");
|
String challenge = json.getString("challenge");
|
||||||
int difficulty = json.getInt("difficulty");
|
int difficulty = json.getInt("difficulty");
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote challenge - " + challenge + " with difficulty of " + difficulty);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote challenge - " + challenge + " with difficulty of " + difficulty);
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
// Solve the puzzle
|
// Solve the puzzle
|
||||||
String solution = Registration.solvePuzzle(challenge, difficulty);
|
String solution = solvePuzzle(challenge, difficulty);
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote confirmation solution is " + solution);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote confirmation solution is " + solution);
|
||||||
|
|
||||||
// Confirm vote
|
// Confirm vote
|
||||||
return confirmVote(videoId, userId, solution);
|
return confirmVote(videoId, userId, solution);
|
||||||
} else {
|
} else {
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote response was " + connection.getResponseCode());
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote response was " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
}
|
}
|
||||||
connection.disconnect();
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to send vote", ex);
|
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to send vote", ex);
|
||||||
}
|
}
|
||||||
@ -132,6 +261,14 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean confirmVote(String videoId, String userId, String solution) {
|
private static boolean confirmVote(String videoId, String userId, String solution) {
|
||||||
|
ReVancedUtils.verifyOffMainThread();
|
||||||
|
Objects.requireNonNull(videoId);
|
||||||
|
Objects.requireNonNull(userId);
|
||||||
|
Objects.requireNonNull(solution);
|
||||||
|
|
||||||
|
if (checkIfRateLimitInEffect("confirmVote")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
|
||||||
applyCommonRequestSettings(connection);
|
applyCommonRequestSettings(connection);
|
||||||
@ -141,20 +278,28 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
||||||
os.write(input, 0, input.length);
|
os.write(input, 0, input.length);
|
||||||
}
|
}
|
||||||
if (connection.getResponseCode() == 200) {
|
final int responseCode = connection.getResponseCode();
|
||||||
|
if (checkIfRateLimitWasHit(responseCode)) {
|
||||||
|
connection.disconnect();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
String result = parseJson(connection);
|
String result = parseJson(connection);
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote confirmation result was " + result);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote confirmation result was " + result);
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
if (result.equalsIgnoreCase("true")) {
|
if (result.equalsIgnoreCase("true")) {
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote was successful for user " + userId);
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote was successful for user " + userId);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote was unsuccessful for user " + userId);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote confirmation response was " + connection.getResponseCode());
|
LogHelper.debug(ReturnYouTubeDislikeApi.class, "Vote confirmation response was " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
}
|
}
|
||||||
connection.disconnect();
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to confirm vote", ex);
|
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to confirm vote", ex);
|
||||||
}
|
}
|
||||||
@ -163,12 +308,12 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
|
|
||||||
// utils
|
// utils
|
||||||
|
|
||||||
private static void applyCommonRequestSettings(HttpURLConnection connection) throws Exception {
|
private static void applyCommonRequestSettings(HttpURLConnection connection) throws ProtocolException {
|
||||||
connection.setRequestMethod("POST");
|
connection.setRequestMethod("POST");
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("Accept", "application/json");
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
connection.setConnectTimeout(5 * 1000);
|
connection.setConnectTimeout(HTTP_CONNECTION_DEFAULT_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
@ -180,4 +325,68 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
private static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
|
private static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
|
||||||
return Requester.getJSONObject(connection);
|
return Requester.getJSONObject(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String solvePuzzle(String challenge, int difficulty) {
|
||||||
|
byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[20];
|
||||||
|
for (int i = 4; i < 20; i++) {
|
||||||
|
buffer[i] = decodedChallenge[i - 4];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int maxCount = (int) (Math.pow(2, difficulty + 1) * 5);
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-512");
|
||||||
|
for (int i = 0; i < maxCount; i++) {
|
||||||
|
buffer[0] = (byte) i;
|
||||||
|
buffer[1] = (byte) (i >> 8);
|
||||||
|
buffer[2] = (byte) (i >> 16);
|
||||||
|
buffer[3] = (byte) (i >> 24);
|
||||||
|
byte[] messageDigest = md.digest(buffer);
|
||||||
|
|
||||||
|
if (countLeadingZeroes(messageDigest) >= difficulty) {
|
||||||
|
return Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(ReturnYouTubeDislikeApi.class, "Failed to solve puzzle", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/157202
|
||||||
|
private static String randomString(int len) {
|
||||||
|
String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
SecureRandom rnd = new SecureRandom();
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder(len);
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
|
sb.append(AB.charAt(rnd.nextInt(AB.length())));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int countLeadingZeroes(byte[] uInt8View) {
|
||||||
|
int zeroes = 0;
|
||||||
|
int value = 0;
|
||||||
|
for (int i = 0; i < uInt8View.length; i++) {
|
||||||
|
value = uInt8View[i] & 0xFF;
|
||||||
|
if (value == 0) {
|
||||||
|
zeroes += 8;
|
||||||
|
} else {
|
||||||
|
int count = 1;
|
||||||
|
if (value >>> 4 == 0) {
|
||||||
|
count += 4;
|
||||||
|
value <<= 4;
|
||||||
|
}
|
||||||
|
if (value >>> 6 == 0) {
|
||||||
|
count += 2;
|
||||||
|
value <<= 2;
|
||||||
|
}
|
||||||
|
zeroes += count - (value >>> 7);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zeroes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,9 @@ public enum SettingsEnum {
|
|||||||
private final ReturnType returnType;
|
private final ReturnType returnType;
|
||||||
private final boolean rebootApp;
|
private final boolean rebootApp;
|
||||||
|
|
||||||
private Object value = null;
|
// must be volatile, as some settings are changed from non-main threads
|
||||||
|
// of note, the object value is persistently stored using SharedPreferences (which is thread safe)
|
||||||
|
private volatile Object value;
|
||||||
|
|
||||||
SettingsEnum(String path, Object defaultValue, ReturnType returnType) {
|
SettingsEnum(String path, Object defaultValue, ReturnType returnType) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
@ -25,4 +25,4 @@ public class LogHelper {
|
|||||||
public static void info(Class clazz, String message) {
|
public static void info(Class clazz, String message) {
|
||||||
Log.i("revanced: " + (clazz != null ? clazz.getSimpleName() : ""), message);
|
Log.i("revanced: " + (clazz != null ? clazz.getSimpleName() : ""), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package app.revanced.integrations.utils;
|
package app.revanced.integrations.utils;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
@ -10,10 +11,14 @@ import app.revanced.integrations.sponsorblock.player.PlayerType;
|
|||||||
public class ReVancedUtils {
|
public class ReVancedUtils {
|
||||||
|
|
||||||
private static PlayerType env;
|
private static PlayerType env;
|
||||||
public static boolean newVideo = false;
|
private static boolean newVideo = false;
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
public static Context context;
|
public static Context context;
|
||||||
|
|
||||||
|
private ReVancedUtils() {
|
||||||
|
} // utility class
|
||||||
|
|
||||||
public static boolean containsAny(final String value, final String... targets) {
|
public static boolean containsAny(final String value, final String... targets) {
|
||||||
for (String string : targets)
|
for (String string : targets)
|
||||||
if (!string.isEmpty() && value.contains(string)) return true;
|
if (!string.isEmpty() && value.contains(string)) return true;
|
||||||
@ -52,10 +57,6 @@ public class ReVancedUtils {
|
|||||||
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
return context.getResources().getIdentifier(name, defType, context.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void runOnMainThread(Runnable runnable) {
|
|
||||||
new Handler(Looper.getMainLooper()).post(runnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Context getContext() {
|
public static Context getContext() {
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
return context;
|
return context;
|
||||||
@ -68,4 +69,33 @@ public class ReVancedUtils {
|
|||||||
public static boolean isTablet(Context context) {
|
public static boolean isTablet(Context context) {
|
||||||
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void runOnMainThread(Runnable runnable) {
|
||||||
|
new Handler(Looper.getMainLooper()).post(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return if the calling thread is on the main thread
|
||||||
|
*/
|
||||||
|
public static boolean currentIsOnMainThread() {
|
||||||
|
return Looper.getMainLooper().isCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IllegalStateException if the calling thread is _not_ on the main thread
|
||||||
|
*/
|
||||||
|
public static void verifyOnMainThread() throws IllegalStateException {
|
||||||
|
if (!currentIsOnMainThread()) {
|
||||||
|
throw new IllegalStateException("Must call _on_ the main thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws IllegalStateException if the calling thread _is_ on the main thread
|
||||||
|
*/
|
||||||
|
public static void verifyOffMainThread() throws IllegalStateException {
|
||||||
|
if (currentIsOnMainThread()) {
|
||||||
|
throw new IllegalStateException("Must call _off_ the main thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user