diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32e25974..cd5adccf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## [0.89.1-dev.1](https://github.com/revanced/revanced-integrations/compare/v0.89.0...v0.89.1-dev.1) (2022-12-30)
+
+
+### Performance Improvements
+
+* **youtube/general-ads-patch:** reduce list of ignored component names ([#261](https://github.com/revanced/revanced-integrations/issues/261)) ([8d233a2](https://github.com/revanced/revanced-integrations/commit/8d233a2f82f49b93adb90e2b11c9fa46836dd9b0))
+
# [0.89.0](https://github.com/revanced/revanced-integrations/compare/v0.88.0...v0.89.0) (2022-12-30)
diff --git a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java
index 0ead2cd8..d0c5379e 100644
--- a/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java
+++ b/app/src/main/java/app/revanced/integrations/patches/GeneralAdsPatch.java
@@ -13,9 +13,7 @@ public final class GeneralAdsPatch extends Filter {
"related_video_with_context",
"comment_thread", // skip blocking anything in the comments
"|comment.", // skip blocking anything in the comments replies
- "download_",
"library_recent_shelf",
- "playlist_add_to_option_wrapper" // do not block on "add to playlist" flyout menu
};
private final BlockRule custom = new CustomBlockRule(
diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java
index d2276b07..1de57b7a 100644
--- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -1,15 +1,24 @@
package app.revanced.integrations.returnyoutubedislike;
+import static app.revanced.integrations.sponsorblock.StringRef.str;
+
import android.content.Context;
import android.icu.text.CompactDecimalFormat;
-import android.icu.text.DecimalFormat;
-import android.icu.text.DecimalFormatSymbols;
import android.os.Build;
+import android.text.Spannable;
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.Nullable;
+import java.text.NumberFormat;
import java.util.Locale;
import java.util.Objects;
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.ReVancedUtils;
import app.revanced.integrations.utils.SharedPrefHelper;
+import app.revanced.integrations.utils.ThemeHelper;
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.
+ *
* Must be less than 5 seconds, as per:
* 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
@@ -82,8 +92,8 @@ public class ReturnYouTubeDislike {
/**
* Used to format like/dislike count.
*/
- @GuardedBy("ReturnYouTubeDislike.class") // not thread safe
- private static DecimalFormat dislikePercentageFormatter;
+ @GuardedBy("ReturnYouTubeDislike.class")
+ private static NumberFormat dislikePercentageFormatter;
public static void onEnabledChange(boolean enabled) {
isEnabled = enabled;
@@ -111,7 +121,7 @@ public class ReturnYouTubeDislike {
synchronized (videoIdLockObject) {
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
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.
+ *
+ * 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 textRef) {
if (!isEnabled) return;
try {
- var conversionContextString = conversionContext.toString();
+ String conversionContextString = conversionContext.toString();
- boolean isSegmentedButton = false;
- // Check for new component
+ final boolean isSegmentedButton;
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
isSegmentedButton = true;
- } else if (!conversionContextString.contains("|dislike_button.eml|")) {
+ } else if (conversionContextString.contains("|dislike_button.eml|")) {
+ isSegmentedButton = false;
+ } else {
return;
}
@@ -144,22 +160,26 @@ public class ReturnYouTubeDislike {
if (SettingsEnum.DEBUG.getBoolean() && !fetchFuture.isDone()) {
fetchStartTime = System.currentTimeMillis();
}
- votingData = fetchFuture.get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS);
+ votingData = fetchFuture.get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_FETCH_VOTES_TO_COMPLETE, TimeUnit.MILLISECONDS);
} 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;
} finally {
recordTimeUISpentWaitingForNetworkCall(fetchStartTime);
}
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;
}
- updateDislike(textRef, isSegmentedButton, votingData);
- LogHelper.printDebug(() -> "Updated text");
+ if (updateDislike(textRef, isSegmentedButton, votingData)) {
+ 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) {
- 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
- * and the network call fails, this will return NULL
+ * and the network call fails, this returns NULL
*/
@Nullable
private static String getUserId() {
@@ -213,20 +233,38 @@ public class ReturnYouTubeDislike {
return userId;
}
- private static void updateDislike(AtomicReference textRef, boolean isSegmentedButton, RYDVoteData voteData) {
- SpannableString oldSpannableString = (SpannableString) textRef.get();
- String newDislikeString = SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
- ? formatDislikePercentage(voteData.dislikePercentage)
- : formatDislikeCount(voteData.dislikeCount);
+ /**
+ * @param isSegmentedButton if UI is using the segmented single UI component for both like and dislike
+ * @return false, if the text reference already has dislike information and no changes were made.
+ */
+ private static boolean updateDislike(AtomicReference 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
- // parse out the like count as a string
- String oldLikesString = oldSpannableString.toString().split(" \\| ")[0];
+ // note: some locales use right to left layout (arabic, hebrew, etc),
+ // and care must be taken to retain the existing RTL encoding character on the likes string
+ // 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,
// and the like count appears as a device language specific string that says 'Like'
- // check if the first character is not a number
- if (!Character.isDigit(oldLikesString.charAt(0))) {
+ // check if the string contains any numbers
+ if (!stringContainsNumber(oldLikesString)) {
// likes are hidden.
// RYD does not provide usable data for these types of videos,
// 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
//
- newDislikeString = "Hidden"; // for now, this is not localized
LogHelper.printDebug(() -> "Like count is hidden by video creator. "
+ "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 {
- // temporary fix for https://github.com/revanced/revanced-integrations/issues/118
- newDislikeString = oldLikesString + " | " + newDislikeString;
+ Spannable likesSpan = newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikesString);
+
+ // 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);
- // Copy style (foreground color, etc) to new string
- Object[] spans = oldSpannableString.getSpans(0, oldSpannableString.length(), Object.class);
- for (Object span : spans) {
- newSpannableString.setSpan(span, 0, newDislikeString.length(), oldSpannableString.getSpanFlags(span));
+ textRef.set(replacementSpannable);
+ return true;
+ }
+
+ /**
+ * 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) {
@@ -262,6 +404,10 @@ public class ReturnYouTubeDislike {
String formatted;
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
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;
LogHelper.printDebug(() -> "Locale: " + locale);
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
@@ -277,28 +423,22 @@ public class ReturnYouTubeDislike {
}
private static String formatDislikePercentage(float dislikePercentage) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- String formatted;
- synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
- if (dislikePercentageFormatter == null) {
- Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
- LogHelper.printDebug(() -> "Locale: " + 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;
+ String formatted;
+ synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
+ if (dislikePercentageFormatter == null) {
+ Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
+ LogHelper.printDebug(() -> "Locale: " + locale);
+ dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
}
- LogHelper.printDebug(() -> "Dislike percentage: " + dislikePercentage + " formatted as: " + formatted);
- return formatted;
+ if (dislikePercentage >= 0.01) { // at least 1%
+ dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
+ } else {
+ dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
+ }
+ formatted = dislikePercentageFormatter.format(dislikePercentage);
}
-
- // never will be reached, as the oldest supported YouTube app requires Android N or greater
- return (int) (100 * dislikePercentage) + "%";
+ LogHelper.printDebug(() -> "Dislike percentage: " + dislikePercentage + " formatted as: " + formatted);
+ return formatted;
}
@@ -324,6 +464,6 @@ public class ReturnYouTubeDislike {
final long averageTimeForcedToWait = totalTimeUIWaitedOnNetworkCalls / numberOfTimesUIWaitedOnNetworkCalls;
LogHelper.printDebug(() -> "UI thread forced to wait: " + numberOfTimesUIWaitedOnNetworkCalls + " times, "
+ "total wait time: " + totalTimeUIWaitedOnNetworkCalls + "ms, "
- + "average wait time: " + averageTimeForcedToWait + "ms") ;
+ + "average wait time: " + averageTimeForcedToWait + "ms");
}
}
diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
index 73a5711c..f0850c1a 100644
--- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
+++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java
@@ -97,7 +97,7 @@ public class ReturnYouTubeDislikeApi {
private static volatile long fetchCallResponseTimeMin;
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}
@@ -128,8 +128,8 @@ public class ReturnYouTubeDislikeApi {
} // 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.
+ * Used to debug the app UI and verify UI timeout logic works
*
* @param maximumTimeToWait maximum time to wait
*/
diff --git a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java
index 4dd98bbc..6799c3f3 100644
--- a/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java
+++ b/app/src/main/java/app/revanced/integrations/utils/ReVancedUtils.java
@@ -6,6 +6,8 @@ import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
+import java.text.Bidi;
+import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
@@ -125,6 +127,15 @@ public class ReVancedUtils {
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
*/
diff --git a/gradle.properties b/gradle.properties
index e06a62d8..2507c510 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
org.gradle.jvmargs = -Xmx2048m
android.useAndroidX = true
-version = 0.89.0
+version = 0.89.1-dev.1