From 61569ba111af82aaff60d11863bc57221a295fe8 Mon Sep 17 00:00:00 2001 From: Zain Date: Sat, 19 Oct 2024 19:26:27 +0700 Subject: [PATCH] feat(YouTube): Support versions `19.25` and `19.34` (#689) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com> --- .../revanced/integrations/shared/Utils.java | 36 +- .../AbstractPreferenceFragment.java | 6 + .../preference/ImportExportPreference.java | 2 + .../ResettableEditTextPreference.java | 4 + .../patches/BackgroundPlaybackPatch.java | 4 +- .../youtube/patches/ChangeStartPagePatch.java | 126 +++++- .../patches/HidePlayerButtonsPatch.java | 38 +- .../youtube/patches/MiniplayerPatch.java | 196 ++++++++- .../patches/ReturnYouTubeDislikePatch.java | 3 + .../youtube/patches/SlideToSeekPatch.java | 8 +- .../youtube/patches/VersionCheckPatch.java | 10 + .../youtube/patches/VideoInformation.java | 18 +- .../youtube/patches/components/Filter.java | 90 ++++ .../patches/components/FilterGroup.java | 214 ++++++++++ .../patches/components/FilterGroupList.java | 85 ++++ .../components/LayoutComponentsFilter.java | 3 +- .../patches/components/LithoFilterPatch.java | 394 +----------------- .../patches/components/ShortsFilter.java | 70 +++- .../patches/theme/SeekbarColorPatch.java | 64 ++- .../youtube/requests/Requester.java | 2 +- .../ReturnYouTubeDislike.java | 7 +- .../youtube/settings/Settings.java | 40 +- .../SponsorBlockPreferenceFragment.java | 2 + .../youtube/shared/NavigationBar.java | 77 ++-- .../SegmentCategoryListPreference.java | 3 + .../ui/CreateSegmentButtonController.java | 4 +- .../ui/VotingButtonController.java | 4 +- 27 files changed, 989 insertions(+), 521 deletions(-) create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java create mode 100644 app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java index 0192c57190..c00886a317 100644 --- a/app/src/main/java/app/revanced/integrations/shared/Utils.java +++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java @@ -1,10 +1,7 @@ package app.revanced.integrations.shared; import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; +import android.app.*; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -268,6 +265,20 @@ public interface MatchFilter { boolean matches(T object); } + /** + * Includes sub children. + * + * @noinspection unchecked + */ + public static R getChildViewByResourceName(@NonNull View view, @NonNull String str) { + var child = view.findViewById(Utils.getResourceIdentifier(str, "id")); + if (child != null) { + return (R) child; + } + + throw new IllegalArgumentException("View with resource name '" + str + "' not found"); + } + /** * @param searchRecursively If children ViewGroups should also be * recursively searched using depth first search. @@ -710,4 +721,21 @@ public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { pref.setOrder(order); } } + + /** + * If {@link Fragment} uses [Android library] rather than [AndroidX library], + * the Dialog theme corresponding to [Android library] should be used. + *

+ * If not, the following issues will occur: + * ReVanced/revanced-patches#3061 + *

+ * To prevent these issues, apply the Dialog theme corresponding to [Android library]. + */ + public static void setEditTextDialogTheme(AlertDialog.Builder builder) { + final int editTextDialogStyle = getResourceIdentifier( + "revanced_edit_text_dialog_style", "style"); + if (editTextDialogStyle != 0) { + builder.getContext().setTheme(editTextDialogStyle); + } + } } 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 b7fa68ac69..e6d07ccde1 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 @@ -11,6 +11,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; import app.revanced.integrations.shared.settings.BooleanSetting; import app.revanced.integrations.shared.settings.Setting; @@ -141,8 +142,13 @@ private void updatePreferenceScreen(@NonNull PreferenceScreen screen, } else if (pref.hasKey()) { String key = pref.getKey(); Setting setting = Setting.getSettingFromPath(key); + if (setting != null) { updatePreference(pref, setting, syncSettingValue, applySettingToPreference); + } else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference + || pref instanceof EditTextPreference || pref instanceof ListPreference)) { + // Probably a typo in the patches preference declaration. + Logger.printException(() -> "Preference key has no setting: " + key); } } } diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java index 5c8e7c9ba9..1b86c3cde2 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java @@ -66,6 +66,8 @@ public boolean onPreferenceClick(Preference preference) { @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { try { + Utils.setEditTextDialogTheme(builder); + // Show the user the settings in JSON format. builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { Utils.setClipboard(getEditText().getText().toString()); diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java index 4cf1f27795..b62331fec9 100644 --- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java +++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java @@ -7,6 +7,8 @@ import android.util.AttributeSet; import android.widget.Button; import android.widget.EditText; + +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.shared.settings.Setting; import app.revanced.integrations.shared.Logger; @@ -33,6 +35,8 @@ public ResettableEditTextPreference(Context context) { @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { super.onPrepareDialogBuilder(builder); + Utils.setEditTextDialogTheme(builder); + Setting setting = Setting.getSettingFromPath(getKey()); if (setting != null) { builder.setNeutralButton(str("revanced_settings_reset"), null); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java index e6e449a0cb..d84813d014 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java @@ -8,7 +8,9 @@ public class BackgroundPlaybackPatch { /** * Injection point. */ - public static boolean playbackIsNotShort() { + public static boolean allowBackgroundPlayback(boolean original) { + if (original) return true; + // Steps to verify most edge cases: // 1. Open a regular video // 2. Minimize app (PIP should appear) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java index f31af61b1b..d87dca10d3 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java @@ -1,21 +1,129 @@ package app.revanced.integrations.youtube.patches; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + import android.content.Intent; -import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class ChangeStartPagePatch { - public static void changeIntent(final Intent intent) { - final var startPage = Settings.START_PAGE.get(); - if (startPage.isEmpty()) return; - Logger.printDebug(() -> "Changing start page to " + startPage); + public enum StartPage { + /** + * Unmodified type, and same as un-patched. + */ + ORIGINAL("", null), + + /** + * Browse id. + */ + BROWSE("FEguide_builder", TRUE), + EXPLORE("FEexplore", TRUE), + HISTORY("FEhistory", TRUE), + LIBRARY("FElibrary", TRUE), + MOVIE("FEstorefront", TRUE), + SUBSCRIPTIONS("FEsubscriptions", TRUE), + TRENDING("FEtrending", TRUE), + + /** + * Channel id, this can be used as a browseId. + */ + GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE), + LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE), + MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE), + SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE), + + /** + * Playlist id, this can be used as a browseId. + */ + LIKED_VIDEO("VLLL", TRUE), + WATCH_LATER("VLWL", TRUE), + + /** + * Intent action. + */ + SEARCH("com.google.android.youtube.action.open.search", FALSE), + SHORTS("com.google.android.youtube.action.open.shorts", FALSE); + + @Nullable + final Boolean isBrowseId; + + @NonNull + final String id; + + StartPage(@NonNull String id, @Nullable Boolean isBrowseId) { + this.id = id; + this.isBrowseId = isBrowseId; + } + + private boolean isBrowseId() { + return TRUE.equals(isBrowseId); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean isIntentAction() { + return FALSE.equals(isBrowseId); + } + } + + /** + * Intent action when YouTube is cold started from the launcher. + *

+ * If you don't check this, the hooking will also apply in the following cases: + * Case 1. The user clicked Shorts button on the YouTube shortcut. + * Case 2. The user clicked Shorts button on the YouTube widget. + * In this case, instead of opening Shorts, the start page specified by the user is opened. + */ + private static final String ACTION_MAIN = "android.intent.action.MAIN"; + + private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get(); + + /** + * There is an issue where the back button on the toolbar doesn't work properly. + * As a workaround for this issue, instead of overriding the browserId multiple times, just override it once. + */ + private static boolean appLaunched = false; + + public static String overrideBrowseId(@NonNull String original) { + if (!START_PAGE.isBrowseId()) { + return original; + } + + if (appLaunched) { + Logger.printDebug(() -> "Ignore override browseId as the app already launched"); + return original; + } + appLaunched = true; + + Logger.printDebug(() -> "Changing browseId to " + START_PAGE.id); + return START_PAGE.id; + } + + public static void overrideIntentAction(@NonNull Intent intent) { + if (!START_PAGE.isIntentAction()) { + return; + } + + if (!ACTION_MAIN.equals(intent.getAction())) { + Logger.printDebug(() -> "Ignore override intent action" + + " as the current activity is not the entry point of the application"); + return; + } + + if (appLaunched) { + Logger.printDebug(() -> "Ignore override intent action as the app already launched"); + return; + } + appLaunched = true; - if (startPage.startsWith("www")) - intent.setData(Uri.parse(startPage)); - else - intent.setAction("com.google.android.youtube.action." + startPage); + final String intentAction = START_PAGE.id; + Logger.printDebug(() -> "Changing intent action to " + intentAction); + intent.setAction(intentAction); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java index aa501280b3..bc876cc327 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java @@ -1,17 +1,47 @@ package app.revanced.integrations.youtube.patches; +import android.view.View; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class HidePlayerButtonsPatch { + private static final boolean HIDE_PLAYER_BUTTONS_ENABLED = Settings.HIDE_PLAYER_BUTTONS.get(); + + private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID = + Utils.getResourceIdentifier("player_control_previous_button_touch_area", "id"); + + private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = + Utils.getResourceIdentifier("player_control_next_button_touch_area", "id"); + /** * Injection point. */ - public static boolean previousOrNextButtonIsVisible(boolean previousOrNextButtonVisible) { - if (Settings.HIDE_PLAYER_BUTTONS.get()) { - return false; + public static void hidePreviousNextButtons(View parentView) { + if (!HIDE_PLAYER_BUTTONS_ENABLED) { + return; + } + + // Must use a deferred call to main thread to hide the button. + // Otherwise the layout crashes if set to hidden now. + Utils.runOnMainThread(() -> { + hideView(parentView, PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID); + hideView(parentView, PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID); + }); + } + + private static void hideView(View parentView, int resourceId) { + View nextPreviousButton = parentView.findViewById(resourceId); + + if (nextPreviousButton == null) { + Logger.printException(() -> "Could not find player previous/next button"); + return; } - return previousOrNextButtonVisible; + + Logger.printDebug(() -> "Hiding previous/next button"); + Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java index 22ea9f1017..9af9fd5e8d 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java @@ -2,20 +2,22 @@ import static app.revanced.integrations.shared.StringRef.str; import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import static app.revanced.integrations.youtube.patches.VersionCheckPatch.*; +import android.util.DisplayMetrics; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.Nullable; 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; -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "SpellCheckingInspection"}) public final class MiniplayerPatch { /** @@ -28,7 +30,12 @@ public enum MiniplayerType { TABLET(true, null), MODERN_1(null, 1), MODERN_2(null, 2), - MODERN_3(null, 3); + MODERN_3(null, 3), + /** + * Half broken miniplayer, that might be work in progress or left over abandoned code. + * Can force this type by editing the import/export settings. + */ + MODERN_4(null, 4); /** * Legacy tablet hook value. @@ -52,6 +59,35 @@ public boolean isModern() { } } + private static final int MINIPLAYER_SIZE; + + static { + // YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size. + DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics(); + final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density); + + // YT seems to use a minimum height to calculate the minimum miniplayer width based on the video. + // 170 seems to be the smallest that can be used and using less makes no difference. + final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works. + final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding. + // Round down to the nearest 5 pixels, to keep any error toasts easier to read. + final int WIDTH_DIP_MAX = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5); + Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX); + + int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get(); + + if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) { + Utils.showToastLong(str("revanced_miniplayer_width_dip_invalid_toast", + WIDTH_DIP_MIN, WIDTH_DIP_MAX)); + + // Instead of resetting, clamp the size at the bounds. + dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX)); + Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth); + } + + MINIPLAYER_SIZE = dipWidth; + } + /** * Modern subtitle overlay for {@link MiniplayerType#MODERN_2}. * Resource is not present in older targets, and this field will be zero. @@ -61,8 +97,21 @@ public boolean isModern() { private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get(); + /** + * Cannot turn off double tap with modern 2 or 3 with later targets, + * as forcing it off breakings tapping the miniplayer. + */ + private static final boolean DOUBLE_TAP_ACTION_ENABLED = + // 19.29+ is very broken if double tap is not enabled. + IS_19_29_OR_GREATER || + (CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get()); + + private static final boolean DRAG_AND_DROP_ENABLED = + CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + private static final boolean HIDE_EXPAND_CLOSE_ENABLED = - (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get(); + Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get() + && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.isAvailable(); private static final boolean HIDE_SUBTEXT_ENABLED = (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get(); @@ -70,8 +119,29 @@ public boolean isModern() { private static final boolean HIDE_REWIND_FORWARD_ENABLED = CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get(); + private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED = + Settings.MINIPLAYER_ROUNDED_CORNERS.get(); + + /** + * Remove a broken and always present subtitle text that is only + * present with {@link MiniplayerType#MODERN_2}. Bug was fixed in 19.21. + */ + private static final boolean HIDE_BROKEN_MODERN_2_SUBTITLE = + CURRENT_TYPE == MODERN_2 && !IS_19_21_OR_GREATER; + private static final int OPACITY_LEVEL; + public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + MiniplayerType type = Settings.MINIPLAYER_TYPE.get(); + return (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3)) + || (!IS_19_26_OR_GREATER && type == MODERN_1 + && !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get()) + || (IS_19_29_OR_GREATER && type == MODERN_3); + } + } + static { int opacity = Settings.MINIPLAYER_OPACITY.get(); @@ -122,6 +192,90 @@ public static void adjustMiniplayerOpacity(ImageView view) { } } + /** + * Injection point. + */ + public static boolean getModernFeatureFlagsActiveOverride(boolean original) { + if (original) Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original); + + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return CURRENT_TYPE.isModern(); + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDoubleTapAction(boolean original) { + if (original) Logger.printDebug(() -> "enableMiniplayerDoubleTapAction original: " + true); + + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return DOUBLE_TAP_ACTION_ENABLED; + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDragAndDrop(boolean original) { + if (original) Logger.printDebug(() -> "enableMiniplayerDragAndDrop original: " + true); + + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return DRAG_AND_DROP_ENABLED; + } + + + /** + * Injection point. + */ + public static boolean setRoundedCorners(boolean original) { + if (original) Logger.printDebug(() -> "setRoundedCorners original: " + true); + + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_ROUNDED_CORNERS_ENABLED; + } + + return original; + } + + /** + * Injection point. + */ + public static int setMiniplayerDefaultSize(int original) { + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_SIZE; + } + + return original; + } + + /** + * Injection point. + */ + public static float setMovementBoundFactor(float original) { + // Not clear if customizing this is useful or not. + // So for now just log this and use the original value. + if (original != 1.0) Logger.printDebug(() -> "setMovementBoundFactor original: " + original); + + return original; + } + + /** + * Injection point. + */ + public static boolean setDropShadow(boolean original) { + if (original) Logger.printDebug(() -> "setViewElevation original: " + true); + + return original; + } + /** * Injection point. */ @@ -140,27 +294,35 @@ public static void hideMiniplayerRewindForward(ImageView view) { * Injection point. */ public static void hideMiniplayerSubTexts(View view) { - // Different subviews are passed in, but only TextView and layouts are of interest here. - final boolean hideView = HIDE_SUBTEXT_ENABLED && (view instanceof TextView || view instanceof LinearLayout); - Utils.hideViewByRemovingFromParentUnderCondition(hideView, view); + try { + // Different subviews are passed in, but only TextView is of interest here. + if (HIDE_SUBTEXT_ENABLED && view instanceof TextView) { + Logger.printDebug(() -> "Hiding subtext view"); + Utils.hideViewByRemovingFromParentUnderCondition(true, view); + } + } catch (Exception ex) { + Logger.printException(() -> "hideMiniplayerSubTexts failure", ex); + } } /** * Injection point. */ public static void playerOverlayGroupCreated(View group) { - // Modern 2 has an half broken subtitle that is always present. - // Always hide it to make the miniplayer mostly usable. - if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { - if (group instanceof ViewGroup) { - View subtitleText = Utils.getChildView((ViewGroup) group, true, - view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); - - if (subtitleText != null) { - subtitleText.setVisibility(View.GONE); - Logger.printDebug(() -> "Modern overlay subtitle view set to hidden"); + try { + if (HIDE_BROKEN_MODERN_2_SUBTITLE && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { + if (group instanceof ViewGroup) { + View subtitleText = Utils.getChildView((ViewGroup) group, true, + view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); + + if (subtitleText != null) { + subtitleText.setVisibility(View.GONE); + Logger.printDebug(() -> "Modern overlay subtitle view set to hidden"); + } } } + } catch (Exception ex) { + Logger.printException(() -> "playerOverlayGroupCreated failure", ex); } } } 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 ec52ef076f..3a47ef52cc 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 @@ -699,10 +699,12 @@ public static void sendVote(int vote) { if (!Settings.RYD_ENABLED.get()) { return; } + final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized(); if (isNoneHiddenOrMinimized && !Settings.RYD_SHORTS.get()) { return; } + ReturnYouTubeDislike videoData = currentVideoData; if (videoData == null) { Logger.printDebug(() -> "Cannot send vote, as current video data is null"); @@ -723,6 +725,7 @@ public static void sendVote(int vote) { return; } } + Logger.printException(() -> "Unknown vote type: " + vote); } catch (Exception ex) { Logger.printException(() -> "sendVote failure", ex); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java index 5a6f56290c..7d6b209012 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java @@ -4,7 +4,11 @@ @SuppressWarnings("unused") public final class SlideToSeekPatch { - public static boolean isSlideToSeekDisabled() { - return !Settings.SLIDE_TO_SEEK.get(); + private static final Boolean SLIDE_TO_SEEK_DISABLED = !Settings.SLIDE_TO_SEEK.get(); + + public static boolean isSlideToSeekDisabled(boolean isDisabled) { + if (!isDisabled) return isDisabled; + + return SLIDE_TO_SEEK_DISABLED; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java new file mode 100644 index 0000000000..4ede010158 --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java @@ -0,0 +1,10 @@ +package app.revanced.integrations.youtube.patches; + +import app.revanced.integrations.shared.Utils; + +public class VersionCheckPatch { + public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0; + public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0; + public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0; + public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0; +} diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java index 0d7a29aa66..9df12b57d4 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java @@ -18,7 +18,7 @@ public final class VideoInformation { public interface PlaybackController { // Methods are added to YT classes during patching. boolean seekTo(long videoTime); - boolean seekToRelative(long videoTimeOffset); + void seekToRelative(long videoTimeOffset); } private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; @@ -229,21 +229,19 @@ public static boolean seekTo(final long seekTime) { /** * Seeks a relative amount. Should always be used over {@link #seekTo(long)} * when the desired seek time is an offset of the current time. - * - * @noinspection UnusedReturnValue */ - public static boolean seekToRelative(long seekTime) { + public static void seekToRelative(long seekTime) { Utils.verifyOnMainThread(); try { Logger.printDebug(() -> "Seeking relative to: " + seekTime); - // Try regular playback controller first, and it will not succeed if casting. + // 19.39+ does not have a boolean return type for relative seek. + // But can call both methods and it works correctly for both situations. PlaybackController controller = playerControllerRef.get(); if (controller == null) { Logger.printDebug(() -> "Cannot seek relative as player controller is null"); } else { - if (controller.seekToRelative(seekTime)) return true; - Logger.printDebug(() -> "seekToRelative did not succeeded. Trying MXD."); + controller.seekToRelative(seekTime); } // Adjust the fine adjustment function so it's at least 1 second before/after. @@ -258,13 +256,11 @@ public static boolean seekToRelative(long seekTime) { controller = mdxPlayerDirectorRef.get(); if (controller == null) { Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null"); - return false; + } else { + controller.seekToRelative(adjustedSeekTime); } - - return controller.seekToRelative(adjustedSeekTime); } catch (Exception ex) { Logger.printException(() -> "Failed to seek relative", ex); - return false; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java new file mode 100644 index 0000000000..ed1c56c92f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java @@ -0,0 +1,90 @@ +package app.revanced.integrations.youtube.patches.components; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.settings.BaseSettings; + +/** + * Filters litho based components. + * + * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} + * and {@link #addPathCallbacks(StringFilterGroup...)}. + * + * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to + * either an identifier or a path. + * Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) + * or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern). + * + * All callbacks must be registered before the constructor completes. + */ +abstract class Filter { + + public enum FilterContentType { + IDENTIFIER, + PATH, + PROTOBUFFER + } + + /** + * Identifier callbacks. Do not add to this instance, + * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}. + */ + protected final List identifierCallbacks = new ArrayList<>(); + /** + * Path callbacks. Do not add to this instance, + * and instead use {@link #addPathCallbacks(StringFilterGroup...)}. + */ + protected final List pathCallbacks = new ArrayList<>(); + + /** + * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addIdentifierCallbacks(StringFilterGroup... groups) { + identifierCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addPathCallbacks(StringFilterGroup... groups) { + pathCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Called after an enabled filter has been matched. + * Default implementation is to always filter the matched component and log the action. + * Subclasses can perform additional or different checks if needed. + *

+ * If the content is to be filtered, subclasses should always + * call this method (and never return a plain 'true'). + * That way the logs will always show when a component was filtered and which filter hide it. + *

+ * Method is called off the main thread. + * + * @param matchedGroup The actual filter that matched. + * @param contentType The type of content matched. + * @param contentIndex Matched index of the identifier or path. + * @return True if the litho component should be filtered out. + */ + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (BaseSettings.DEBUG.get()) { + String filterSimpleName = getClass().getSimpleName(); + if (contentType == FilterContentType.IDENTIFIER) { + Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier); + } else { + Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path); + } + } + return true; + } +} + diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java new file mode 100644 index 0000000000..5cc101593a --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java @@ -0,0 +1,214 @@ +package app.revanced.integrations.youtube.patches.components; + +import androidx.annotation.NonNull; + +import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.settings.BooleanSetting; +import app.revanced.integrations.youtube.ByteTrieSearch; + +abstract class FilterGroup { + final static class FilterGroupResult { + private BooleanSetting setting; + private int matchedIndex; + private int matchedLength; + // In the future it might be useful to include which pattern matched, + // but for now that is not needed. + + FilterGroupResult() { + this(null, -1, 0); + } + + FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) { + setValues(setting, matchedIndex, matchedLength); + } + + public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) { + this.setting = setting; + this.matchedIndex = matchedIndex; + this.matchedLength = matchedLength; + } + + /** + * A null value if the group has no setting, + * or if no match is returned from {@link FilterGroupList#check(Object)}. + */ + public BooleanSetting getSetting() { + return setting; + } + + public boolean isFiltered() { + return matchedIndex >= 0; + } + + /** + * Matched index of first pattern that matched, or -1 if nothing matched. + */ + public int getMatchedIndex() { + return matchedIndex; + } + + /** + * Length of the matched filter pattern. + */ + public int getMatchedLength() { + return matchedLength; + } + } + + protected final BooleanSetting setting; + protected final T[] filters; + + /** + * Initialize a new filter group. + * + * @param setting The associated setting. + * @param filters The filters. + */ + @SafeVarargs + public FilterGroup(final BooleanSetting setting, final T... filters) { + this.setting = setting; + this.filters = filters; + if (filters.length == 0) { + throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)"); + } + } + + public boolean isEnabled() { + return setting == null || setting.get(); + } + + /** + * @return If {@link FilterGroupList} should include this group when searching. + * By default, all filters are included except non enabled settings that require reboot. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean includeInSearch() { + return isEnabled() || !setting.rebootApp; + } + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting); + } + + public abstract FilterGroupResult check(final T stack); +} + +class StringFilterGroup extends FilterGroup { + + public StringFilterGroup(final BooleanSetting setting, final String... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult check(final String string) { + int matchedIndex = -1; + int matchedLength = 0; + if (isEnabled()) { + for (String pattern : filters) { + if (!string.isEmpty()) { + final int indexOf = string.indexOf(pattern); + if (indexOf >= 0) { + matchedIndex = indexOf; + matchedLength = pattern.length(); + break; + } + } + } + } + return new FilterGroupResult(setting, matchedIndex, matchedLength); + } +} + +/** + * If you have more than 1 filter patterns, then all instances of + * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])}, + * which uses a prefix tree to give better performance. + */ +class ByteArrayFilterGroup extends FilterGroup { + + private volatile int[][] failurePatterns; + + // Modified implementation from https://stackoverflow.com/a/1507813 + private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) { + // Finds the first occurrence of the pattern in the byte array using + // KMP matching algorithm. + int patternLength = pattern.length; + for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1]; + } + if (pattern[j] == data[i]) { + j++; + } + if (j == patternLength) { + return i - patternLength + 1; + } + } + return -1; + } + + private static int[] createFailurePattern(byte[] pattern) { + // Computes the failure function using a boot-strapping process, + // where the pattern is matched against itself. + final int patternLength = pattern.length; + final int[] failure = new int[patternLength]; + + for (int i = 1, j = 0; i < patternLength; i++) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1]; + } + if (pattern[j] == pattern[i]) { + j++; + } + failure[i] = j; + } + return failure; + } + + public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) { + super(setting, filters); + } + + /** + * Converts the Strings into byte arrays. Used to search for text in binary data. + */ + public ByteArrayFilterGroup(BooleanSetting setting, String... filters) { + super(setting, ByteTrieSearch.convertStringsToBytes(filters)); + } + + private synchronized void buildFailurePatterns() { + if (failurePatterns != null) return; // Thread race and another thread already initialized the search. + Logger.printDebug(() -> "Building failure array for: " + this); + int[][] failurePatterns = new int[filters.length][]; + int i = 0; + for (byte[] pattern : filters) { + failurePatterns[i++] = createFailurePattern(pattern); + } + this.failurePatterns = failurePatterns; // Must set after initialization finishes. + } + + @Override + public FilterGroupResult check(final byte[] bytes) { + int matchedLength = 0; + int matchedIndex = -1; + if (isEnabled()) { + int[][] failures = failurePatterns; + if (failures == null) { + buildFailurePatterns(); // Lazy load. + failures = failurePatterns; + } + for (int i = 0, length = filters.length; i < length; i++) { + byte[] filter = filters[i]; + matchedIndex = indexOf(bytes, filter, failures[i]); + if (matchedIndex >= 0) { + matchedLength = filter.length; + break; + } + } + } + return new FilterGroupResult(setting, matchedIndex, matchedLength); + } +} + diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java new file mode 100644 index 0000000000..9babba0e2f --- /dev/null +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java @@ -0,0 +1,85 @@ +package app.revanced.integrations.youtube.patches.components; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.util.*; +import java.util.function.Consumer; + +import app.revanced.integrations.youtube.ByteTrieSearch; +import app.revanced.integrations.youtube.StringTrieSearch; +import app.revanced.integrations.youtube.TrieSearch; + +abstract class FilterGroupList> implements Iterable { + + private final List filterGroups = new ArrayList<>(); + private final TrieSearch search = createSearchGraph(); + + @SafeVarargs + protected final void addAll(final T... groups) { + filterGroups.addAll(Arrays.asList(groups)); + + for (T group : groups) { + if (!group.includeInSearch()) { + continue; + } + for (V pattern : group.filters) { + search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + if (group.isEnabled()) { + FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; + result.setValues(group.setting, matchedStartIndex, matchedLength); + return true; + } + return false; + }); + } + } + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void forEach(@NonNull Consumer action) { + filterGroups.forEach(action); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @NonNull + @Override + public Spliterator spliterator() { + return filterGroups.spliterator(); + } + + protected FilterGroup.FilterGroupResult check(V stack) { + FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); + search.matches(stack, result); + return result; + + } + + protected abstract TrieSearch createSearchGraph(); +} + +final class StringFilterGroupList extends FilterGroupList { + protected StringTrieSearch createSearchGraph() { + return new StringTrieSearch(); + } +} + +/** + * If searching for a single byte pattern, then it is slightly better to use + * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster + * than a prefix tree to search for only 1 pattern. + */ +final class ByteArrayFilterGroupList extends FilterGroupList { + protected ByteTrieSearch createSearchGraph() { + return new ByteTrieSearch(); + } +} \ 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 9e3ed7db5e..d0026ecb15 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 @@ -423,7 +423,6 @@ private static boolean hideShelves() { // Check navigation button last. // Only filter if the library tab is not selected. // This check is important as the shelf layout is used for the library tab playlists. - NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); - return selectedNavButton != null && !selectedNavButton.isLibraryOrYouTab(); + return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY; } } 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 a7c9972004..5664ea8d7f 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 @@ -1,389 +1,15 @@ package app.revanced.integrations.youtube.patches.components; -import android.os.Build; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; import java.util.List; -import java.util.Spliterator; -import java.util.function.Consumer; import app.revanced.integrations.shared.Logger; -import app.revanced.integrations.shared.settings.BooleanSetting; -import app.revanced.integrations.shared.settings.BaseSettings; -import app.revanced.integrations.youtube.ByteTrieSearch; import app.revanced.integrations.youtube.StringTrieSearch; -import app.revanced.integrations.youtube.TrieSearch; import app.revanced.integrations.youtube.settings.Settings; -abstract class FilterGroup { - final static class FilterGroupResult { - private BooleanSetting setting; - private int matchedIndex; - private int matchedLength; - // In the future it might be useful to include which pattern matched, - // but for now that is not needed. - - FilterGroupResult() { - this(null, -1, 0); - } - - FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) { - setValues(setting, matchedIndex, matchedLength); - } - - public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) { - this.setting = setting; - this.matchedIndex = matchedIndex; - this.matchedLength = matchedLength; - } - - /** - * A null value if the group has no setting, - * or if no match is returned from {@link FilterGroupList#check(Object)}. - */ - public BooleanSetting getSetting() { - return setting; - } - - public boolean isFiltered() { - return matchedIndex >= 0; - } - - /** - * Matched index of first pattern that matched, or -1 if nothing matched. - */ - public int getMatchedIndex() { - return matchedIndex; - } - - /** - * Length of the matched filter pattern. - */ - public int getMatchedLength() { - return matchedLength; - } - } - - protected final BooleanSetting setting; - protected final T[] filters; - - /** - * Initialize a new filter group. - * - * @param setting The associated setting. - * @param filters The filters. - */ - @SafeVarargs - public FilterGroup(final BooleanSetting setting, final T... filters) { - this.setting = setting; - this.filters = filters; - if (filters.length == 0) { - throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)"); - } - } - - public boolean isEnabled() { - return setting == null || setting.get(); - } - - /** - * @return If {@link FilterGroupList} should include this group when searching. - * By default, all filters are included except non enabled settings that require reboot. - */ - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean includeInSearch() { - return isEnabled() || !setting.rebootApp; - } - - @NonNull - @Override - public String toString() { - return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting); - } - - public abstract FilterGroupResult check(final T stack); -} - -class StringFilterGroup extends FilterGroup { - - public StringFilterGroup(final BooleanSetting setting, final String... filters) { - super(setting, filters); - } - - @Override - public FilterGroupResult check(final String string) { - int matchedIndex = -1; - int matchedLength = 0; - if (isEnabled()) { - for (String pattern : filters) { - if (!string.isEmpty()) { - final int indexOf = string.indexOf(pattern); - if (indexOf >= 0) { - matchedIndex = indexOf; - matchedLength = pattern.length(); - break; - } - } - } - } - return new FilterGroupResult(setting, matchedIndex, matchedLength); - } -} - -/** - * If you have more than 1 filter patterns, then all instances of - * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])}, - * which uses a prefix tree to give better performance. - */ -class ByteArrayFilterGroup extends FilterGroup { - - private volatile int[][] failurePatterns; - - // Modified implementation from https://stackoverflow.com/a/1507813 - private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) { - // Finds the first occurrence of the pattern in the byte array using - // KMP matching algorithm. - int patternLength = pattern.length; - for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) { - while (j > 0 && pattern[j] != data[i]) { - j = failure[j - 1]; - } - if (pattern[j] == data[i]) { - j++; - } - if (j == patternLength) { - return i - patternLength + 1; - } - } - return -1; - } - - private static int[] createFailurePattern(byte[] pattern) { - // Computes the failure function using a boot-strapping process, - // where the pattern is matched against itself. - final int patternLength = pattern.length; - final int[] failure = new int[patternLength]; - - for (int i = 1, j = 0; i < patternLength; i++) { - while (j > 0 && pattern[j] != pattern[i]) { - j = failure[j - 1]; - } - if (pattern[j] == pattern[i]) { - j++; - } - failure[i] = j; - } - return failure; - } - - public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) { - super(setting, filters); - } - - /** - * Converts the Strings into byte arrays. Used to search for text in binary data. - */ - public ByteArrayFilterGroup(BooleanSetting setting, String... filters) { - super(setting, ByteTrieSearch.convertStringsToBytes(filters)); - } - - private synchronized void buildFailurePatterns() { - if (failurePatterns != null) return; // Thread race and another thread already initialized the search. - Logger.printDebug(() -> "Building failure array for: " + this); - int[][] failurePatterns = new int[filters.length][]; - int i = 0; - for (byte[] pattern : filters) { - failurePatterns[i++] = createFailurePattern(pattern); - } - this.failurePatterns = failurePatterns; // Must set after initialization finishes. - } - - @Override - public FilterGroupResult check(final byte[] bytes) { - int matchedLength = 0; - int matchedIndex = -1; - if (isEnabled()) { - int[][] failures = failurePatterns; - if (failures == null) { - buildFailurePatterns(); // Lazy load. - failures = failurePatterns; - } - for (int i = 0, length = filters.length; i < length; i++) { - byte[] filter = filters[i]; - matchedIndex = indexOf(bytes, filter, failures[i]); - if (matchedIndex >= 0) { - matchedLength = filter.length; - break; - } - } - } - return new FilterGroupResult(setting, matchedIndex, matchedLength); - } -} - - -abstract class FilterGroupList> implements Iterable { - - private final List filterGroups = new ArrayList<>(); - private final TrieSearch search = createSearchGraph(); - - @SafeVarargs - protected final void addAll(final T... groups) { - filterGroups.addAll(Arrays.asList(groups)); - - for (T group : groups) { - if (!group.includeInSearch()) { - continue; - } - for (V pattern : group.filters) { - search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { - if (group.isEnabled()) { - FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; - result.setValues(group.setting, matchedStartIndex, matchedLength); - return true; - } - return false; - }); - } - } - } - - @NonNull - @Override - public Iterator iterator() { - return filterGroups.iterator(); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public void forEach(@NonNull Consumer action) { - filterGroups.forEach(action); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @NonNull - @Override - public Spliterator spliterator() { - return filterGroups.spliterator(); - } - - protected FilterGroup.FilterGroupResult check(V stack) { - FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); - search.matches(stack, result); - return result; - - } - - protected abstract TrieSearch createSearchGraph(); -} - -final class StringFilterGroupList extends FilterGroupList { - protected StringTrieSearch createSearchGraph() { - return new StringTrieSearch(); - } -} - -/** - * If searching for a single byte pattern, then it is slightly better to use - * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster - * than a prefix tree to search for only 1 pattern. - */ -final class ByteArrayFilterGroupList extends FilterGroupList { - protected ByteTrieSearch createSearchGraph() { - return new ByteTrieSearch(); - } -} - -/** - * Filters litho based components. - * - * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} - * and {@link #addPathCallbacks(StringFilterGroup...)}. - * - * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to - * either an identifier or a path. - * Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} - * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) - * or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern). - * - * All callbacks must be registered before the constructor completes. - */ -abstract class Filter { - - public enum FilterContentType { - IDENTIFIER, - PATH, - PROTOBUFFER - } - - /** - * Identifier callbacks. Do not add to this instance, - * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}. - */ - protected final List identifierCallbacks = new ArrayList<>(); - /** - * Path callbacks. Do not add to this instance, - * and instead use {@link #addPathCallbacks(StringFilterGroup...)}. - */ - protected final List pathCallbacks = new ArrayList<>(); - - /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} - * if any of the groups are found. - */ - protected final void addIdentifierCallbacks(StringFilterGroup... groups) { - identifierCallbacks.addAll(Arrays.asList(groups)); - } - - /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} - * if any of the groups are found. - */ - protected final void addPathCallbacks(StringFilterGroup... groups) { - pathCallbacks.addAll(Arrays.asList(groups)); - } - - /** - * Called after an enabled filter has been matched. - * Default implementation is to always filter the matched component and log the action. - * Subclasses can perform additional or different checks if needed. - *

- * If the content is to be filtered, subclasses should always - * call this method (and never return a plain 'true'). - * That way the logs will always show when a component was filtered and which filter hide it. - *

- * Method is called off the main thread. - * - * @param matchedGroup The actual filter that matched. - * @param contentType The type of content matched. - * @param contentIndex Matched index of the identifier or path. - * @return True if the litho component should be filtered out. - */ - boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - if (BaseSettings.DEBUG.get()) { - String filterSimpleName = getClass().getSimpleName(); - if (contentType == FilterContentType.IDENTIFIER) { - Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier); - } else { - Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path); - } - } - return true; - } -} - -/** - * Placeholder for actual filters. - */ -final class DummyFilter extends Filter { } - @SuppressWarnings("unused") public final class LithoFilterPatch { /** @@ -520,9 +146,9 @@ public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) { @SuppressWarnings("unused") public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) { try { - // It is assumed that protobufBuffer is empty as well in this case. - if (pathBuilder.length() == 0) + if (pathBuilder.length() == 0) { return false; + } ByteBuffer protobufBuffer = bufferThreadLocal.get(); final byte[] bufferArray; @@ -542,14 +168,22 @@ public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBu pathBuilder.toString(), bufferArray); Logger.printDebug(() -> "Searching " + parameter); - if (parameter.identifier != null) { - if (identifierSearchTree.matches(parameter.identifier, parameter)) return true; + if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) { + return true; + } + + if (pathSearchTree.matches(parameter.path, parameter)) { + return true; } - if (pathSearchTree.matches(parameter.path, parameter)) return true; } catch (Exception ex) { Logger.printException(() -> "Litho filter failure", ex); } return false; } -} \ No newline at end of file +} + +/** + * Placeholder for actual filters. + */ +final class DummyFilter extends Filter { } \ No newline at end of file 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 191b6ae43f..f48d10cf8b 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 @@ -9,6 +9,11 @@ import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.List; + +import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.NavigationBar; @@ -16,14 +21,26 @@ @SuppressWarnings("unused") public final class ShortsFilter extends Filter { - public static PivotBar pivotBar; // Set by patch. - + public static final Boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get(); private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; + /** * For paid promotion label and subscribe button that appears in the channel bar. */ private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml"; + /** + * Tags that appears when opening the Shorts player. + */ + private static final List REEL_WATCH_FRAGMENT_INIT_PLAYBACK = Arrays.asList("r_fs", "r_ts"); + + /** + * Vertical padding between the bottom of the screen and the seekbar, when the Shorts navigation bar is hidden. + */ + public static final int HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT = 100; + + private static WeakReference pivotBarRef = new WeakReference<>(null); + private final StringFilterGroup shortsCompactFeedVideoPath; private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer; @@ -241,9 +258,7 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) { // Selectively filter to avoid false positive filtering of other subscribe/join buttons. if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) { - return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } @@ -258,9 +273,7 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff // Video action buttons (like, dislike, comment, share, remix) have the same path. if (matchedGroup == actionBar) { if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) { - return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } @@ -268,9 +281,7 @@ boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBuff if (matchedGroup == suggestedAction) { // Suggested actions can be at the start or in the middle of a path. if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) { - return super.isFiltered( - identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex - ); + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } @@ -343,6 +354,14 @@ public static void hideShortsShelf(final View shortsShelfView) { } } + public static int getSoundButtonSize(int original) { + if (Settings.HIDE_SHORTS_SOUND_BUTTON.get()) { + return 0; + } + + return original; + } + // region Hide the buttons in older versions of YouTube. New versions use Litho. public static void hideLikeButton(final View likeButtonView) { @@ -374,17 +393,30 @@ public static void hideShortsShareButton(final View shareButtonView) { // endregion - public static void hideNavigationBar() { - if (!Settings.HIDE_SHORTS_NAVIGATION_BAR.get()) return; - if (pivotBar == null) return; + public static void setNavigationBar(PivotBar view) { + Logger.printDebug(() -> "Setting navigation bar"); + pivotBarRef = new WeakReference<>(view); + } + + public static void hideNavigationBar(String tag) { + if (HIDE_SHORTS_NAVIGATION_BAR) { + if (REEL_WATCH_FRAGMENT_INIT_PLAYBACK.contains(tag)) { + var pivotBar = pivotBarRef.get(); + if (pivotBar == null) return; - pivotBar.setVisibility(View.GONE); + Logger.printDebug(() -> "Hiding navbar by setting to GONE"); + pivotBar.setVisibility(View.GONE); + } else { + Logger.printDebug(() -> "Ignoring tag: " + tag); + } + } } - public static View hideNavigationBar(final View navigationBarView) { - if (Settings.HIDE_SHORTS_NAVIGATION_BAR.get()) - return null; // Hides the navigation bar. + public static int getNavigationBarHeight(int original) { + if (HIDE_SHORTS_NAVIGATION_BAR) { + return HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT; + } - return navigationBarView; + return original; } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java index 17caa5f6dc..2e97a27d86 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java @@ -4,20 +4,32 @@ import android.graphics.Color; -import app.revanced.integrations.youtube.settings.Settings; +import java.util.Arrays; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") public final class SeekbarColorPatch { - private static final boolean USE_SEEKBAR_CUSTOM_COLOR = Settings.SEEKBAR_CUSTOM_COLOR.get(); + private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = Settings.SEEKBAR_CUSTOM_COLOR.get(); /** * Default color of the seekbar. */ private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000; + /** + * Default colors of the gradient seekbar. + */ + private static final int[] ORIGINAL_SEEKBAR_GRADIENT_COLORS = { 0xFFFF0033, 0xFFFF2791 }; + + /** + * Default positions of the gradient seekbar. + */ + private static final float[] ORIGINAL_SEEKBAR_GRADIENT_POSITIONS = { 0.8f, 1.0f }; + /** * Default YouTube seekbar color brightness. */ @@ -40,7 +52,7 @@ public final class SeekbarColorPatch { Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv); ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2]; - if (USE_SEEKBAR_CUSTOM_COLOR) { + if (SEEKBAR_CUSTOM_COLOR_ENABLED) { loadCustomSeekbarColor(); } } @@ -60,6 +72,14 @@ public static int getSeekbarColor() { return seekbarColor; } + public static boolean playerSeekbarGradientEnabled(boolean original) { + if (original) { + Logger.printDebug(() -> "playerSeekbarGradientEnabled original: " + true); + if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false; + } + + return original; + } /** * Injection point. @@ -74,17 +94,42 @@ public static int getLithoColor(int colorValue) { if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { return 0x00000000; } + return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR); } return colorValue; } + /** + * Injection point. + */ + public static void setLinearGradient(int[] colors, float[] positions) { + if (SEEKBAR_CUSTOM_COLOR_ENABLED) { + // Most litho usage of linear gradients is hooked here, + // so must only change if the values are those for the seekbar. + if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors) + && Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) { + Arrays.fill(colors, Settings.HIDE_SEEKBAR_THUMBNAIL.get() + ? 0x00000000 + : seekbarColor); + return; + } + + Logger.printDebug(() -> "Ignoring gradient colors: " + Arrays.toString(colors) + + " positions: " + Arrays.toString(positions)); + } + } + /** * Injection point. * * Overrides color when video player seekbar is clicked. */ public static int getVideoPlayerSeekbarClickedColor(int colorValue) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED) { + return colorValue; + } + return colorValue == ORIGINAL_SEEKBAR_COLOR ? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR) : colorValue; @@ -96,6 +141,10 @@ public static int getVideoPlayerSeekbarClickedColor(int colorValue) { * Overrides color used for the video player seekbar. */ public static int getVideoPlayerSeekbarColor(int originalColor) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED) { + return originalColor; + } + return getSeekbarColorValue(originalColor); } @@ -105,9 +154,10 @@ public static int getVideoPlayerSeekbarColor(int originalColor) { */ private static int getSeekbarColorValue(int originalColor) { try { - if (!USE_SEEKBAR_CUSTOM_COLOR || originalColor == seekbarColor) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED || originalColor == seekbarColor) { return originalColor; // nothing to do } + final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR); // The seekbar uses the same color but different brightness for different situations. @@ -131,11 +181,13 @@ private static int getSeekbarColorValue(int originalColor) { } } - static int clamp(int value, int lower, int upper) { + /** @noinspection SameParameterValue */ + private static int clamp(int value, int lower, int upper) { return Math.max(lower, Math.min(value, upper)); } - static float clamp(float value, float lower, float upper) { + /** @noinspection SameParameterValue */ + private static float clamp(float value, float lower, float upper) { return Math.max(lower, Math.min(value, upper)); } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java index c62e34f591..d4c8e532ce 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java +++ b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java @@ -44,7 +44,7 @@ private static String parseInputStreamAndClose(InputStream inputStream) throws I String line; while ((line = reader.readLine()) != null) { jsonBuilder.append(line); - jsonBuilder.append("\n"); + jsonBuilder.append('\n'); } return jsonBuilder.toString(); } 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 b63d0484e0..8b97077cbe 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 @@ -585,8 +585,13 @@ && spansHaveEqualTextAndColor(original, originalDislikeSpan)) { public void sendVote(@NonNull Vote vote) { Utils.verifyOnMainThread(); Objects.requireNonNull(vote); + try { - if (isShort != PlayerType.getCurrent().isNoneOrHidden()) { + PlayerType currentType = PlayerType.getCurrent(); + if (isShort != currentType.isNoneHiddenOrMinimized()) { + Logger.printDebug(() -> "Cannot vote for video: " + videoId + + " as current player type does not match: " + currentType); + // Shorts was loaded with regular video present, then Shorts was closed. // and then user voted on the now visible original video. // Cannot send a vote, because this instance is for the wrong video. 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 78a89f6a70..85af3e46f9 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 @@ -1,5 +1,18 @@ package app.revanced.integrations.youtube.settings; +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.integrations.shared.settings.Setting.*; +import static app.revanced.integrations.youtube.patches.ChangeStartPagePatch.StartPage; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; +import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.settings.*; import app.revanced.integrations.shared.settings.preference.SharedPrefCategory; @@ -12,18 +25,6 @@ import app.revanced.integrations.youtube.patches.spoof.SpoofVideoStreamsPatch; import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -import static app.revanced.integrations.shared.settings.Setting.*; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1; -import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3; -import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*; -import static java.lang.Boolean.FALSE; -import static java.lang.Boolean.TRUE; - @SuppressWarnings("deprecation") public class Settings extends BaseSettings { // Video @@ -130,16 +131,21 @@ public class Settings extends BaseSettings { public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE); public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true); public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true); - public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE); + public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true); public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE); public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE); public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE); // Miniplayer public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true); - public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); + private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4); + public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability()); public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN); public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1)); // External downloader @@ -188,7 +194,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_VIDEO_QUALITY_MENU_FOOTER = new BooleanSetting("revanced_hide_video_quality_menu_footer", FALSE); // General layout - public static final StringSetting START_PAGE = new StringSetting("revanced_start_page", ""); + public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.ORIGINAL, true); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message"); public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "17.33.42", true, parent(SPOOF_APP_VERSION)); public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message"); @@ -239,12 +245,12 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE); public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", FALSE); public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE); - public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", TRUE, true); + public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true); // Seekbar public static final BooleanSetting DISABLE_PRECISE_SEEKING_GESTURE = new BooleanSetting("revanced_disable_precise_seeking_gesture", TRUE); public static final BooleanSetting SEEKBAR_TAPPING = new BooleanSetting("revanced_seekbar_tapping", TRUE); - public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE); + public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE, true); public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE); public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true); public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE); diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java index 2de654a2ba..7b00931995 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java @@ -381,6 +381,8 @@ private void addGeneralCategory(final Context context, PreferenceScreen screen) importExport = new EditTextPreference(context) { protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder); + builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { Utils.setClipboard(getEditText().getText().toString()); }); 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 5123e78d6a..4c220d09e2 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 @@ -8,6 +8,8 @@ import androidx.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; @@ -156,11 +158,11 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { try { String lastEnumName = lastYTNavigationEnumName; - for (NavigationButton button : NavigationButton.values()) { - if (button.ytEnumName.equals(lastEnumName)) { + for (NavigationButton buttonType : NavigationButton.values()) { + if (buttonType.ytEnumNames.contains(lastEnumName)) { Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); - viewToButtonMap.put(navigationButtonGroup, button); - navigationTabCreatedCallback(button, navigationButtonGroup); + viewToButtonMap.put(navigationButtonGroup, buttonType); + navigationTabCreatedCallback(buttonType, navigationButtonGroup); return; } } @@ -184,10 +186,10 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { public static void navigationImageResourceTabLoaded(View view) { // 'You' tab has no YT enum name and the enum hook is not called for it. // Compare the last enum to figure out which tab this actually is. - if (CREATE.ytEnumName.equals(lastYTNavigationEnumName)) { + if (CREATE.ytEnumNames.contains(lastYTNavigationEnumName)) { navigationTabLoaded(view); } else { - lastYTNavigationEnumName = NavigationButton.LIBRARY_YOU.ytEnumName; + lastYTNavigationEnumName = NavigationButton.LIBRARY.ytEnumNames.get(0); navigationTabLoaded(view); } } @@ -237,44 +239,39 @@ private static void navigationTabCreatedCallback(NavigationButton button, View t } public enum NavigationButton { - HOME("PIVOT_HOME"), - SHORTS("TAB_SHORTS"), + HOME("PIVOT_HOME", "TAB_HOME_CAIRO"), + SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"), /** * Create new video tab. * This tab will never be in a selected state, even if the create video UI is on screen. */ - CREATE("CREATION_TAB_LARGE"), - SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS"), + CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"), + SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"), /** * Notifications tab. Only present when * {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active. */ - NOTIFICATIONS("TAB_ACTIVITY"), + NOTIFICATIONS("TAB_ACTIVITY", "TAB_ACTIVITY_CAIRO"), /** - * Library tab when the user is not logged in. + * Library tab, including if the user is in incognito mode or when logged out. */ - LIBRARY_LOGGED_OUT("ACCOUNT_CIRCLE"), - /** - * User is logged in with incognito mode enabled. - */ - LIBRARY_INCOGNITO("INCOGNITO_CIRCLE"), - /** - * Old library tab (pre 'You' layout), only present when version spoofing. - */ - LIBRARY_OLD_UI("VIDEO_LIBRARY_WHITE"), - /** - * 'You' library tab that is sometimes momentarily loaded. - * When this is loaded, {@link #LIBRARY_YOU} is also present. - * - * This might be a temporary tab while the user profile photo is loading, - * but its exact purpose is not entirely clear. - */ - LIBRARY_PIVOT_UNKNOWN("PIVOT_LIBRARY"), - /** - * Modern library tab with 'You' layout. - */ - // The hooked YT code does not use an enum, and a dummy name is used here. - LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME"); + LIBRARY( + // Modern library tab with 'You' layout. + // The hooked YT code does not use an enum, and a dummy name is used here. + "YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME", + // User is logged out. + "ACCOUNT_CIRCLE", + "ACCOUNT_CIRCLE_CAIRO", + // User is logged in with incognito mode enabled. + "INCOGNITO_CIRCLE", + "INCOGNITO_CAIRO", + // Old library tab (pre 'You' layout), only present when version spoofing. + "VIDEO_LIBRARY_WHITE", + // 'You' library tab that is sometimes momentarily loaded. + // This might be a temporary tab while the user profile photo is loading, + // but its exact purpose is not entirely clear. + "PIVOT_LIBRARY" + ); @Nullable private static volatile NavigationButton selectedNavigationButton; @@ -303,16 +300,10 @@ public static NavigationButton getSelectedNavigationButton() { /** * YouTube enum name for this tab. */ - private final String ytEnumName; - - NavigationButton(String ytEnumName) { - this.ytEnumName = ytEnumName; - } + private final List ytEnumNames; - public boolean isLibraryOrYouTab() { - return this == LIBRARY_YOU || this == LIBRARY_PIVOT_UNKNOWN - || this == LIBRARY_OLD_UI || this == LIBRARY_INCOGNITO - || this == LIBRARY_LOGGED_OUT; + NavigationButton(String... ytEnumNames) { + this.ytEnumNames = Arrays.asList(ytEnumNames); } } } diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java index 09235e8504..61c40c05dc 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java @@ -21,6 +21,7 @@ import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +@SuppressWarnings("deprecation") public class SegmentCategoryListPreference extends ListPreference { private final SegmentCategory category; private EditText mEditText; @@ -45,6 +46,8 @@ public SegmentCategoryListPreference(Context context, SegmentCategory category) @Override protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { try { + Utils.setEditTextDialogTheme(builder); + Context context = builder.getContext(); TableLayout table = new TableLayout(context); table.setOrientation(LinearLayout.HORIZONTAL); diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java index 9b5c7376d8..48c97e8ec5 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java @@ -25,8 +25,8 @@ public class CreateSegmentButtonController { public static void initialize(View youtubeControlsLayout) { try { Logger.printDebug(() -> "initializing new segment button"); - ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById( - getResourceIdentifier("revanced_sb_create_segment_button", "id"))); + ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName( + youtubeControlsLayout, "revanced_sb_create_segment_button")); imageView.setVisibility(View.GONE); imageView.setOnClickListener(v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility()); diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java index 52a4660d85..02d3e2796c 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java +++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java @@ -27,8 +27,8 @@ public class VotingButtonController { public static void initialize(View youtubeControlsLayout) { try { Logger.printDebug(() -> "initializing voting button"); - ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById( - getResourceIdentifier("revanced_sb_voting_button", "id"))); + ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName( + youtubeControlsLayout, "revanced_sb_voting_button")); imageView.setVisibility(View.GONE); imageView.setOnClickListener(v -> SponsorBlockUtils.onVotingClicked(v.getContext()));