diff --git a/CHANGELOG.md b/CHANGELOG.md index 59e0082a..8bb8f964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +# [1.3.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.2.2-dev.3...v1.3.0-dev.1) (2024-02-02) + + +### Features + +* **YouTube - Custom filter:** Custom filtering of the protocol buffer ([#562](https://github.com/ReVanced/revanced-integrations/issues/562)) ([0eb7f3f](https://github.com/ReVanced/revanced-integrations/commit/0eb7f3f3af99bf6566526f9c48db2248d93e166c)) + +## [1.2.2-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.2.2-dev.2...v1.2.2-dev.3) (2024-01-31) + + +### Bug Fixes + +* **TikTok:** Add missing settings strings ([#561](https://github.com/ReVanced/revanced-integrations/issues/561)) ([04621f8](https://github.com/ReVanced/revanced-integrations/commit/04621f8a36490df0b35a3940ecc1fbebd6ee287f)) + +## [1.2.2-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.2.2-dev.1...v1.2.2-dev.2) (2024-01-29) + + +### Bug Fixes + +* **YouTube - ReturnYouTubeDislike:** Do not show more than 1 connection toasts if the API is broken ([#560](https://github.com/ReVanced/revanced-integrations/issues/560)) ([2c73209](https://github.com/ReVanced/revanced-integrations/commit/2c7320937adc01221017c740b86c4d6851c96797)) + +## [1.2.2-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.2.1...v1.2.2-dev.1) (2024-01-28) + + +### Bug Fixes + +* **YouTube:** Correctly show channel page on tablet devices ([#558](https://github.com/ReVanced/revanced-integrations/issues/558)) ([d0edafb](https://github.com/ReVanced/revanced-integrations/commit/d0edafb1afc7b87a8eeb085a9e48dc2d20af2f3a)) + ## [1.2.1](https://github.com/ReVanced/revanced-integrations/compare/v1.2.0...v1.2.1) (2024-01-28) diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java b/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java index 33dc8f05..9f6b3ae5 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/Setting.java @@ -97,7 +97,7 @@ public abstract class Setting { @NonNull private static List> allLoadedSettingsSorted() { Collections.sort(SETTINGS, (Setting o1, Setting o2) -> o1.key.compareTo(o2.key)); - return Collections.unmodifiableList(SETTINGS); + return allLoadedSettings(); } /** @@ -131,6 +131,7 @@ public abstract class Setting { /** * Confirmation message to display, if the user tries to change the setting from the default value. + * Currently this works only for Boolean setting types. */ @Nullable public final StringRef userDialogMessage; @@ -206,10 +207,9 @@ public abstract class Setting { /** * Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. */ - public static void migrateOldSettingToNew(@NonNull Setting oldSetting, @NonNull Setting newSetting) { + public static void migrateOldSettingToNew(@NonNull Setting oldSetting, @NonNull Setting newSetting) { if (!oldSetting.isSetToDefault()) { Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting); - //noinspection unchecked newSetting.save(oldSetting.value); oldSetting.resetToDefault(); } diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java index 1f36ac43..6ef0c778 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java @@ -7,6 +7,8 @@ import android.content.SharedPreferences; import android.os.Bundle; import android.preference.*; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.settings.BooleanSetting; @@ -25,6 +27,13 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { */ public static boolean settingImportInProgress; + /** + * Confirm and restart dialog button text and title. + * Set by subclasses if Strings cannot be added as a resource. + */ + @Nullable + protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle; + /** * Used to prevent showing reboot dialog, if user cancels a setting user dialog. */ @@ -80,11 +89,15 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { } private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { - final var context = getContext(); + Utils.verifyOnMainThread(); + final var context = getContext(); + if (confirmDialogTitle == null) { + confirmDialogTitle = str("revanced_settings_confirm_user_dialog_title"); + } showingUserDialogMessage = true; new AlertDialog.Builder(context) - .setTitle(str("revanced_settings_confirm_user_dialog_title")) + .setTitle(confirmDialogTitle) .setMessage(setting.userDialogMessage.toString()) .setPositiveButton(android.R.string.ok, (dialog, id) -> { if (setting.rebootApp) { @@ -201,12 +214,17 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { } public static void showRestartDialog(@NonNull final Context context) { - String positiveButton = str("revanced_settings_restart"); - - new AlertDialog.Builder(context).setMessage(str("revanced_settings_restart_title")) - .setPositiveButton(positiveButton, (dialog, id) -> { - Utils.restartApp(context); - }) + Utils.verifyOnMainThread(); + if (restartDialogTitle == null) { + restartDialogTitle = str("revanced_settings_restart_title"); + } + if (restartDialogButtonText == null) { + restartDialogButtonText = str("revanced_settings_restart"); + } + new AlertDialog.Builder(context) + .setMessage(restartDialogTitle) + .setPositiveButton(restartDialogButtonText, (dialog, id) + -> Utils.restartApp(context)) .setNegativeButton(android.R.string.cancel, null) .setCancelable(false) .show(); diff --git a/app/src/main/java/app/revanced/integrations/tiktok/settings/preference/ReVancedPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/tiktok/settings/preference/ReVancedPreferenceFragment.java index 9f62a2ab..c90fb93b 100644 --- a/app/src/main/java/app/revanced/integrations/tiktok/settings/preference/ReVancedPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/tiktok/settings/preference/ReVancedPreferenceFragment.java @@ -17,6 +17,12 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { protected void initialize() { final var context = getContext(); + // Currently no resources can be compiled for TikTok (fails with aapt error). + // So all TikTok Strings are hard coded in integrations. + restartDialogTitle = "Refresh and restart"; + restartDialogButtonText = "Restart"; + confirmDialogTitle = "Do you wish to proceed?"; + PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); setPreferenceScreen(preferenceScreen); diff --git a/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java b/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java index 778b6c90..8316597d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/TrieSearch.java @@ -231,47 +231,57 @@ public abstract class TrieSearch { } /** + * This method is static and uses a loop to avoid all recursion. + * This is done for performance since the JVM does not do tail recursion optimization. + * + * @param startNode Node to start the search from. * @param searchText Text to search for patterns in. * @param searchTextLength Length of the search text. * @param searchTextIndex Current recursive search text index. Also, the end index of the current pattern match. - * @param currentMatchLength current search depth, and also the length of the current pattern match. * @return If any pattern matches, and it's associated callback halted the search. */ - private boolean matches(T searchText, int searchTextLength, int searchTextIndex, int currentMatchLength, - Object callbackParameter) { - if (leaf != null && leaf.matches(this, - searchText, searchTextLength, searchTextIndex, callbackParameter)) { - return true; // Leaf exists and it matched the search text. - } - if (endOfPatternCallback != null) { - final int matchStartIndex = searchTextIndex - currentMatchLength; - for (@Nullable TriePatternMatchedCallback callback : endOfPatternCallback) { - if (callback == null) { - return true; // No callback and all matches are valid. - } - if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) { - return true; // Callback confirmed the match. + private static boolean matches(final TrieNode startNode, final T searchText, final int searchTextLength, + int searchTextIndex, final Object callbackParameter) { + TrieNode node = startNode; + int currentMatchLength = 0; + + while (true) { + TrieCompressedPath leaf = node.leaf; + if (leaf != null && leaf.matches(node, searchText, searchTextLength, searchTextIndex, callbackParameter)) { + return true; // Leaf exists and it matched the search text. + } + List> endOfPatternCallback = node.endOfPatternCallback; + if (endOfPatternCallback != null) { + final int matchStartIndex = searchTextIndex - currentMatchLength; + for (@Nullable TriePatternMatchedCallback callback : endOfPatternCallback) { + if (callback == null) { + return true; // No callback and all matches are valid. + } + if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) { + return true; // Callback confirmed the match. + } } } - } - if (children == null) { - return false; // Reached a graph end point and there's no further patterns to search. - } - if (searchTextIndex == searchTextLength) { - return false; // Reached end of the search text and found no matches. - } + TrieNode[] children = node.children; + if (children == null) { + return false; // Reached a graph end point and there's no further patterns to search. + } + if (searchTextIndex == searchTextLength) { + return false; // Reached end of the search text and found no matches. + } - final char character = getCharValue(searchText, searchTextIndex); - if (isInvalidRange(character)) { - return false; // Not an ASCII letter/number/symbol. + // Use the start node to reduce VM method lookup, since all nodes are the same class type. + final char character = startNode.getCharValue(searchText, searchTextIndex); + final int arrayIndex = hashIndexForTableSize(children.length, character); + TrieNode child = children[arrayIndex]; + if (child == null || child.nodeValue != character) { + return false; + } + + node = child; + searchTextIndex++; + currentMatchLength++; } - final int arrayIndex = hashIndexForTableSize(children.length, character); - TrieNode child = children[arrayIndex]; - if (child == null || child.nodeValue != character) { - return false; - } - return child.matches(searchText, searchTextLength, searchTextIndex + 1, - currentMatchLength + 1, callbackParameter); } /** @@ -388,7 +398,7 @@ public abstract class TrieSearch { return false; // No patterns were added. } for (int i = startIndex; i < endIndex; i++) { - if (root.matches(textToSearch, endIndex, i, 0, callbackParameter)) return true; + if (TrieNode.matches(root, textToSearch, endIndex, i, callbackParameter)) return true; } return false; } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java index 5133283f..51774f04 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java @@ -12,6 +12,7 @@ import androidx.annotation.Nullable; import app.revanced.integrations.youtube.patches.components.ReturnYouTubeDislikeFilterPatch; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.integrations.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.PlayerType; import app.revanced.integrations.shared.Logger; @@ -21,7 +22,6 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; import static app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote; @@ -70,17 +70,16 @@ public class ReturnYouTubeDislikePatch { private static volatile boolean lithoShortsShouldUseCurrentData; /** - * Last video id prefetched. Field is prevent prefetching the same video id multiple times in a row. + * Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row. */ @Nullable private static volatile String lastPrefetchedVideoId; public static void onRYDStatusChange(boolean rydEnabled) { - if (!rydEnabled) { - // Must remove all values to protect against using stale data - // if the user enables RYD while a video is on screen. - clearData(); - } + ReturnYouTubeDislikeApi.resetRateLimits(); + // Must remove all values to protect against using stale data + // if the user enables RYD while a video is on screen. + clearData(); } private static void clearData() { @@ -240,10 +239,6 @@ public class ReturnYouTubeDislikePatch { } replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original, true, isRollingNumber); - - // When spoofing between 17.09.xx and 17.30.xx the UI is the old layout - // but uses litho and the dislikes is "|dislike_button.eml|". - // But spoofing to that range gives a broken UI layout so no point checking for that. } else if (!isRollingNumber && conversionContextString.contains("|shorts_dislike_button.eml|")) { // Litho Shorts player. if (!Settings.RYD_SHORTS.get()) { @@ -300,9 +295,10 @@ public class ReturnYouTubeDislikePatch { @NonNull String original) { try { CharSequence replacement = onLithoTextLoaded(conversionContext, original, true); - if (!replacement.toString().equals(original)) { + String replacementString = replacement.toString(); + if (!replacementString.equals(original)) { rollingNumberSpan = replacement; - return replacement.toString(); + return replacementString; } // Else, the text was not a likes count but instead the view count or something else. } catch (Exception ex) { Logger.printException(() -> "onRollingNumberLoaded failure", ex); @@ -348,9 +344,8 @@ public class ReturnYouTubeDislikePatch { } else { view.setCompoundDrawables(separator, null, null, null); } - // Liking/disliking can cause the span to grow in size, - // which is ok and is laid out correctly, - // but if the user then undoes their action the layout will not remove the extra padding. + // Disliking can cause the span to grow in size, which is ok and is laid out correctly, + // but if the user then removes their dislike the layout will not adjust to the new shorter width. // Use a center alignment to take up any extra space. view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); // Single line mode does not clip words if the span is larger than the view bounds. diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/WideSearchbarPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/WideSearchbarPatch.java index 4d63a252..fbe7cb4e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/WideSearchbarPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/WideSearchbarPatch.java @@ -4,7 +4,8 @@ import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class WideSearchbarPatch { - public static boolean enableWideSearchbar() { - return Settings.WIDE_SEARCHBAR.get(); + + public static boolean enableWideSearchbar(boolean original) { + return Settings.WIDE_SEARCHBAR.get() || original; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/CustomFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/CustomFilter.java new file mode 100644 index 00000000..f3c59660 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/CustomFilter.java @@ -0,0 +1,178 @@ +package app.revanced.integrations.youtube.patches.components; + +import static app.revanced.integrations.shared.StringRef.str; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.ByteTrieSearch; +import app.revanced.integrations.youtube.StringTrieSearch; +import app.revanced.integrations.youtube.settings.Settings; + +/** + * Allows custom filtering using a path and optionally a proto buffer string. + */ +@SuppressWarnings("unused") +final class CustomFilter extends Filter { + + private static void showInvalidSyntaxToast(@NonNull String expression) { + Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression)); + } + + private static void showInvalidCharactersToast(@NonNull String expression) { + Utils.showToastLong(str("revanced_custom_filter_toast_invalid_characters", expression)); + } + + private static class CustomFilterGroup extends StringFilterGroup { + /** + * Optional character for the path that indicates the custom filter path must match the start. + * Must be the first character of the expression. + */ + public static final String SYNTAX_STARTS_WITH = "^"; + + /** + * Optional character that separates the path from a proto buffer string pattern. + */ + public static final String SYNTAX_BUFFER_SYMBOL = "$"; + + /** + * @return the parsed objects, or NULL if there was a parse error. + */ + @Nullable + @SuppressWarnings("ConstantConditions") + static Collection parseCustomFilterGroups() { + String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get(); + if (rawCustomFilterText.isBlank()) { + return Collections.emptyList(); + } + + // Map key is the path including optional special characters (^ and/or $) + Map result = new HashMap<>(); + Pattern pattern = Pattern.compile( + "(" // map key group + + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with + + "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path + + "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol + + ")" // end map key group + + "(.*)"); // optional buffer string + + for (String expression : rawCustomFilterText.split("\n")) { + if (expression.isBlank()) continue; + + Matcher matcher = pattern.matcher(expression); + if (!matcher.find()) { + showInvalidSyntaxToast(expression); + return null; + } + + final String mapKey = matcher.group(1); + final boolean pathStartsWith = !matcher.group(2).isEmpty(); + final String path = matcher.group(3); + final boolean hasBufferSymbol = !matcher.group(4).isEmpty(); + final String bufferString = matcher.group(5); + + if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) { + showInvalidSyntaxToast(expression); + return null; + } + if (!StringTrieSearch.isValidPattern(path) + || (hasBufferSymbol && !StringTrieSearch.isValidPattern(bufferString))) { + // Currently only ASCII is allowed. + showInvalidCharactersToast(path); + return null; + } + + // Use one group object for all expressions with the same path. + // This ensures the buffer is searched exactly once + // when multiple paths are used with different buffer strings. + CustomFilterGroup group = result.get(mapKey); + if (group == null) { + group = new CustomFilterGroup(pathStartsWith, path); + result.put(mapKey, group); + } + if (hasBufferSymbol) { + group.addBufferString(bufferString); + } + } + + return result.values(); + } + + final boolean startsWith; + ByteTrieSearch bufferSearch; + + CustomFilterGroup(boolean startsWith, @NonNull String path) { + super(Settings.CUSTOM_FILTER, path); + this.startsWith = startsWith; + } + + void addBufferString(@NonNull String bufferString) { + if (bufferSearch == null) { + bufferSearch = new ByteTrieSearch(); + } + bufferSearch.addPattern(bufferString.getBytes()); + } + + @NonNull + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CustomFilterGroup{"); + builder.append("path="); + if (startsWith) builder.append(SYNTAX_STARTS_WITH); + builder.append(filters[0]); + + if (bufferSearch != null) { + String delimitingCharacter = "❙"; + builder.append(", bufferStrings="); + builder.append(delimitingCharacter); + for (byte[] bufferString : bufferSearch.getPatterns()) { + builder.append(new String(bufferString)); + builder.append(delimitingCharacter); + } + } + builder.append("}"); + return builder.toString(); + } + } + + public CustomFilter() { + Collection groups = CustomFilterGroup.parseCustomFilterGroups(); + if (groups == null) { + Settings.CUSTOM_FILTER_STRINGS.resetToDefault(); + Utils.showToastLong(str("revanced_custom_filter_toast_reset")); + groups = Objects.requireNonNull(CustomFilterGroup.parseCustomFilterGroups()); + } + + if (!groups.isEmpty()) { + CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]); + Logger.printDebug(()-> "Using Custom filters: " + Arrays.toString(groupsArray)); + addPathCallbacks(groupsArray); + } + } + + @Override + public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + // All callbacks are custom filter groups. + CustomFilterGroup custom = (CustomFilterGroup) matchedGroup; + if (custom.startsWith && contentIndex != 0) { + return false; + } + if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) { + return false; + } + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java index 368422a9..a260c04e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java @@ -18,7 +18,6 @@ public final class LayoutComponentsFilter extends Filter { null, "cell_description_body" ); - private final CustomFilterGroup custom; private static final ByteArrayFilterGroup mixPlaylists = new ByteArrayFilterGroup( Settings.HIDE_MIX_PLAYLISTS, @@ -68,11 +67,6 @@ public final class LayoutComponentsFilter extends Filter { // Paths. - custom = new CustomFilterGroup( - Settings.CUSTOM_FILTER, - Settings.CUSTOM_FILTER_STRINGS - ); - final var communityPosts = new StringFilterGroup( Settings.HIDE_COMMUNITY_POSTS, "post_base_wrapper" @@ -226,7 +220,6 @@ public final class LayoutComponentsFilter extends Filter { ); addPathCallbacks( - custom, expandableMetadata, inFeedSurvey, notifyMe, @@ -270,8 +263,7 @@ public final class LayoutComponentsFilter extends Filter { if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); - if (matchedGroup != custom && exceptions.matches(path)) - return false; // Exceptions are not filtered. + if (exceptions.matches(path)) return false; // Exceptions are not filtered. // TODO: This also hides the feed Shorts shelf header if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java index 4d9d370e..66ad85f2 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java @@ -18,7 +18,6 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.settings.BooleanSetting; import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.shared.settings.StringSetting; import app.revanced.integrations.youtube.ByteTrieSearch; import app.revanced.integrations.youtube.StringTrieSearch; import app.revanced.integrations.youtube.TrieSearch; @@ -138,25 +137,6 @@ class StringFilterGroup extends FilterGroup { } } -final class CustomFilterGroup extends StringFilterGroup { - - private static String[] getFilterPatterns(StringSetting setting) { - String[] patterns = setting.get().split("\\s+"); - for (String pattern : patterns) { - if (!StringTrieSearch.isValidPattern(pattern)) { - Utils.showToastLong("Invalid custom filter, resetting to default"); - setting.resetToDefault(); - return getFilterPatterns(setting); - } - } - return patterns; - } - - public CustomFilterGroup(BooleanSetting setting, StringSetting filter) { - super(setting, getFilterPatterns(filter)); - } -} - /** * If you have more than 1 filter patterns, then all instances of * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])}, diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java index 7df27578..7e7fe68a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -151,7 +151,7 @@ public class ReturnYouTubeDislike { private final Future future; /** - * Time this instance and the future was created. + * Time this instance and the fetch future was created. */ private final long timeFetched; @@ -185,12 +185,12 @@ public class ReturnYouTubeDislike { /** * Color of the left and middle separator, based on the color of the right separator. - * It's unknown where YT gets the color from, and the colors here are approximated by hand. - * Ideally, the color here would be the actual color YT uses at runtime. + * It's unknown where YT gets the color from, and the values here are approximated by hand. + * Ideally, this would be the actual color YT uses at runtime. * * Older versions before the 'Me' library tab use a slightly different color. * If spoofing was previously used and is now turned off, - * or an old version was recently upgraded then the old colors are sometimes used. + * or an old version was recently upgraded then the old colors are sometimes still used. */ private static int getSeparatorColor() { if (IS_SPOOFING_TO_OLD_SEPARATOR_COLOR) { @@ -411,7 +411,7 @@ public class ReturnYouTubeDislike { } /** - * Should be called if the user changes settings for dislikes appearance. + * Should be called if the user changes dislikes appearance settings. */ public static void clearAllUICaches() { synchronized (fetchCache) { diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index 0b1c8e81..73db5f00 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -62,7 +62,7 @@ public class ReturnYouTubeDislikeApi { * How long to wait until API calls are resumed, if the API requested a back off. * No clear guideline of how long to wait until resuming. */ - private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes. + private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 10 * 60 * 1000; // 10 Minutes. /** * How long to wait until API calls are resumed, if any connection error occurs. @@ -72,7 +72,13 @@ public class ReturnYouTubeDislikeApi { /** * If non zero, then the system time of when API calls can resume. */ - private static volatile long timeToResumeAPICalls; // must be volatile, since different threads read/write to this + private static volatile long timeToResumeAPICalls; + + /** + * If the last API getVotes call failed for any reason (including server requested rate limit). + * Used to prevent showing repeat connection toasts when the API is down. + */ + private static volatile boolean lastApiCallFailed; /** * Number of times {@link #HTTP_STATUS_CODE_RATE_LIMIT} was requested by RYD api. @@ -148,6 +154,18 @@ public class ReturnYouTubeDislikeApi { } } + /** + * Clears any backoff rate limits in effect. + * Should be called if RYD is turned on/off. + */ + public static void resetRateLimits() { + if (lastApiCallFailed || timeToResumeAPICalls != 0) { + Logger.printDebug(() -> "Reset rate limit"); + } + lastApiCallFailed = false; + timeToResumeAPICalls = 0; + } + /** * @return True, if api rate limit is in effect. */ @@ -193,25 +211,36 @@ public class ReturnYouTubeDislikeApi { timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS; fetchCallResponseTimeLast = responseTimeOfFetchCall; fetchCallNumberOfFailures++; + lastApiCallFailed = true; } else if (rateLimitHit) { Logger.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " + BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds"); timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS; numberOfRateLimitRequestsEncountered++; fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT; - Utils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); + if (!lastApiCallFailed && Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) { + Utils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); + } + lastApiCallFailed = true; } else { fetchCallResponseTimeLast = responseTimeOfFetchCall; + lastApiCallFailed = false; } } - private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { - if (Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) { - Utils.showToastShort(toastMessage); - } - if (ex != null) { - Logger.printInfo(() -> toastMessage, ex); + private static void handleConnectionError(@NonNull String toastMessage, + @Nullable Exception ex, + boolean showLongToast) { + if (!lastApiCallFailed && Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) { + if (showLongToast) { + Utils.showToastLong(toastMessage); + } else { + Utils.showToastShort(toastMessage); + } } + lastApiCallFailed = true; + + Logger.printInfo(() -> toastMessage, ex); } /** @@ -262,13 +291,15 @@ public class ReturnYouTubeDislikeApi { // fall thru to update statistics } } else { - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + // Unexpected response code. Most likely RYD is temporarily broken. + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); } - connection.disconnect(); // something went wrong, might as well disconnect - } catch (SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error - handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex); + connection.disconnect(); // Something went wrong, might as well disconnect. + } catch (SocketTimeoutException ex) { + handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false); } catch (IOException ex) { - handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex); + handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex, true); } catch (Exception ex) { // should never happen Logger.printException(() -> "Failed to fetch votes", ex, str("revanced_ryd_failure_generic", ex.getMessage())); @@ -309,12 +340,13 @@ public class ReturnYouTubeDislikeApi { String solution = solvePuzzle(challenge, difficulty); return confirmRegistration(userId, solution); } - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); connection.disconnect(); } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex, true); } catch (Exception ex) { Logger.printException(() -> "Failed to register user", ex); // should never happen } @@ -356,12 +388,14 @@ public class ReturnYouTubeDislikeApi { final String resultLog = result == null ? "(no response)" : result; Logger.printInfo(() -> "Failed to confirm registration for user: " + userId + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), + ex, true); } catch (Exception ex) { Logger.printException(() -> "Failed to confirm registration for user: " + userId + "solution: " + solution, ex); @@ -429,12 +463,13 @@ public class ReturnYouTubeDislikeApi { } Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote + " response code was: " + responseCode); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex, true); } catch (Exception ex) { // should never happen Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); @@ -477,12 +512,14 @@ public class ReturnYouTubeDislikeApi { final String resultLog = result == null ? "(no response)" : result; Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId + " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); - handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); connection.disconnect(); // something went wrong, might as well disconnect } catch (SocketTimeoutException ex) { - handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); } catch (IOException ex) { - handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), ex); + handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), + ex, true); } catch (Exception ex) { Logger.printException(() -> "Failed to confirm vote for video: " + videoId + " solution: " + solution, ex); // should never happen diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 07b6ddf2..bbf11fba 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -292,7 +292,7 @@ public class Settings extends BaseSettings { static { // region Migration - // region Migrate settings from old Preference categories into replacement "revanced_prefs" category. + // Migrate settings from old Preference categories into replacement "revanced_prefs" category. // This region must run before all other migration code. // The YT and RYD migration portion of this can be removed anytime, @@ -338,7 +338,6 @@ public class Settings extends BaseSettings { migrateFromOldPreferences(ytPrefs, setting, key); } } - // end region // Do _not_ delete this SB private user id migration property until sometime in 2024. @@ -346,16 +345,6 @@ public class Settings extends BaseSettings { // and more time should be given for users who rarely upgrade. migrateOldSettingToNew(DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID); - // This migration may need to remain here for a while. - // Older online guides will still reference using commas, - // and this code will automatically convert anything the user enters to newline format, - // and also migrate any imported older settings that using commas. - String componentsToFilter = Settings.CUSTOM_FILTER_STRINGS.get(); - if (componentsToFilter.contains(",")) { - Logger.printInfo(() -> "Migrating custom filter strings to new line format"); - Settings.CUSTOM_FILTER_STRINGS.save(componentsToFilter.replace(",", "\n")); - } - // endregion } } diff --git a/gradle.properties b/gradle.properties index 381af0f7..79d3bc10 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 1.2.1 +version = 1.3.0-dev.1