mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-07 10:35:49 +01:00
feat(youtube/return-youtube-dislike): better formatting and LTR support (#252)
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de> Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
a26975611e
commit
b839600728
@ -1,15 +1,24 @@
|
|||||||
package app.revanced.integrations.returnyoutubedislike;
|
package app.revanced.integrations.returnyoutubedislike;
|
||||||
|
|
||||||
|
import static app.revanced.integrations.sponsorblock.StringRef.str;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.icu.text.CompactDecimalFormat;
|
import android.icu.text.CompactDecimalFormat;
|
||||||
import android.icu.text.DecimalFormat;
|
|
||||||
import android.icu.text.DecimalFormatSymbols;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.CharacterStyle;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
import android.text.style.RelativeSizeSpan;
|
||||||
|
import android.text.style.ScaleXSpan;
|
||||||
|
|
||||||
import androidx.annotation.GuardedBy;
|
import androidx.annotation.GuardedBy;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.text.NumberFormat;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@ -25,15 +34,16 @@ import app.revanced.integrations.settings.SettingsEnum;
|
|||||||
import app.revanced.integrations.utils.LogHelper;
|
import app.revanced.integrations.utils.LogHelper;
|
||||||
import app.revanced.integrations.utils.ReVancedUtils;
|
import app.revanced.integrations.utils.ReVancedUtils;
|
||||||
import app.revanced.integrations.utils.SharedPrefHelper;
|
import app.revanced.integrations.utils.SharedPrefHelper;
|
||||||
|
import app.revanced.integrations.utils.ThemeHelper;
|
||||||
|
|
||||||
public class ReturnYouTubeDislike {
|
public class ReturnYouTubeDislike {
|
||||||
/**
|
/**
|
||||||
* Maximum amount of time to block the UI from updates while waiting for dislike network call to complete.
|
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
|
||||||
*
|
* <p>
|
||||||
* Must be less than 5 seconds, as per:
|
* Must be less than 5 seconds, as per:
|
||||||
* https://developer.android.com/topic/performance/vitals/anr
|
* https://developer.android.com/topic/performance/vitals/anr
|
||||||
*/
|
*/
|
||||||
private static final long MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE = 4000;
|
private static final long MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE = 4000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to send votes, one by one, in the same order the user created them
|
* Used to send votes, one by one, in the same order the user created them
|
||||||
@ -82,8 +92,8 @@ public class ReturnYouTubeDislike {
|
|||||||
/**
|
/**
|
||||||
* Used to format like/dislike count.
|
* Used to format like/dislike count.
|
||||||
*/
|
*/
|
||||||
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
|
@GuardedBy("ReturnYouTubeDislike.class")
|
||||||
private static DecimalFormat dislikePercentageFormatter;
|
private static NumberFormat dislikePercentageFormatter;
|
||||||
|
|
||||||
public static void onEnabledChange(boolean enabled) {
|
public static void onEnabledChange(boolean enabled) {
|
||||||
isEnabled = enabled;
|
isEnabled = enabled;
|
||||||
@ -111,7 +121,7 @@ public class ReturnYouTubeDislike {
|
|||||||
|
|
||||||
synchronized (videoIdLockObject) {
|
synchronized (videoIdLockObject) {
|
||||||
currentVideoId = videoId;
|
currentVideoId = videoId;
|
||||||
// no need to wrap the fetchDislike call in a try/catch,
|
// no need to wrap the call in a try/catch,
|
||||||
// as any exceptions are propagated out in the later Future#Get call
|
// as any exceptions are propagated out in the later Future#Get call
|
||||||
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
|
||||||
}
|
}
|
||||||
@ -120,18 +130,24 @@ public class ReturnYouTubeDislike {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BEWARE! This method is sometimes called on the main thread, but it usually is called _off_ the main thread!
|
/**
|
||||||
|
* This method is sometimes called on the main thread, but it usually is called _off_ the main thread.
|
||||||
|
* <p>
|
||||||
|
* This method can be called multiple times for the same UI element (including after dislikes was added)
|
||||||
|
* This code should avoid needlessly replacing the same UI element with identical versions.
|
||||||
|
*/
|
||||||
public static void onComponentCreated(Object conversionContext, AtomicReference<Object> textRef) {
|
public static void onComponentCreated(Object conversionContext, AtomicReference<Object> textRef) {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var conversionContextString = conversionContext.toString();
|
String conversionContextString = conversionContext.toString();
|
||||||
|
|
||||||
boolean isSegmentedButton = false;
|
final boolean isSegmentedButton;
|
||||||
// Check for new component
|
|
||||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||||
isSegmentedButton = true;
|
isSegmentedButton = true;
|
||||||
} else if (!conversionContextString.contains("|dislike_button.eml|")) {
|
} else if (conversionContextString.contains("|dislike_button.eml|")) {
|
||||||
|
isSegmentedButton = false;
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,22 +160,26 @@ public class ReturnYouTubeDislike {
|
|||||||
if (SettingsEnum.DEBUG.getBoolean() && !fetchFuture.isDone()) {
|
if (SettingsEnum.DEBUG.getBoolean() && !fetchFuture.isDone()) {
|
||||||
fetchStartTime = System.currentTimeMillis();
|
fetchStartTime = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
votingData = fetchFuture.get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS);
|
votingData = fetchFuture.get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
|
||||||
} catch (TimeoutException e) {
|
} catch (TimeoutException e) {
|
||||||
LogHelper.printDebug(() -> "UI timed out waiting for dislike fetch to complete");
|
LogHelper.printDebug(() -> "UI timed out waiting for fetch votes to complete");
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
recordTimeUISpentWaitingForNetworkCall(fetchStartTime);
|
recordTimeUISpentWaitingForNetworkCall(fetchStartTime);
|
||||||
}
|
}
|
||||||
if (votingData == null) {
|
if (votingData == null) {
|
||||||
LogHelper.printDebug(() -> "Cannot add dislike count to UI (RYD data not available)");
|
LogHelper.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDislike(textRef, isSegmentedButton, votingData);
|
if (updateDislike(textRef, isSegmentedButton, votingData)) {
|
||||||
LogHelper.printDebug(() -> "Updated text");
|
LogHelper.printDebug(() -> "Updated dislike span to: " + textRef.get());
|
||||||
|
} else {
|
||||||
|
LogHelper.printDebug(() -> "Ignoring dislike span: " + textRef.get()
|
||||||
|
+ " that appears to already show voting data: " + votingData);
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Error while trying to update dislikes text", ex);
|
LogHelper.printException(() -> "Error while trying to update dislikes", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,10 +212,10 @@ public class ReturnYouTubeDislike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must call off main thread, as this will make a network call if user has not yet been registered
|
* Must call off main thread, as this will make a network call if user is not yet registered
|
||||||
*
|
*
|
||||||
* @return ReturnYouTubeDislike user ID. If user registration has never happened
|
* @return ReturnYouTubeDislike user ID. If user registration has never happened
|
||||||
* and the network call fails, this will return NULL
|
* and the network call fails, this returns NULL
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String getUserId() {
|
private static String getUserId() {
|
||||||
@ -213,20 +233,38 @@ public class ReturnYouTubeDislike {
|
|||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void updateDislike(AtomicReference<Object> textRef, boolean isSegmentedButton, RYDVoteData voteData) {
|
/**
|
||||||
SpannableString oldSpannableString = (SpannableString) textRef.get();
|
* @param isSegmentedButton if UI is using the segmented single UI component for both like and dislike
|
||||||
String newDislikeString = SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
|
* @return false, if the text reference already has dislike information and no changes were made.
|
||||||
? formatDislikePercentage(voteData.dislikePercentage)
|
*/
|
||||||
: formatDislikeCount(voteData.dislikeCount);
|
private static boolean updateDislike(AtomicReference<Object> textRef, boolean isSegmentedButton, RYDVoteData voteData) {
|
||||||
|
Spannable oldSpannable = (Spannable) textRef.get();
|
||||||
|
String oldLikesString = oldSpannable.toString();
|
||||||
|
Spannable replacementSpannable;
|
||||||
|
|
||||||
if (isSegmentedButton) { // both likes and dislikes are on a custom segmented button
|
// note: some locales use right to left layout (arabic, hebrew, etc),
|
||||||
// parse out the like count as a string
|
// and care must be taken to retain the existing RTL encoding character on the likes string
|
||||||
String oldLikesString = oldSpannableString.toString().split(" \\| ")[0];
|
// otherwise text will incorrectly show as left to right
|
||||||
|
// if making changes to this code, change device settings to a RTL language and verify layout is correct
|
||||||
|
|
||||||
|
if (!isSegmentedButton) {
|
||||||
|
// simple replacement of 'dislike' with a number/percentage
|
||||||
|
if (stringContainsNumber(oldLikesString)) {
|
||||||
|
// already is a number, and was modified in a previous call to this method
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
replacementSpannable = newSpannableWithDislikes(oldSpannable, voteData);
|
||||||
|
} else {
|
||||||
|
String leftSegmentedSeparatorString = ReVancedUtils.isRightToLeftTextLayout() ? "\u200F| " : "| ";
|
||||||
|
|
||||||
|
if (oldLikesString.contains(leftSegmentedSeparatorString)) {
|
||||||
|
return false; // dislikes was previously added
|
||||||
|
}
|
||||||
|
|
||||||
// YouTube creators can hide the like count on a video,
|
// YouTube creators can hide the like count on a video,
|
||||||
// and the like count appears as a device language specific string that says 'Like'
|
// and the like count appears as a device language specific string that says 'Like'
|
||||||
// check if the first character is not a number
|
// check if the string contains any numbers
|
||||||
if (!Character.isDigit(oldLikesString.charAt(0))) {
|
if (!stringContainsNumber(oldLikesString)) {
|
||||||
// likes are hidden.
|
// likes are hidden.
|
||||||
// RYD does not provide usable data for these types of videos,
|
// RYD does not provide usable data for these types of videos,
|
||||||
// and the API returns bogus data (zero likes and zero dislikes)
|
// and the API returns bogus data (zero likes and zero dislikes)
|
||||||
@ -239,22 +277,126 @@ public class ReturnYouTubeDislike {
|
|||||||
//
|
//
|
||||||
// Change the "Likes" string to show that likes and dislikes are hidden
|
// Change the "Likes" string to show that likes and dislikes are hidden
|
||||||
//
|
//
|
||||||
newDislikeString = "Hidden"; // for now, this is not localized
|
|
||||||
LogHelper.printDebug(() -> "Like count is hidden by video creator. "
|
LogHelper.printDebug(() -> "Like count is hidden by video creator. "
|
||||||
+ "RYD does not provide data for videos with hidden likes.");
|
+ "RYD does not provide data for videos with hidden likes.");
|
||||||
|
|
||||||
|
String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
|
||||||
|
if (hiddenMessageString.equals(oldLikesString)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
replacementSpannable = newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
|
||||||
} else {
|
} else {
|
||||||
// temporary fix for https://github.com/revanced/revanced-integrations/issues/118
|
Spannable likesSpan = newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikesString);
|
||||||
newDislikeString = oldLikesString + " | " + newDislikeString;
|
|
||||||
|
// left and middle separator
|
||||||
|
String middleSegmentedSeparatorString = " • ";
|
||||||
|
Spannable leftSeparatorSpan = newSpanUsingStylingOfAnotherSpan(oldSpannable, leftSegmentedSeparatorString);
|
||||||
|
Spannable middleSeparatorSpan = newSpanUsingStylingOfAnotherSpan(oldSpannable, middleSegmentedSeparatorString);
|
||||||
|
// style the separator appearance to mimic the existing layout
|
||||||
|
final int separatorColor = ThemeHelper.isDarkTheme()
|
||||||
|
? 0xFF414141 // dark gray
|
||||||
|
: 0xFFD9D9D9; // light gray
|
||||||
|
addSpanStyling(leftSeparatorSpan, new ForegroundColorSpan(separatorColor));
|
||||||
|
addSpanStyling(middleSeparatorSpan, new ForegroundColorSpan(separatorColor));
|
||||||
|
MetricAffectingSpan separatorStyle = new MetricAffectingSpan() {
|
||||||
|
final float separatorHorizontalCompression = 0.71f; // horizontally compress the separator and its spacing
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMeasureState(TextPaint tp) {
|
||||||
|
tp.setTextScaleX(separatorHorizontalCompression);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint tp) {
|
||||||
|
tp.setTextScaleX(separatorHorizontalCompression);
|
||||||
|
tp.setAntiAlias(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addSpanStyling(leftSeparatorSpan, separatorStyle);
|
||||||
|
addSpanStyling(middleSeparatorSpan, separatorStyle);
|
||||||
|
|
||||||
|
Spannable dislikeSpan = newSpannableWithDislikes(oldSpannable, voteData);
|
||||||
|
|
||||||
|
// use a larger font size on the left separator, but this causes the entire span (including the like/dislike text)
|
||||||
|
// to move downward. Use a custom span to adjust the span back upward, at a relative ratio
|
||||||
|
class RelativeVerticalOffsetSpan extends CharacterStyle {
|
||||||
|
final float relativeVerticalShiftRatio;
|
||||||
|
|
||||||
|
RelativeVerticalOffsetSpan(float relativeVerticalShiftRatio) {
|
||||||
|
this.relativeVerticalShiftRatio = relativeVerticalShiftRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint tp) {
|
||||||
|
tp.baselineShift -= (int) (relativeVerticalShiftRatio * tp.getFontMetrics().top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ratio values tested on Android 13, Samsung, Google and OnePlus branded phones, using screen densities of 300 to 560
|
||||||
|
// On other devices and fonts the left separator may be vertically shifted by a few pixels,
|
||||||
|
// but it's good enough and still visually better than not doing this scaling/shifting
|
||||||
|
final float verticalShiftRatio = -0.38f; // shift up by 38%
|
||||||
|
final float verticalLeftSeparatorShiftRatio = -0.075f; // shift up by 8%
|
||||||
|
final float horizontalStretchRatio = 0.92f; // stretch narrower by 8%
|
||||||
|
final float leftSeparatorFontRatio = 1.87f; // increase height by 87%
|
||||||
|
|
||||||
|
addSpanStyling(leftSeparatorSpan, new RelativeSizeSpan(leftSeparatorFontRatio));
|
||||||
|
addSpanStyling(leftSeparatorSpan, new ScaleXSpan(horizontalStretchRatio));
|
||||||
|
|
||||||
|
// shift the left separator up by a smaller amount, to visually align it after changing the size
|
||||||
|
addSpanStyling(leftSeparatorSpan, new RelativeVerticalOffsetSpan(verticalLeftSeparatorShiftRatio));
|
||||||
|
addSpanStyling(likesSpan, new RelativeVerticalOffsetSpan(verticalShiftRatio));
|
||||||
|
addSpanStyling(middleSeparatorSpan, new RelativeVerticalOffsetSpan(verticalShiftRatio));
|
||||||
|
addSpanStyling(dislikeSpan, new RelativeVerticalOffsetSpan(verticalShiftRatio));
|
||||||
|
|
||||||
|
// middle separator does not need resizing
|
||||||
|
|
||||||
|
// put everything together
|
||||||
|
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
|
builder.append(leftSeparatorSpan);
|
||||||
|
builder.append(likesSpan);
|
||||||
|
builder.append(middleSeparatorSpan);
|
||||||
|
builder.append(dislikeSpan);
|
||||||
|
replacementSpannable = new SpannableString(builder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SpannableString newSpannableString = new SpannableString(newDislikeString);
|
textRef.set(replacementSpannable);
|
||||||
// Copy style (foreground color, etc) to new string
|
return true;
|
||||||
Object[] spans = oldSpannableString.getSpans(0, oldSpannableString.length(), Object.class);
|
}
|
||||||
for (Object span : spans) {
|
|
||||||
newSpannableString.setSpan(span, 0, newDislikeString.length(), oldSpannableString.getSpanFlags(span));
|
/**
|
||||||
|
* Correctly handles any unicode numbers (such as Arabic numbers)
|
||||||
|
*
|
||||||
|
* @return if the string contains at least 1 number
|
||||||
|
*/
|
||||||
|
private static boolean stringContainsNumber(String text) {
|
||||||
|
for (int index = 0, length = text.length(); index < length; index++) {
|
||||||
|
if (Character.isDigit(text.codePointAt(index))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
textRef.set(newSpannableString);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addSpanStyling(Spannable destination, Object styling) {
|
||||||
|
destination.setSpan(styling, 0, destination.length(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Spannable newSpannableWithDislikes(Spannable sourceStyling, RYDVoteData voteData) {
|
||||||
|
return newSpanUsingStylingOfAnotherSpan(sourceStyling,
|
||||||
|
SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
|
||||||
|
? formatDislikePercentage(voteData.dislikePercentage)
|
||||||
|
: formatDislikeCount(voteData.dislikeCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Spannable newSpanUsingStylingOfAnotherSpan(Spannable sourceStyle, String newSpanText) {
|
||||||
|
SpannableString destination = new SpannableString(newSpanText);
|
||||||
|
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
|
||||||
|
for (Object span : spans) {
|
||||||
|
destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span));
|
||||||
|
}
|
||||||
|
return destination;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String formatDislikeCount(long dislikeCount) {
|
private static String formatDislikeCount(long dislikeCount) {
|
||||||
@ -262,6 +404,10 @@ public class ReturnYouTubeDislike {
|
|||||||
String formatted;
|
String formatted;
|
||||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||||
if (dislikeCountFormatter == null) {
|
if (dislikeCountFormatter == null) {
|
||||||
|
// Note: Java number formatters will use the locale specific number characters.
|
||||||
|
// such as Arabic which formats "1.2" into "١٫٢"
|
||||||
|
// But YouTube disregards locale specific number characters
|
||||||
|
// and instead shows english number characters everywhere.
|
||||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||||
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||||
@ -277,28 +423,22 @@ public class ReturnYouTubeDislike {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String formatDislikePercentage(float dislikePercentage) {
|
private static String formatDislikePercentage(float dislikePercentage) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
String formatted;
|
||||||
String formatted;
|
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
if (dislikePercentageFormatter == null) {
|
||||||
if (dislikePercentageFormatter == null) {
|
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||||
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
|
||||||
dislikePercentageFormatter = new DecimalFormat("", new DecimalFormatSymbols(locale));
|
|
||||||
}
|
|
||||||
if (dislikePercentage == 0 || dislikePercentage >= 0.01) { // zero, or at least 1%
|
|
||||||
dislikePercentageFormatter.applyLocalizedPattern("0"); // show only whole percentage points
|
|
||||||
} else { // between (0, 1)%
|
|
||||||
dislikePercentageFormatter.applyLocalizedPattern("0.#"); // show 1 digit precision
|
|
||||||
}
|
|
||||||
final char percentChar = dislikePercentageFormatter.getDecimalFormatSymbols().getPercent();
|
|
||||||
formatted = dislikePercentageFormatter.format(100 * dislikePercentage) + percentChar;
|
|
||||||
}
|
}
|
||||||
LogHelper.printDebug(() -> "Dislike percentage: " + dislikePercentage + " formatted as: " + formatted);
|
if (dislikePercentage >= 0.01) { // at least 1%
|
||||||
return formatted;
|
dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
|
||||||
|
} else {
|
||||||
|
dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
|
||||||
|
}
|
||||||
|
formatted = dislikePercentageFormatter.format(dislikePercentage);
|
||||||
}
|
}
|
||||||
|
LogHelper.printDebug(() -> "Dislike percentage: " + dislikePercentage + " formatted as: " + formatted);
|
||||||
// never will be reached, as the oldest supported YouTube app requires Android N or greater
|
return formatted;
|
||||||
return (int) (100 * dislikePercentage) + "%";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -324,6 +464,6 @@ public class ReturnYouTubeDislike {
|
|||||||
final long averageTimeForcedToWait = totalTimeUIWaitedOnNetworkCalls / numberOfTimesUIWaitedOnNetworkCalls;
|
final long averageTimeForcedToWait = totalTimeUIWaitedOnNetworkCalls / numberOfTimesUIWaitedOnNetworkCalls;
|
||||||
LogHelper.printDebug(() -> "UI thread forced to wait: " + numberOfTimesUIWaitedOnNetworkCalls + " times, "
|
LogHelper.printDebug(() -> "UI thread forced to wait: " + numberOfTimesUIWaitedOnNetworkCalls + " times, "
|
||||||
+ "total wait time: " + totalTimeUIWaitedOnNetworkCalls + "ms, "
|
+ "total wait time: " + totalTimeUIWaitedOnNetworkCalls + "ms, "
|
||||||
+ "average wait time: " + averageTimeForcedToWait + "ms") ;
|
+ "average wait time: " + averageTimeForcedToWait + "ms");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
private static volatile long fetchCallResponseTimeMin;
|
private static volatile long fetchCallResponseTimeMin;
|
||||||
private static volatile long fetchCallResponseTimeMax;
|
private static volatile long fetchCallResponseTimeMax;
|
||||||
|
|
||||||
public static final int FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT = -2;
|
public static final int FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If rate limit was hit, this returns {@link #FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT}
|
* If rate limit was hit, this returns {@link #FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT}
|
||||||
@ -128,8 +128,8 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
} // utility class
|
} // utility class
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only to simulate a slow api call, for debugging the app UI with slow url calls.
|
|
||||||
* 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
|
||||||
*
|
*
|
||||||
* @param maximumTimeToWait maximum time to wait
|
* @param maximumTimeToWait maximum time to wait
|
||||||
*/
|
*/
|
||||||
|
@ -6,6 +6,8 @@ import android.content.res.Resources;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.text.Bidi;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
@ -125,6 +127,15 @@ public class ReVancedUtils {
|
|||||||
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final boolean isRightToLeftTextLayout =
|
||||||
|
new Bidi(Locale.getDefault().getDisplayLanguage(), Bidi.DIRECTION_DEFAULT_RIGHT_TO_LEFT).isRightToLeft();
|
||||||
|
/**
|
||||||
|
* If the device language uses right to left text layout (hebrew, arabic, etc)
|
||||||
|
*/
|
||||||
|
public static boolean isRightToLeftTextLayout() {
|
||||||
|
return isRightToLeftTextLayout;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically logs any exceptions the runnable throws
|
* Automatically logs any exceptions the runnable throws
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user