chore: Merge branch dev to main (#559)

This commit is contained in:
oSumAtrIX 2024-02-05 20:24:42 +01:00 committed by GitHub
commit 26345522a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 369 additions and 135 deletions

View File

@ -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) ## [1.2.1](https://github.com/ReVanced/revanced-integrations/compare/v1.2.0...v1.2.1) (2024-01-28)

View File

@ -97,7 +97,7 @@ public abstract class Setting<T> {
@NonNull @NonNull
private static List<Setting<?>> allLoadedSettingsSorted() { private static List<Setting<?>> allLoadedSettingsSorted() {
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key)); 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<T> {
/** /**
* Confirmation message to display, if the user tries to change the setting from the default value. * 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 @Nullable
public final StringRef userDialogMessage; public final StringRef userDialogMessage;
@ -206,10 +207,9 @@ public abstract class Setting<T> {
/** /**
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. * 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 <T> void migrateOldSettingToNew(@NonNull Setting<T> oldSetting, @NonNull Setting<T> newSetting) {
if (!oldSetting.isSetToDefault()) { if (!oldSetting.isSetToDefault()) {
Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting); Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting);
//noinspection unchecked
newSetting.save(oldSetting.value); newSetting.save(oldSetting.value);
oldSetting.resetToDefault(); oldSetting.resetToDefault();
} }

View File

@ -7,6 +7,8 @@ import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.preference.*; import android.preference.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BooleanSetting; import app.revanced.integrations.shared.settings.BooleanSetting;
@ -25,6 +27,13 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
*/ */
public static boolean settingImportInProgress; 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. * 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) { 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; showingUserDialogMessage = true;
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setTitle(str("revanced_settings_confirm_user_dialog_title")) .setTitle(confirmDialogTitle)
.setMessage(setting.userDialogMessage.toString()) .setMessage(setting.userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> { .setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) { if (setting.rebootApp) {
@ -201,12 +214,17 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
} }
public static void showRestartDialog(@NonNull final Context context) { public static void showRestartDialog(@NonNull final Context context) {
String positiveButton = str("revanced_settings_restart"); Utils.verifyOnMainThread();
if (restartDialogTitle == null) {
new AlertDialog.Builder(context).setMessage(str("revanced_settings_restart_title")) restartDialogTitle = str("revanced_settings_restart_title");
.setPositiveButton(positiveButton, (dialog, id) -> { }
Utils.restartApp(context); 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) .setNegativeButton(android.R.string.cancel, null)
.setCancelable(false) .setCancelable(false)
.show(); .show();

View File

@ -17,6 +17,12 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
protected void initialize() { protected void initialize() {
final var context = getContext(); 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); PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen); setPreferenceScreen(preferenceScreen);

View File

@ -231,47 +231,57 @@ public abstract class TrieSearch<T> {
} }
/** /**
* 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 searchText Text to search for patterns in.
* @param searchTextLength Length of the search text. * @param searchTextLength Length of the search text.
* @param searchTextIndex Current recursive search text index. Also, the end index of the current pattern match. * @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. * @return If any pattern matches, and it's associated callback halted the search.
*/ */
private boolean matches(T searchText, int searchTextLength, int searchTextIndex, int currentMatchLength, private static <T> boolean matches(final TrieNode<T> startNode, final T searchText, final int searchTextLength,
Object callbackParameter) { int searchTextIndex, final Object callbackParameter) {
if (leaf != null && leaf.matches(this, TrieNode<T> node = startNode;
searchText, searchTextLength, searchTextIndex, callbackParameter)) { int currentMatchLength = 0;
return true; // Leaf exists and it matched the search text.
} while (true) {
if (endOfPatternCallback != null) { TrieCompressedPath<T> leaf = node.leaf;
final int matchStartIndex = searchTextIndex - currentMatchLength; if (leaf != null && leaf.matches(node, searchText, searchTextLength, searchTextIndex, callbackParameter)) {
for (@Nullable TriePatternMatchedCallback<T> callback : endOfPatternCallback) { return true; // Leaf exists and it matched the search text.
if (callback == null) { }
return true; // No callback and all matches are valid. List<TriePatternMatchedCallback<T>> endOfPatternCallback = node.endOfPatternCallback;
} if (endOfPatternCallback != null) {
if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) { final int matchStartIndex = searchTextIndex - currentMatchLength;
return true; // Callback confirmed the match. for (@Nullable TriePatternMatchedCallback<T> 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.
}
} }
} }
} TrieNode<T>[] children = node.children;
if (children == null) { if (children == null) {
return false; // Reached a graph end point and there's no further patterns to search. return false; // Reached a graph end point and there's no further patterns to search.
} }
if (searchTextIndex == searchTextLength) { if (searchTextIndex == searchTextLength) {
return false; // Reached end of the search text and found no matches. return false; // Reached end of the search text and found no matches.
} }
final char character = getCharValue(searchText, searchTextIndex); // Use the start node to reduce VM method lookup, since all nodes are the same class type.
if (isInvalidRange(character)) { final char character = startNode.getCharValue(searchText, searchTextIndex);
return false; // Not an ASCII letter/number/symbol. final int arrayIndex = hashIndexForTableSize(children.length, character);
TrieNode<T> child = children[arrayIndex];
if (child == null || child.nodeValue != character) {
return false;
}
node = child;
searchTextIndex++;
currentMatchLength++;
} }
final int arrayIndex = hashIndexForTableSize(children.length, character);
TrieNode<T> 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<T> {
return false; // No patterns were added. return false; // No patterns were added.
} }
for (int i = startIndex; i < endIndex; i++) { 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; return false;
} }

View File

@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
import app.revanced.integrations.youtube.patches.components.ReturnYouTubeDislikeFilterPatch; import app.revanced.integrations.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike; 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.settings.Settings;
import app.revanced.integrations.youtube.shared.PlayerType; import app.revanced.integrations.youtube.shared.PlayerType;
import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Logger;
@ -21,7 +22,6 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import static app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote; import static app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote;
@ -70,17 +70,16 @@ public class ReturnYouTubeDislikePatch {
private static volatile boolean lithoShortsShouldUseCurrentData; 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 @Nullable
private static volatile String lastPrefetchedVideoId; private static volatile String lastPrefetchedVideoId;
public static void onRYDStatusChange(boolean rydEnabled) { public static void onRYDStatusChange(boolean rydEnabled) {
if (!rydEnabled) { ReturnYouTubeDislikeApi.resetRateLimits();
// Must remove all values to protect against using stale data // Must remove all values to protect against using stale data
// if the user enables RYD while a video is on screen. // if the user enables RYD while a video is on screen.
clearData(); clearData();
}
} }
private static void clearData() { private static void clearData() {
@ -240,10 +239,6 @@ public class ReturnYouTubeDislikePatch {
} }
replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original, replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original,
true, isRollingNumber); 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|")) { } else if (!isRollingNumber && conversionContextString.contains("|shorts_dislike_button.eml|")) {
// Litho Shorts player. // Litho Shorts player.
if (!Settings.RYD_SHORTS.get()) { if (!Settings.RYD_SHORTS.get()) {
@ -300,9 +295,10 @@ public class ReturnYouTubeDislikePatch {
@NonNull String original) { @NonNull String original) {
try { try {
CharSequence replacement = onLithoTextLoaded(conversionContext, original, true); CharSequence replacement = onLithoTextLoaded(conversionContext, original, true);
if (!replacement.toString().equals(original)) { String replacementString = replacement.toString();
if (!replacementString.equals(original)) {
rollingNumberSpan = replacement; rollingNumberSpan = replacement;
return replacement.toString(); return replacementString;
} // Else, the text was not a likes count but instead the view count or something else. } // Else, the text was not a likes count but instead the view count or something else.
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onRollingNumberLoaded failure", ex); Logger.printException(() -> "onRollingNumberLoaded failure", ex);
@ -348,9 +344,8 @@ public class ReturnYouTubeDislikePatch {
} else { } else {
view.setCompoundDrawables(separator, null, null, null); view.setCompoundDrawables(separator, null, null, null);
} }
// Liking/disliking can cause the span to grow in size, // Disliking can cause the span to grow in size, which is ok and is laid out correctly,
// 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.
// but if the user then undoes their action the layout will not remove the extra padding.
// Use a center alignment to take up any extra space. // Use a center alignment to take up any extra space.
view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
// Single line mode does not clip words if the span is larger than the view bounds. // Single line mode does not clip words if the span is larger than the view bounds.

View File

@ -4,7 +4,8 @@ import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class WideSearchbarPatch { 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;
} }
} }

View File

@ -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<CustomFilterGroup> 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<String, CustomFilterGroup> 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<CustomFilterGroup> 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);
}
}

View File

@ -18,7 +18,6 @@ public final class LayoutComponentsFilter extends Filter {
null, null,
"cell_description_body" "cell_description_body"
); );
private final CustomFilterGroup custom;
private static final ByteArrayFilterGroup mixPlaylists = new ByteArrayFilterGroup( private static final ByteArrayFilterGroup mixPlaylists = new ByteArrayFilterGroup(
Settings.HIDE_MIX_PLAYLISTS, Settings.HIDE_MIX_PLAYLISTS,
@ -68,11 +67,6 @@ public final class LayoutComponentsFilter extends Filter {
// Paths. // Paths.
custom = new CustomFilterGroup(
Settings.CUSTOM_FILTER,
Settings.CUSTOM_FILTER_STRINGS
);
final var communityPosts = new StringFilterGroup( final var communityPosts = new StringFilterGroup(
Settings.HIDE_COMMUNITY_POSTS, Settings.HIDE_COMMUNITY_POSTS,
"post_base_wrapper" "post_base_wrapper"
@ -226,7 +220,6 @@ public final class LayoutComponentsFilter extends Filter {
); );
addPathCallbacks( addPathCallbacks(
custom,
expandableMetadata, expandableMetadata,
inFeedSurvey, inFeedSurvey,
notifyMe, notifyMe,
@ -270,8 +263,7 @@ public final class LayoutComponentsFilter extends Filter {
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
if (matchedGroup != custom && exceptions.matches(path)) if (exceptions.matches(path)) return false; // Exceptions are not filtered.
return false; // Exceptions are not filtered.
// TODO: This also hides the feed Shorts shelf header // TODO: This also hides the feed Shorts shelf header
if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false; if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false;

View File

@ -18,7 +18,6 @@ import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BooleanSetting; import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.shared.settings.BaseSettings; 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.ByteTrieSearch;
import app.revanced.integrations.youtube.StringTrieSearch; import app.revanced.integrations.youtube.StringTrieSearch;
import app.revanced.integrations.youtube.TrieSearch; import app.revanced.integrations.youtube.TrieSearch;
@ -138,25 +137,6 @@ class StringFilterGroup extends FilterGroup<String> {
} }
} }
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 * If you have more than 1 filter patterns, then all instances of
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])}, * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},

View File

@ -151,7 +151,7 @@ public class ReturnYouTubeDislike {
private final Future<RYDVoteData> future; private final Future<RYDVoteData> future;
/** /**
* Time this instance and the future was created. * Time this instance and the fetch future was created.
*/ */
private final long timeFetched; 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. * 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. * It's unknown where YT gets the color from, and the values here are approximated by hand.
* Ideally, the color here would be the actual color YT uses at runtime. * Ideally, this would be the actual color YT uses at runtime.
* *
* Older versions before the 'Me' library tab use a slightly different color. * Older versions before the 'Me' library tab use a slightly different color.
* If spoofing was previously used and is now turned off, * 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() { private static int getSeparatorColor() {
if (IS_SPOOFING_TO_OLD_SEPARATOR_COLOR) { 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() { public static void clearAllUICaches() {
synchronized (fetchCache) { synchronized (fetchCache) {

View File

@ -62,7 +62,7 @@ public class ReturnYouTubeDislikeApi {
* How long to wait until API calls are resumed, if the API requested a back off. * 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. * 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. * 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. * 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. * 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. * @return True, if api rate limit is in effect.
*/ */
@ -193,25 +211,36 @@ public class ReturnYouTubeDislikeApi {
timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS; timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS;
fetchCallResponseTimeLast = responseTimeOfFetchCall; fetchCallResponseTimeLast = responseTimeOfFetchCall;
fetchCallNumberOfFailures++; fetchCallNumberOfFailures++;
lastApiCallFailed = true;
} else if (rateLimitHit) { } else if (rateLimitHit) {
Logger.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " Logger.printDebug(() -> "API rate limit was hit. Stopping API calls for the next "
+ BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds"); + BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds");
timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS; timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS;
numberOfRateLimitRequestsEncountered++; numberOfRateLimitRequestsEncountered++;
fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT; 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 { } else {
fetchCallResponseTimeLast = responseTimeOfFetchCall; fetchCallResponseTimeLast = responseTimeOfFetchCall;
lastApiCallFailed = false;
} }
} }
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { private static void handleConnectionError(@NonNull String toastMessage,
if (Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) { @Nullable Exception ex,
Utils.showToastShort(toastMessage); boolean showLongToast) {
} if (!lastApiCallFailed && Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) {
if (ex != null) { if (showLongToast) {
Logger.printInfo(() -> toastMessage, ex); 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 // fall thru to update statistics
} }
} else { } 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 connection.disconnect(); // Something went wrong, might as well disconnect.
} catch (SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error } catch (SocketTimeoutException ex) {
handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex); handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
// should never happen // should never happen
Logger.printException(() -> "Failed to fetch votes", ex, str("revanced_ryd_failure_generic", ex.getMessage())); 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); String solution = solvePuzzle(challenge, difficulty);
return confirmRegistration(userId, solution); 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(); connection.disconnect();
} catch (SocketTimeoutException ex) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
Logger.printException(() -> "Failed to register user", ex); // should never happen 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; final String resultLog = result == null ? "(no response)" : result;
Logger.printInfo(() -> "Failed to confirm registration for user: " + userId Logger.printInfo(() -> "Failed to confirm registration for user: " + userId
+ " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); + " 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 connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
Logger.printException(() -> "Failed to confirm registration for user: " + userId Logger.printException(() -> "Failed to confirm registration for user: " + userId
+ "solution: " + solution, ex); + "solution: " + solution, ex);
@ -429,12 +463,13 @@ public class ReturnYouTubeDislikeApi {
} }
Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote
+ " response code was: " + responseCode); + " 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 connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
// should never happen // should never happen
Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); 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; final String resultLog = result == null ? "(no response)" : result;
Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution + " responseCode: " + responseCode + " responseString: " + resultLog); + " 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 connection.disconnect(); // something went wrong, might as well disconnect
} catch (SocketTimeoutException ex) { } catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false);
} catch (IOException ex) { } 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) { } catch (Exception ex) {
Logger.printException(() -> "Failed to confirm vote for video: " + videoId Logger.printException(() -> "Failed to confirm vote for video: " + videoId
+ " solution: " + solution, ex); // should never happen + " solution: " + solution, ex); // should never happen

View File

@ -292,7 +292,7 @@ public class Settings extends BaseSettings {
static { static {
// region Migration // 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. // This region must run before all other migration code.
// The YT and RYD migration portion of this can be removed anytime, // 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); migrateFromOldPreferences(ytPrefs, setting, key);
} }
} }
// end region
// Do _not_ delete this SB private user id migration property until sometime in 2024. // 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. // and more time should be given for users who rarely upgrade.
migrateOldSettingToNew(DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID); 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 // endregion
} }
} }

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
android.useAndroidX = true android.useAndroidX = true
version = 1.2.1 version = 1.3.0-dev.1