From 4c81e96a74cfc49923238c4a294b59f36b5e6c36 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:29:46 +0400 Subject: [PATCH] feat(YouTube - Alternative thumbnails): Selectively enable for home / subscription / search (#593) Co-authored-by: oSumAtrIX --- .../shared/settings/EnumSetting.java | 101 +++++++++++ .../integrations/shared/settings/Setting.java | 1 + .../preference/SharedPrefCategory.java | 53 ++++-- .../patches/AlternativeThumbnailsPatch.java | 169 +++++++++++++----- .../youtube/patches/components/AdsFilter.java | 4 +- .../patches/components/ButtonsFilter.java | 8 +- .../patches/components/CustomFilter.java | 9 +- .../components/KeywordContentFilter.java | 4 +- .../components/LayoutComponentsFilter.java | 5 +- .../patches/components/LithoFilterPatch.java | 1 - .../ReturnYouTubeDislikeFilterPatch.java | 9 +- .../patches/components/ShortsFilter.java | 3 - .../youtube/settings/Settings.java | 21 ++- ...AlternativeThumbnailsStatusPreference.java | 84 --------- .../youtube/shared/NavigationBar.java | 32 ++-- 15 files changed, 314 insertions(+), 190 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java delete mode 100644 app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsStatusPreference.java diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java b/app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java new file mode 100644 index 00000000..20ef4821 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/shared/settings/EnumSetting.java @@ -0,0 +1,101 @@ +package app.revanced.integrations.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.integrations.shared.Logger; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Locale; +import java.util.Objects; + +/** + * If an Enum value is removed or changed, any saved or imported data using the + * non-existent value will be reverted to the default value + * (the event is logged, but no user error is displayed). + * + * All saved JSON text is converted to lowercase to keep the output less obnoxious. + */ +@SuppressWarnings("unused") +public class EnumSetting extends Setting { + public EnumSetting(String key, T defaultValue) { + super(key, defaultValue); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + public EnumSetting(String key, T defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + public EnumSetting(String key, T defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getEnum(key, defaultValue); + } + + @Override + protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException { + String enumName = json.getString(importExportKey); + try { + return getEnumFromString(enumName); + } catch (IllegalArgumentException ex) { + // Info level to allow removing enum values in the future without showing any user errors. + Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex); + return defaultValue; + } + } + + @Override + protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException { + // Use lowercase to keep the output less ugly. + json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH)); + } + + @NonNull + private T getEnumFromString(String enumName) { + //noinspection ConstantConditions + for (Enum value : defaultValue.getClass().getEnumConstants()) { + if (value.name().equalsIgnoreCase(enumName)) { + // noinspection unchecked + return (T) value; + } + } + throw new IllegalArgumentException("Unknown enum value: " + enumName); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = getEnumFromString(Objects.requireNonNull(newValue)); + } + + @Override + public void save(@NonNull T newValue) { + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + preferences.saveEnumAsString(key, newValue); + } + + @NonNull + @Override + public T get() { + return value; + } +} 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 9f6b3ae5..f5f7b890 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 @@ -324,6 +324,7 @@ public abstract class Setting { } /** + * @param importExportKey The JSON key. The JSONObject parameter will contain data for this key. * @return the value stored using the import/export key. Do not set any values in this method. */ protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException; diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java index 1a92e9f0..9c7fa45c 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/SharedPrefCategory.java @@ -32,17 +32,31 @@ public class SharedPrefCategory { private void removeConflictingPreferenceKeyValue(@NonNull String key) { Logger.printException(() -> "Found conflicting preference: " + key); - preferences.edit().remove(key).apply(); + removeKey(key); } private void saveObjectAsString(@NonNull String key, @Nullable Object value) { preferences.edit().putString(key, (value == null ? null : value.toString())).apply(); } + /** + * Removes any preference data type that has the specified key. + */ + public void removeKey(@NonNull String key) { + preferences.edit().remove(Objects.requireNonNull(key)).apply(); + } + public void saveBoolean(@NonNull String key, boolean value) { preferences.edit().putBoolean(key, value).apply(); } + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveEnumAsString(@NonNull String key, @Nullable Enum value) { + saveObjectAsString(key, value); + } + /** * @param value a NULL parameter removes the value from the preferences */ @@ -83,6 +97,28 @@ public class SharedPrefCategory { } } + @NonNull + public T getEnum(@NonNull String key, @NonNull T _default) { + Objects.requireNonNull(_default); + try { + String enumName = preferences.getString(key, null); + if (enumName != null) { + try { + // noinspection unchecked + return (T) Enum.valueOf(_default.getClass(), enumName); + } catch (IllegalArgumentException ex) { + // Info level to allow removing enum values in the future without showing any user errors. + Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName); + removeKey(key); + } + } + } catch (ClassCastException ex) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + } + return _default; + } + public boolean getBoolean(@NonNull String key, boolean _default) { try { return preferences.getBoolean(key, _default); @@ -100,17 +136,16 @@ public class SharedPrefCategory { if (value != null) { return Integer.valueOf(value); } - return _default; - } catch (ClassCastException ex) { + } catch (ClassCastException | NumberFormatException ex) { try { // Old data previously stored as primitive. return preferences.getInt(key, _default); } catch (ClassCastException ex2) { // Value stored is a completely different type (should never happen). removeConflictingPreferenceKeyValue(key); - return _default; } } + return _default; } @NonNull @@ -120,15 +155,14 @@ public class SharedPrefCategory { if (value != null) { return Long.valueOf(value); } - return _default; - } catch (ClassCastException ex) { + } catch (ClassCastException | NumberFormatException ex) { try { return preferences.getLong(key, _default); } catch (ClassCastException ex2) { removeConflictingPreferenceKeyValue(key); - return _default; } } + return _default; } @NonNull @@ -138,15 +172,14 @@ public class SharedPrefCategory { if (value != null) { return Float.valueOf(value); } - return _default; - } catch (ClassCastException ex) { + } catch (ClassCastException | NumberFormatException ex) { try { return preferences.getFloat(key, _default); } catch (ClassCastException ex2) { removeConflictingPreferenceKeyValue(key); - return _default; } } + return _default; } @NonNull diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java index a16d481a..1e0d2e1e 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/AlternativeThumbnailsPatch.java @@ -5,9 +5,15 @@ import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import app.revanced.integrations.shared.settings.BaseSettings; +import app.revanced.integrations.shared.settings.EnumSetting; +import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.shared.NavigationBar; +import app.revanced.integrations.youtube.shared.PlayerType; + import org.chromium.net.UrlRequest; import org.chromium.net.UrlResponseInfo; import org.chromium.net.impl.CronetUrlRequest; @@ -21,6 +27,12 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_HOME; +import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_LIBRARY; +import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_PLAYER; +import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SEARCH; +import static app.revanced.integrations.youtube.settings.Settings.ALT_THUMBNAIL_SUBSCRIPTIONS; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; /** * Alternative YouTube thumbnails. @@ -39,16 +51,72 @@ import static app.revanced.integrations.shared.StringRef.str; * If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail * is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution, * because a noticeable number of videos do not have hq720 and too much fail to load. - *

- * Ideas for improvements: - * - Selectively allow using original thumbnails in some situations, - * such as videos subscription feed, watch history, or in search results. - * - Save to a temporary file the video id's verified to have alt thumbnails. - * This would speed up loading the watch history and users saved playlists. */ @SuppressWarnings("unused") public final class AlternativeThumbnailsPatch { + // These must be class declarations if declared here, + // otherwise the app will not load due to cyclic initialization errors. + public static final class DeArrowAvailability implements Setting.Availability { + public static boolean usingDeArrowAnywhere() { + return ALT_THUMBNAIL_HOME.get().useDeArrow + || ALT_THUMBNAIL_SUBSCRIPTIONS.get().useDeArrow + || ALT_THUMBNAIL_LIBRARY.get().useDeArrow + || ALT_THUMBNAIL_PLAYER.get().useDeArrow + || ALT_THUMBNAIL_SEARCH.get().useDeArrow; + } + + @Override + public boolean isAvailable() { + return usingDeArrowAnywhere(); + } + } + + public static final class StillImagesAvailability implements Setting.Availability { + public static boolean usingStillImagesAnywhere() { + return ALT_THUMBNAIL_HOME.get().useStillImages + || ALT_THUMBNAIL_SUBSCRIPTIONS.get().useStillImages + || ALT_THUMBNAIL_LIBRARY.get().useStillImages + || ALT_THUMBNAIL_PLAYER.get().useStillImages + || ALT_THUMBNAIL_SEARCH.get().useStillImages; + } + + @Override + public boolean isAvailable() { + return usingStillImagesAnywhere(); + } + } + + public enum ThumbnailOption { + ORIGINAL(false, false), + DEARROW(true, false), + DEARROW_STILL_IMAGES(true, true), + STILL_IMAGES(false, true); + + final boolean useDeArrow; + final boolean useStillImages; + + ThumbnailOption(boolean useDeArrow, boolean useStillImages) { + this.useDeArrow = useDeArrow; + this.useStillImages = useStillImages; + } + } + + public enum ThumbnailStillTime { + BEGINNING(1), + MIDDLE(2), + END(3); + + /** + * The url alt image number. Such as the 2 in 'hq720_2.jpg' + */ + final int altImageNumber; + + ThumbnailStillTime(int altImageNumber) { + this.altImageNumber = altImageNumber; + } + } + private static final Uri dearrowApiUri; /** @@ -66,6 +134,11 @@ public final class AlternativeThumbnailsPatch { */ private static volatile long timeToResumeDeArrowAPICalls; + /** + * Used only for debug logging. + */ + private static volatile EnumSetting currentOptionSetting; + static { dearrowApiUri = validateSettings(); final int port = dearrowApiUri.getPort(); @@ -78,13 +151,6 @@ public final class AlternativeThumbnailsPatch { * Fix any bad imported data. */ private static Uri validateSettings() { - final int altThumbnailType = Settings.ALT_THUMBNAIL_STILLS_TIME.get(); - if (altThumbnailType < 1 || altThumbnailType > 3) { - Utils.showToastLong("Invalid Alternative still thumbnail type: " - + altThumbnailType + ". Using default"); - Settings.ALT_THUMBNAIL_STILLS_TIME.resetToDefault(); - } - Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get()); // Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made. String scheme = apiUri.getScheme(); @@ -96,12 +162,21 @@ public final class AlternativeThumbnailsPatch { return apiUri; } - private static boolean usingDeArrow() { - return Settings.ALT_THUMBNAIL_DEARROW.get(); - } - - private static boolean usingVideoStills() { - return Settings.ALT_THUMBNAIL_STILLS.get(); + private static EnumSetting optionSettingForCurrentNavigation() { + if (NavigationBar.isSearchBarActive()) { // Must check search first. + return ALT_THUMBNAIL_SEARCH; + } + if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { + return ALT_THUMBNAIL_PLAYER; + } + if (NavigationButton.HOME.isSelected()) { + return ALT_THUMBNAIL_HOME; + } + if (NavigationButton.SUBSCRIPTIONS.isSelected() || NavigationButton.NOTIFICATIONS.isSelected()) { + return ALT_THUMBNAIL_SUBSCRIPTIONS; + } + // A library tab variant is active. + return ALT_THUMBNAIL_LIBRARY; } /** @@ -179,9 +254,16 @@ public final class AlternativeThumbnailsPatch { */ public static String overrideImageURL(String originalUrl) { try { - final boolean usingDeArrow = usingDeArrow(); - final boolean usingVideoStills = usingVideoStills(); - if (!usingDeArrow && !usingVideoStills) { + EnumSetting optionSetting = optionSettingForCurrentNavigation(); + ThumbnailOption option = optionSetting.get(); + if (BaseSettings.DEBUG.get()) { + if (currentOptionSetting != optionSetting) { + currentOptionSetting = optionSetting; + Logger.printDebug(() -> "Changed to setting: " + optionSetting.key); + } + } + + if (option == ThumbnailOption.ORIGINAL) { return originalUrl; } @@ -200,14 +282,14 @@ public final class AlternativeThumbnailsPatch { String sanitizedReplacementUrl; final boolean includeTracking; - if (usingDeArrow && canUseDeArrowAPI()) { + if (option.useDeArrow && canUseDeArrowAPI()) { includeTracking = false; // Do not include view tracking parameters with API call. - final String fallbackUrl = usingVideoStills + final String fallbackUrl = option.useStillImages ? buildYoutubeVideoStillURL(decodedUrl, qualityToUse) : decodedUrl.sanitizedUrl; sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl); - } else if (usingVideoStills) { + } else if (option.useStillImages) { includeTracking = true; // Include view tracking parameters if present. sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse); } else { @@ -240,7 +322,7 @@ public final class AlternativeThumbnailsPatch { String url = responseInfo.getUrl(); - if (usingDeArrow() && urlIsDeArrow(url)) { + if (urlIsDeArrow(url)) { Logger.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode); if (statusCode == 304) { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304 @@ -250,7 +332,7 @@ public final class AlternativeThumbnailsPatch { return; } - if (usingVideoStills() && statusCode == 404) { + if (statusCode == 404) { // Fast alt thumbnails is enabled and the thumbnail is not available. // The video is: // - live stream @@ -294,15 +376,13 @@ public final class AlternativeThumbnailsPatch { @Nullable UrlResponseInfo responseInfo, IOException exception) { try { - if (usingDeArrow()) { - String url = ((CronetUrlRequest) request).getHookedUrl(); - if (urlIsDeArrow(url)) { - Logger.printDebug(() -> "handleCronetFailure, exception: " + exception); - final int statusCode = (responseInfo != null) - ? responseInfo.getHttpStatusCode() - : 0; - handleDeArrowError(url, statusCode); - } + String url = ((CronetUrlRequest) request).getHookedUrl(); + if (urlIsDeArrow(url)) { + Logger.printDebug(() -> "handleCronetFailure, exception: " + exception); + final int statusCode = (responseInfo != null) + ? responseInfo.getHttpStatusCode() + : 0; + handleDeArrowError(url, statusCode); } } catch (Exception ex) { Logger.printException(() -> "Callback failure error", ex); @@ -332,13 +412,13 @@ public final class AlternativeThumbnailsPatch { for (ThumbnailQuality quality : values()) { originalNameToEnum.put(quality.originalName, quality); - for (int i = 1; i <= 3; i++) { + for (ThumbnailStillTime time : ThumbnailStillTime.values()) { // 'custom' thumbnails set by the content creator. // These show up in place of regular thumbnails - // and seem to be limited to [1, 3] range. - originalNameToEnum.put(quality.originalName + "_custom_" + i, quality); + // and seem to be limited to the same [1, 3] range as the still captures. + originalNameToEnum.put(quality.originalName + "_custom_" + time.altImageNumber, quality); - altNameToEnum.put(quality.altImageName + i, quality); + altNameToEnum.put(quality.altImageName + time.altImageNumber, quality); } } } @@ -398,7 +478,7 @@ public final class AlternativeThumbnailsPatch { } String getAltImageNameToUse() { - return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get(); + return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get().altImageNumber; } } @@ -510,12 +590,11 @@ public final class AlternativeThumbnailsPatch { boolean imageFileFound; try { - Logger.printDebug(() -> "Verifying image: " + imageUrl); // This hooked code is running on a low priority thread, and it's slightly faster // to run the url connection thru the integrations thread pool which runs at the highest priority. final long start = System.currentTimeMillis(); imageFileFound = Utils.submitOnBackgroundThread(() -> { - final int connectionTimeoutMillis = 5000; + final int connectionTimeoutMillis = 10000; // 10 seconds. HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); connection.setConnectTimeout(connectionTimeoutMillis); connection.setReadTimeout(connectionTimeoutMillis); @@ -533,7 +612,7 @@ public final class AlternativeThumbnailsPatch { } return false; }).get(); - Logger.printDebug(() -> "Alt verification took: " + (System.currentTimeMillis() - start) + "ms"); + Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl); } catch (ExecutionException | InterruptedException ex) { Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex); imageFileFound = false; @@ -597,7 +676,7 @@ public final class AlternativeThumbnailsPatch { ? "" : fullUrl.substring(imageExtensionEndIndex); } - /** @noinspection SameParameterValue*/ + /** @noinspection SameParameterValue */ String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) { // Images could be upgraded to webp if they are not already, but this fails quite often, // especially for new videos uploaded in the last hour. diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java index 0c2233a4..22daf6dd 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/AdsFilter.java @@ -129,8 +129,8 @@ public final class AdsFilter extends Filter { } @Override - public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (exceptions.matches(path)) return false; diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ButtonsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ButtonsFilter.java index c92cba97..a78f9b5a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ButtonsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ButtonsFilter.java @@ -1,14 +1,10 @@ package app.revanced.integrations.youtube.patches.components; -import android.os.Build; - import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") -@RequiresApi(api = Build.VERSION_CODES.N) final class ButtonsFilter extends Filter { private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.eml"; @@ -89,8 +85,8 @@ final class ButtonsFilter extends Filter { } @Override - public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { // If the current matched group is the action bar group, // in case every filter group is enabled, hide the action bar. if (matchedGroup == actionBarGroup) { 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 index 3e62d040..51d86b54 100644 --- 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 @@ -10,7 +10,6 @@ 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; @@ -42,9 +41,9 @@ final class CustomFilter extends Filter { public static final String SYNTAX_BUFFER_SYMBOL = "$"; /** - * @return the parsed objects, or NULL if there was a parse error. + * @return the parsed objects */ - @Nullable + @NonNull @SuppressWarnings("ConstantConditions") static Collection parseCustomFilterGroups() { String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get(); @@ -147,8 +146,8 @@ final class CustomFilter extends Filter { } @Override - public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + 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) { diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java index 0cbc8f71..7858bb74 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/KeywordContentFilter.java @@ -256,8 +256,8 @@ final class KeywordContentFilter extends Filter { } @Override - public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (contentIndex != 0 && matchedGroup == startsWithFilter) { return false; } 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 cb8cd8b1..f32ad545 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 @@ -10,7 +10,6 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.youtube.StringTrieSearch; @SuppressWarnings("unused") -@RequiresApi(api = Build.VERSION_CODES.N) public final class LayoutComponentsFilter extends Filter { private final StringTrieSearch exceptions = new StringTrieSearch(); private static final StringTrieSearch mixPlaylistsExceptions = new StringTrieSearch(); @@ -265,8 +264,8 @@ public final class LayoutComponentsFilter extends Filter { } @Override - public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (matchedGroup == searchResultVideo) { if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) { return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); 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 21b0129a..e1bb9d67 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 @@ -383,7 +383,6 @@ abstract class Filter { */ final class DummyFilter extends Filter { } -@RequiresApi(api = Build.VERSION_CODES.N) @SuppressWarnings("unused") public final class LithoFilterPatch { /** diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java index 605b48be..927e4493 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java @@ -1,11 +1,8 @@ package app.revanced.integrations.youtube.patches.components; -import android.os.Build; - import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -29,7 +26,6 @@ import app.revanced.integrations.youtube.TrieSearch; * * Once a way to asynchronously update litho text is found, this strategy will no longer be needed. */ -@RequiresApi(api = Build.VERSION_CODES.N) public final class ReturnYouTubeDislikeFilterPatch extends Filter { /** @@ -53,6 +49,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter { /** * Injection point. */ + @SuppressWarnings("unused") public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) { try { if (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get()) { @@ -84,8 +81,8 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter { } @Override - public boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray); if (result.isFiltered()) { String matchedVideoId = findVideoId(protobufBufferArray); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java index 90a64f56..8c3dbf26 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java @@ -2,11 +2,9 @@ package app.revanced.integrations.youtube.patches.components; import static app.revanced.integrations.shared.Utils.hideViewUnderCondition; -import android.os.Build; import android.view.View; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; @@ -16,7 +14,6 @@ import app.revanced.integrations.youtube.shared.NavigationBar; import app.revanced.integrations.youtube.shared.PlayerType; @SuppressWarnings("unused") -@RequiresApi(api = Build.VERSION_CODES.N) public final class ShortsFilter extends Filter { public static PivotBar pivotBar; // Set by patch. private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; 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 4322f430..6d2b7224 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 @@ -3,6 +3,10 @@ package app.revanced.integrations.youtube.settings; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.settings.*; import app.revanced.integrations.shared.settings.preference.SharedPrefCategory; +import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.DeArrowAvailability; +import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability; +import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption; +import app.revanced.integrations.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime; import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; @@ -52,13 +56,16 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true); public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE); // Layout - public static final BooleanSetting ALT_THUMBNAIL_STILLS = new BooleanSetting("revanced_alt_thumbnail_stills", FALSE); - public static final IntegerSetting ALT_THUMBNAIL_STILLS_TIME = new IntegerSetting("revanced_alt_thumbnail_stills_time", 2, parent(ALT_THUMBNAIL_STILLS)); - public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, parent(ALT_THUMBNAIL_STILLS)); - public static final BooleanSetting ALT_THUMBNAIL_DEARROW = new BooleanSetting("revanced_alt_thumbnail_dearrow", FALSE); + public static final EnumSetting ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_LIBRARY = new EnumSetting<>("revanced_alt_thumbnail_library", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_PLAYER = new EnumSetting<>("revanced_alt_thumbnail_player", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_SEARCH = new EnumSetting<>("revanced_alt_thumbnail_search", ThumbnailOption.ORIGINAL); public static final StringSetting ALT_THUMBNAIL_DEARROW_API_URL = new StringSetting("revanced_alt_thumbnail_dearrow_api_url", - "https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, parent(ALT_THUMBNAIL_DEARROW)); - public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", TRUE, parent(ALT_THUMBNAIL_DEARROW)); + "https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, new DeArrowAvailability()); + public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", TRUE, new DeArrowAvailability()); + public static final EnumSetting ALT_THUMBNAIL_STILLS_TIME = new EnumSetting<>("revanced_alt_thumbnail_stills_time", ThumbnailStillTime.MIDDLE, new StillImagesAvailability()); + public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, new StillImagesAvailability()); public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE); public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER)); public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true); @@ -365,7 +372,7 @@ public class Settings extends BaseSettings { // Remove any previously saved announcement consumer (a random generated string). - Setting.preferences.saveString("revanced_announcement_consumer", null); + Setting.preferences.removeKey("revanced_announcement_consumer"); // Shorts if (DEPRECATED_HIDE_SHORTS.get()) { diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsStatusPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsStatusPreference.java deleted file mode 100644 index fc81cdc5..00000000 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/AlternativeThumbnailsStatusPreference.java +++ /dev/null @@ -1,84 +0,0 @@ -package app.revanced.integrations.youtube.settings.preference; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.Preference; -import android.preference.PreferenceManager; -import android.util.AttributeSet; -import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.shared.settings.Setting; -import app.revanced.integrations.youtube.settings.Settings; - -import static app.revanced.integrations.shared.StringRef.str; - -/** - * Shows what thumbnails will be used based on the current settings. - */ -@SuppressWarnings("unused") -public class AlternativeThumbnailsStatusPreference extends Preference { - - private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { - // Because this listener may run before the ReVanced settings fragment updates SettingsEnum, - // this could show the prior config and not the current. - // - // Push this call to the end of the main run queue, - // so all other listeners are done and SettingsEnum is up to date. - Utils.runOnMainThread(this::updateUI); - }; - - public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - public AlternativeThumbnailsStatusPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - public AlternativeThumbnailsStatusPreference(Context context) { - super(context); - } - - private void addChangeListener() { - Logger.printDebug(() -> "addChangeListener"); - Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); - } - - private void removeChangeListener() { - Logger.printDebug(() -> "removeChangeListener"); - Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); - } - - @Override - protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { - super.onAttachedToHierarchy(preferenceManager); - updateUI(); - addChangeListener(); - } - - @Override - protected void onPrepareForRemoval() { - super.onPrepareForRemoval(); - removeChangeListener(); - } - - private void updateUI() { - Logger.printDebug(() -> "updateUI"); - final boolean usingDeArrow = Settings.ALT_THUMBNAIL_DEARROW.get(); - final boolean usingVideoStills = Settings.ALT_THUMBNAIL_STILLS.get(); - - final String summaryTextKey; - if (usingDeArrow && usingVideoStills) { - summaryTextKey = "revanced_alt_thumbnail_about_status_dearrow_stills"; - } else if (usingDeArrow) { - summaryTextKey = "revanced_alt_thumbnail_about_status_dearrow"; - } else if (usingVideoStills) { - summaryTextKey = "revanced_alt_thumbnail_about_status_stills"; - } else { - summaryTextKey = "revanced_alt_thumbnail_about_status_disabled"; - } - - setSummary(str(summaryTextKey)); - } -} diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java index 53155a18..4cec2680 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java +++ b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java @@ -1,37 +1,37 @@ package app.revanced.integrations.youtube.shared; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE; + import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; + import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.settings.Settings; -import java.lang.ref.WeakReference; - -import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE; - @SuppressWarnings("unused") public final class NavigationBar { - private static volatile boolean searchbarIsActive; + + private static volatile WeakReference searchBarResultsRef = new WeakReference<>(null); /** * Injection point. */ public static void searchBarResultsViewLoaded(View searchbarResults) { - searchbarResults.getViewTreeObserver().addOnGlobalLayoutListener(() -> { - final boolean isActive = searchbarResults.getParent() != null; - - if (searchbarIsActive != isActive) { - searchbarIsActive = isActive; - Logger.printDebug(() -> "searchbarIsActive: " + isActive); - } - }); + searchBarResultsRef = new WeakReference<>(searchbarResults); } + /** + * @return If the search bar is on screen. + */ public static boolean isSearchBarActive() { - return searchbarIsActive; + View searchbarResults = searchBarResultsRef.get(); + return searchbarResults != null && searchbarResults.getParent() != null; } @@ -99,7 +99,7 @@ public final class NavigationBar { } /** @noinspection EmptyMethod*/ - private static void navigationTabCreatedCallback(NavigationBar.NavigationButton button, View tabView) { + private static void navigationTabCreatedCallback(NavigationButton button, View tabView) { // Code is added during patching. } @@ -117,7 +117,7 @@ public final class NavigationBar { * Notifications tab. Only present when * {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active. */ - ACTIVITY("TAB_ACTIVITY"), + NOTIFICATIONS("TAB_ACTIVITY"), /** * Library tab when the user is not logged in. */