From 595bd1e84f715eeb0ea11cae561eb2cd65cd247f Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:04:26 +0400 Subject: [PATCH 1/5] fix(YouTube - Hide Shorts components): Correctly hide Shorts if navigation tab is changed using device back button --- .../patches/AlternativeThumbnailsPatch.java | 7 +- .../components/KeywordContentFilter.java | 20 +-- .../components/LayoutComponentsFilter.java | 9 +- .../patches/components/ShortsFilter.java | 9 +- .../youtube/shared/NavigationBar.java | 139 ++++++++++++++---- 5 files changed, 130 insertions(+), 54 deletions(-) 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 888649f6f4..33dc3f42dc 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 @@ -167,14 +167,17 @@ private static EnumSetting optionSettingForCurrentNavigation() if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { return ALT_THUMBNAIL_PLAYER; } + // Must check second, as search can be from any tab. if (NavigationBar.isSearchBarActive()) { return ALT_THUMBNAIL_SEARCH; } - if (NavigationButton.HOME.isSelected()) { + + NavigationButton navButtonSelected = NavigationButton.getSelectedNavigationButton(); + if (navButtonSelected == NavigationButton.HOME) { return ALT_THUMBNAIL_HOME; } - if (NavigationButton.SUBSCRIPTIONS.isSelected() || NavigationButton.NOTIFICATIONS.isSelected()) { + if (navButtonSelected == NavigationButton.SUBSCRIPTIONS || navButtonSelected == NavigationButton.NOTIFICATIONS) { return ALT_THUMBNAIL_SUBSCRIPTIONS; } // A library tab variant is active. 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 ed6e63f07f..3eec702678 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 @@ -112,37 +112,25 @@ final class KeywordContentFilter extends Filter { private volatile ByteTrieSearch bufferSearch; - private static void logNavigationState(String state) { - // Enable locally to debug filtering. Default off to reduce log spam. - final boolean LOG_NAVIGATION_STATE = false; - // noinspection ConstantValue - if (LOG_NAVIGATION_STATE) { - Logger.printDebug(() -> "Navigation state: " + state); - } - } - private static boolean hideKeywordSettingIsActive() { // Must check player type first, as search bar can be active behind the player. if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { // For now, consider the under video results the same as the home feed. - logNavigationState("Player active"); return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); } // Must check second, as search can be from any tab. if (NavigationBar.isSearchBarActive()) { - logNavigationState("Search"); return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get(); } - if (NavigationButton.HOME.isSelected()) { - logNavigationState("Home tab"); + + NavigationButton navButtonSelected = NavigationButton.getSelectedNavigationButton(); + if (navButtonSelected == NavigationButton.HOME) { return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); } - if (NavigationButton.SUBSCRIPTIONS.isSelected()) { - logNavigationState("Subscription tab"); + if (navButtonSelected == NavigationButton.SUBSCRIPTIONS) { return Settings.HIDE_SUBSCRIPTIONS_BUTTON.get(); } // User is in the Library or Notifications tab. - logNavigationState("Ignored tab"); 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 ea1369a0bb..a2b2558a1f 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 @@ -1,15 +1,17 @@ package app.revanced.integrations.youtube.patches.components; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; + import android.os.Build; import android.view.View; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import app.revanced.integrations.shared.Utils; -import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.shared.Logger; +import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.StringTrieSearch; +import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.NavigationBar; import app.revanced.integrations.youtube.shared.PlayerType; @@ -368,7 +370,8 @@ public static void hideShowMoreButton(View view) { private static boolean hideShelves() { // Only filter if the library tab is not selected. // This check is important as the shelf layout is used for the library tab playlists. - return !NavigationBar.NavigationButton.libraryOrYouTabIsSelected() + NavigationButton selectedButton = NavigationButton.getSelectedNavigationButton(); + return (selectedButton != null && !selectedButton.isLibraryOrYouTab()) // But if the player is opened while library is selected, // then still filter any recommendations below the player. || PlayerType.getCurrent().isMaximizedOrFullscreen() 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 5b32d7a1f6..a868cdeefa 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 @@ -1,6 +1,7 @@ package app.revanced.integrations.youtube.patches.components; import static app.revanced.integrations.shared.Utils.hideViewUnderCondition; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; import android.view.View; @@ -224,16 +225,20 @@ private static boolean shouldHideShortsFeedItems() { // For now, consider the under video results the same as the home feed. return Settings.HIDE_SHORTS_HOME.get(); } + // Must check second, as search can be from any tab. if (NavigationBar.isSearchBarActive()) { return Settings.HIDE_SHORTS_SEARCH.get(); } - if (NavigationBar.NavigationButton.HOME.isSelected()) { + + NavigationButton navButtonSelected = NavigationButton.getSelectedNavigationButton(); + if (navButtonSelected == NavigationButton.HOME) { return Settings.HIDE_SHORTS_HOME.get(); } - if (NavigationBar.NavigationButton.SUBSCRIPTIONS.isSelected()) { + if (navButtonSelected == NavigationButton.SUBSCRIPTIONS) { return Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); } + // User must be in the library tab. Don't hide the history or any playlists here. return false; } 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 2655a60d4e..209d1eeb1c 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 @@ -2,6 +2,7 @@ import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton.CREATE; +import android.app.Activity; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; @@ -9,9 +10,12 @@ import androidx.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; +import app.revanced.integrations.shared.settings.BaseSettings; import app.revanced.integrations.youtube.settings.Settings; @SuppressWarnings("unused") @@ -19,6 +23,16 @@ public final class NavigationBar { private static volatile WeakReference searchBarResultsRef = new WeakReference<>(null); + /** + * When using the back button and the navigation button changes, the button is updated + * a few milliseconds after litho starts creating the view. To fix this, any thread + * calling for the current navigation button waits until this latch is released. + * + * The latch is also initial set, because on app startup litho can start before the navigation bar is initialized. + */ + @Nullable + private static volatile CountDownLatch navButtonInitializationLatch = new CountDownLatch(1); + /** * Injection point. */ @@ -57,21 +71,16 @@ public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumNa public static void navigationTabLoaded(final View navigationButtonGroup) { try { String lastEnumName = lastYTNavigationEnumName; - for (NavigationButton button : NavigationButton.values()) { - if (button.ytEnumName.equals(lastEnumName)) { - ImageView imageView = Utils.getChildView((ViewGroup) navigationButtonGroup, - true, view -> view instanceof ImageView); - if (imageView != null) { - Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); - - button.imageViewRef = new WeakReference<>(imageView); - navigationTabCreatedCallback(button, navigationButtonGroup); - - return; - } + for (NavigationButton button : NavigationButton.values()) { + if (button.ytEnumName.equals(lastEnumName)) {; + Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); + button.imageViewRef = new WeakReference<>(navigationButtonGroup); + navigationTabCreatedCallback(button, navigationButtonGroup); + return; } } + // Log the unknown tab as exception level, only if debug is enabled. // This is because unknown tabs do no harm, and it's only relevant to developers. if (Settings.DEBUG.get()) { @@ -99,6 +108,52 @@ public static void navigationImageResourceTabLoaded(View view) { } } + /** + * Injection point. + */ + public static void navigationTabSelected(View navButtonImageView, boolean isSelected) { + try { + for (NavigationButton button : NavigationButton.values()) { + View buttonView = button.imageViewRef.get(); + if (buttonView == navButtonImageView) { + if (isSelected) { + if (NavigationButton.selectedNavigationButton != button) { + Logger.printDebug(() -> "Changed to navigation button: " + button); + NavigationButton.selectedNavigationButton = button; + } + + // Wake up any threads waiting to return the currently selected nav button. + CountDownLatch latch = navButtonInitializationLatch; + if (latch != null) { + latch.countDown(); + navButtonInitializationLatch = null; + } + } else if (NavigationButton.selectedNavigationButton == button) { + NavigationButton.selectedNavigationButton = null; + Logger.printDebug(() -> "Navigated away from button: " + button); + } + return; + } + } + + if (BaseSettings.DEBUG.get()) { + // An unknown tab was selected. Only show a toast is debug mode is enabled. + Logger.printException(() -> "Unknown navigation view selected: " + navButtonImageView); + } + NavigationButton.selectedNavigationButton = null; + } catch (Exception ex) { + Logger.printException(() -> "navigationTabSelected failure", ex); + } + } + + /** + * Injection point. + */ + public static void onBackPressed(Activity activity) { + Logger.printDebug(() -> "Back button pressed"); + navButtonInitializationLatch = new CountDownLatch(1); + } + /** @noinspection EmptyMethod*/ private static void navigationTabCreatedCallback(NavigationButton button, View tabView) { // Code is added during patching. @@ -109,8 +164,7 @@ public enum NavigationButton { SHORTS("TAB_SHORTS"), /** * Create new video tab. - * - * {@link #isSelected()} always returns false, even if the create video UI is on screen. + * 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"), @@ -145,41 +199,64 @@ public enum NavigationButton { // The hooked YT code does not use an enum, and a dummy name is used here. LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME"); + @Nullable + private static volatile NavigationButton selectedNavigationButton; + /** + * This will return null only if the currently selected tab is unknown. + * This scenario will only happen if the UI has different tabs due to an A/B user test + * or YT abruptly changes the navigation layout for some other reason. + * + * All code calling this method should handle a null return value. + * * @return The active navigation tab. - * If the user is in the create new video UI, this returns NULL. + * If the user is in the upload video UI, this returns tab currently selected + * on screen (whatever tab the user was on before tapping the upload nav button). */ @Nullable public static NavigationButton getSelectedNavigationButton() { - for (NavigationButton button : values()) { - if (button.isSelected()) return button; + CountDownLatch latch = navButtonInitializationLatch; + if (latch != null) { + Logger.printDebug(() -> "Waiting for navbar button initialization to complete"); + try { + // Timeout should be 2x the average observable time between a back button event + // and when the navigation buttons are updated. + if (!latch.await(250, TimeUnit.MILLISECONDS)) { + // If the user navigates into a menu (such as the watch history) and then uses the + // back button to exit, This will wait for a navigation button change that will not happen. + // Treat this timeout as as normal event. + // + // Changing this to never wait for these situations would require + // knowing if a back button event will change the navigation tab or not. + // This may be difficult to do, and for now the simple answer is to + // stall litho rendering for 250 ms. + Logger.printDebug(() -> "get navigation button wait timed out"); + navButtonInitializationLatch = null; + return null; + } + } catch (InterruptedException ex) { + Logger.printException(() -> "Wait interrupted", ex); // Will never happen. + } + Logger.printDebug(() -> "Waiting complete"); } - return null; - } - /** - * @return If the currently selected tab is a 'You' or library type. - * Covers all known app states including incognito mode and version spoofing. - */ - public static boolean libraryOrYouTabIsSelected() { - return LIBRARY_YOU.isSelected() || LIBRARY_PIVOT_UNKNOWN.isSelected() - || LIBRARY_OLD_UI.isSelected() || LIBRARY_INCOGNITO.isSelected() - || LIBRARY_LOGGED_OUT.isSelected(); + return selectedNavigationButton; } /** * YouTube enum name for this tab. */ private final String ytEnumName; - private volatile WeakReference imageViewRef = new WeakReference<>(null); + private volatile WeakReference imageViewRef = new WeakReference<>(null); NavigationButton(String ytEnumName) { this.ytEnumName = ytEnumName; } - public boolean isSelected() { - ImageView view = imageViewRef.get(); - return view != null && view.isSelected(); + public boolean isLibraryOrYouTab() { + return this == LIBRARY_YOU || this == LIBRARY_PIVOT_UNKNOWN + || this == LIBRARY_OLD_UI || this == LIBRARY_INCOGNITO + || this == LIBRARY_LOGGED_OUT; } } } From 6f742c718a098c0e759c1e2e2c00b31c8c8044a6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:10:59 +0400 Subject: [PATCH 2/5] refactor --- .../youtube/shared/NavigationBar.java | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) 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 209d1eeb1c..431539fd42 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 @@ -4,8 +4,6 @@ import android.app.Activity; import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; import androidx.annotation.Nullable; @@ -31,7 +29,44 @@ public final class NavigationBar { * The latch is also initial set, because on app startup litho can start before the navigation bar is initialized. */ @Nullable - private static volatile CountDownLatch navButtonInitializationLatch = new CountDownLatch(1); + private static volatile CountDownLatch navButtonLatch; + + static { + createNavButtonLatch(); + } + + private static void createNavButtonLatch() { + navButtonLatch = new CountDownLatch(1); + } + + private static void releaseNavButtonLatch() { + CountDownLatch latch = navButtonLatch; + if (latch != null) { + latch.countDown(); + } + navButtonLatch = null; + } + + private static boolean waitForLatchIfNeed() { + CountDownLatch latch = navButtonLatch; + if (latch == null) { + return true; + } + + try { + Logger.printDebug(() -> "Waiting for navbar button latch"); + if (latch.await(1000, TimeUnit.MILLISECONDS)) { + Logger.printDebug(() -> "Waiting complete"); + return true; + } + Logger.printDebug(() -> "Get navigation button wait timed out"); + navButtonLatch = null; + } catch (InterruptedException ex) { + Logger.printException(() -> "Wait interrupted", ex); // Will never happen. + } + + return false; + } /** * Injection point. @@ -123,11 +158,8 @@ public static void navigationTabSelected(View navButtonImageView, boolean isSele } // Wake up any threads waiting to return the currently selected nav button. - CountDownLatch latch = navButtonInitializationLatch; - if (latch != null) { - latch.countDown(); - navButtonInitializationLatch = null; - } + releaseNavButtonLatch(); + } else if (NavigationButton.selectedNavigationButton == button) { NavigationButton.selectedNavigationButton = null; Logger.printDebug(() -> "Navigated away from button: " + button); @@ -151,7 +183,7 @@ public static void navigationTabSelected(View navButtonImageView, boolean isSele */ public static void onBackPressed(Activity activity) { Logger.printDebug(() -> "Back button pressed"); - navButtonInitializationLatch = new CountDownLatch(1); + createNavButtonLatch(); } /** @noinspection EmptyMethod*/ @@ -215,32 +247,10 @@ public enum NavigationButton { */ @Nullable public static NavigationButton getSelectedNavigationButton() { - CountDownLatch latch = navButtonInitializationLatch; - if (latch != null) { - Logger.printDebug(() -> "Waiting for navbar button initialization to complete"); - try { - // Timeout should be 2x the average observable time between a back button event - // and when the navigation buttons are updated. - if (!latch.await(250, TimeUnit.MILLISECONDS)) { - // If the user navigates into a menu (such as the watch history) and then uses the - // back button to exit, This will wait for a navigation button change that will not happen. - // Treat this timeout as as normal event. - // - // Changing this to never wait for these situations would require - // knowing if a back button event will change the navigation tab or not. - // This may be difficult to do, and for now the simple answer is to - // stall litho rendering for 250 ms. - Logger.printDebug(() -> "get navigation button wait timed out"); - navButtonInitializationLatch = null; - return null; - } - } catch (InterruptedException ex) { - Logger.printException(() -> "Wait interrupted", ex); // Will never happen. - } - Logger.printDebug(() -> "Waiting complete"); + if (waitForLatchIfNeed()) { + return selectedNavigationButton; } - - return selectedNavigationButton; + return null; // Latch wait timed out, and it's unclear which tab is selected. } /** From 14527a2638802b0d1c015a93b1f6181340a2206a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 9 Apr 2024 21:07:47 +0400 Subject: [PATCH 3/5] refactor --- .../patches/AlternativeThumbnailsPatch.java | 71 +++++----- .../components/KeywordContentFilter.java | 20 ++- .../components/LayoutComponentsFilter.java | 18 ++- .../patches/components/ShortsFilter.java | 20 ++- .../youtube/shared/NavigationBar.java | 124 +++++++++++------- 5 files changed, 154 insertions(+), 99 deletions(-) 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 33dc3f42dc..df7aab9fcd 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 @@ -1,19 +1,15 @@ package app.revanced.integrations.youtube.patches; +import static app.revanced.integrations.shared.StringRef.str; +import static app.revanced.integrations.youtube.settings.Settings.*; +import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; + import android.net.Uri; + 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; @@ -26,13 +22,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; +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 app.revanced.integrations.youtube.shared.NavigationBar; +import app.revanced.integrations.youtube.shared.PlayerType; /** * Alternative YouTube thumbnails. @@ -134,11 +129,6 @@ public enum ThumbnailStillTime { */ private static volatile long timeToResumeDeArrowAPICalls; - /** - * Used only for debug logging. - */ - private static volatile EnumSetting currentOptionSetting; - static { dearrowApiUri = validateSettings(); final int port = dearrowApiUri.getPort(); @@ -162,26 +152,38 @@ private static Uri validateSettings() { return apiUri; } - private static EnumSetting optionSettingForCurrentNavigation() { + private static ThumbnailOption optionSettingForCurrentNavigation() { // Must check player type first, as search bar can be active behind the player. if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { - return ALT_THUMBNAIL_PLAYER; + return ALT_THUMBNAIL_PLAYER.get(); } // Must check second, as search can be from any tab. if (NavigationBar.isSearchBarActive()) { - return ALT_THUMBNAIL_SEARCH; + return ALT_THUMBNAIL_SEARCH.get(); + } + + // Avoid checking which navigation button is selected, if all other settings are the same. + ThumbnailOption homeOption = ALT_THUMBNAIL_HOME.get(); + ThumbnailOption subscriptionsOption = ALT_THUMBNAIL_SUBSCRIPTIONS.get(); + ThumbnailOption libraryOption = ALT_THUMBNAIL_LIBRARY.get(); + if ((homeOption == subscriptionsOption) && (homeOption == libraryOption)) { + return homeOption; // All are the same option. } - NavigationButton navButtonSelected = NavigationButton.getSelectedNavigationButton(); - if (navButtonSelected == NavigationButton.HOME) { - return ALT_THUMBNAIL_HOME; + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + // Unknown tab, treat as the home tab; + return homeOption; } - if (navButtonSelected == NavigationButton.SUBSCRIPTIONS || navButtonSelected == NavigationButton.NOTIFICATIONS) { - return ALT_THUMBNAIL_SUBSCRIPTIONS; + if (selectedNavButton == NavigationButton.HOME) { + return homeOption; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) { + return subscriptionsOption; } // A library tab variant is active. - return ALT_THUMBNAIL_LIBRARY; + return libraryOption; } /** @@ -259,14 +261,7 @@ private static void handleDeArrowError(@NonNull String url, int statusCode) { */ public static String overrideImageURL(String originalUrl) { try { - EnumSetting optionSetting = optionSettingForCurrentNavigation(); - ThumbnailOption option = optionSetting.get(); - if (BaseSettings.DEBUG.get()) { - if (currentOptionSetting != optionSetting) { - currentOptionSetting = optionSetting; - Logger.printDebug(() -> "Changed to setting: " + optionSetting.key); - } - } + ThumbnailOption option = optionSettingForCurrentNavigation(); if (option == ThumbnailOption.ORIGINAL) { return originalUrl; 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 3eec702678..95b9f466ab 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 @@ -123,12 +123,22 @@ private static boolean hideKeywordSettingIsActive() { return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get(); } - NavigationButton navButtonSelected = NavigationButton.getSelectedNavigationButton(); - if (navButtonSelected == NavigationButton.HOME) { - return Settings.HIDE_KEYWORD_CONTENT_HOME.get(); + // Avoid checking navigation button status if all other settings are off. + final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get(); + final boolean hideSubscriptions = Settings.HIDE_SUBSCRIPTIONS_BUTTON.get(); + if (!hideHome && !hideSubscriptions) { + return false; + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; } - if (navButtonSelected == NavigationButton.SUBSCRIPTIONS) { - return Settings.HIDE_SUBSCRIPTIONS_BUTTON.get(); + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; } // User is in the Library or Notifications tab. 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 a2b2558a1f..c59f64fecc 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 @@ -368,14 +368,18 @@ public static void hideShowMoreButton(View view) { } private static boolean hideShelves() { + // If the player is opened while library is selected, + // then still filter any recommendations below the player. + if (PlayerType.getCurrent().isMaximizedOrFullscreen() + // Or if the search is active while library is selected, then also filter. + || NavigationBar.isSearchBarActive()) { + return true; + } + + // 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 selectedButton = NavigationButton.getSelectedNavigationButton(); - return (selectedButton != null && !selectedButton.isLibraryOrYouTab()) - // But if the player is opened while library is selected, - // then still filter any recommendations below the player. - || PlayerType.getCurrent().isMaximizedOrFullscreen() - // Or if the search is active while library is selected, then also filter. - || NavigationBar.isSearchBarActive(); + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + return selectedNavButton != null && !selectedNavButton.isLibraryOrYouTab(); } } 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 a868cdeefa..bd5a32d7ca 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 @@ -231,12 +231,22 @@ private static boolean shouldHideShortsFeedItems() { return Settings.HIDE_SHORTS_SEARCH.get(); } - NavigationButton navButtonSelected = NavigationButton.getSelectedNavigationButton(); - if (navButtonSelected == NavigationButton.HOME) { - return Settings.HIDE_SHORTS_HOME.get(); + // Avoid checking navigation button status if all other settings are off. + final boolean hideHome = Settings.HIDE_SHORTS_HOME.get(); + final boolean hideSubscriptions = Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); + if (!hideHome && !hideSubscriptions) { + return false; + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; } - if (navButtonSelected == NavigationButton.SUBSCRIPTIONS) { - return Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; } // User must be in the library tab. Don't hide the history or any playlists here. return false; 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 431539fd42..4c02724906 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 @@ -19,19 +19,63 @@ @SuppressWarnings("unused") public final class NavigationBar { + // + // Search bar + // + private static volatile WeakReference searchBarResultsRef = new WeakReference<>(null); /** - * When using the back button and the navigation button changes, the button is updated - * a few milliseconds after litho starts creating the view. To fix this, any thread - * calling for the current navigation button waits until this latch is released. + * Injection point. + */ + public static void searchBarResultsViewLoaded(View searchbarResults) { + searchBarResultsRef = new WeakReference<>(searchbarResults); + } + + /** + * @return If the search bar is on screen. This includes if the player + * is on screen and the search results are behind the player (and not visible). + * Detecting the search is covered by the player can be done by checking {@link PlayerType#isMaximizedOrFullscreen()}. + */ + public static boolean isSearchBarActive() { + View searchbarResults = searchBarResultsRef.get(); + return searchbarResults != null && searchbarResults.getParent() != null; + } + + // + // Navigation bar buttons + // + + /** + * How long to wait for the set nav button latch to be released. Maximum wait time must + * be as small as possible while still allowing enough time for the nav bar to update. + * + * YT calls it's back button handlers out of order, and litho starts + * filtering the previous screen before the navigation bar is updated. + * + * Fixing this situation and not needlessly wait requires somehow detecting if a back button + * key-press will cause a tab change. Typically after pressing the back button, the time + * between the first litho event and the when the nav button is updated is about 10-20ms. * - * The latch is also initial set, because on app startup litho can start before the navigation bar is initialized. + * Using 50-100ms here should be enough time and not noticeable, since YT typically takes + * 100-200ms (or more) just to update the view anyways. + * + * This issue can also be avoided on a patch by patch basis, by avoiding calls to + * {@link NavigationButton#getSelectedNavigationButton()} unless absolutely necessary. + */ + private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 50; + + /** + * Used as a workaround to fix the issue of YT calling back button handlers out of order. + * Used to hold calls to {@link NavigationButton#getSelectedNavigationButton()} + * until the current navigation button can be determined. */ @Nullable private static volatile CountDownLatch navButtonLatch; static { + // On app startup litho can start before the navigation bar is initialized. + // Force it to wait until the nav bar is updated. createNavButtonLatch(); } @@ -42,47 +86,40 @@ private static void createNavButtonLatch() { private static void releaseNavButtonLatch() { CountDownLatch latch = navButtonLatch; if (latch != null) { + navButtonLatch = null; latch.countDown(); } - navButtonLatch = null; } - private static boolean waitForLatchIfNeed() { + private static void waitForNavButtonLatchIfNeed() { CountDownLatch latch = navButtonLatch; if (latch == null) { - return true; + return; + } + + if (Utils.isCurrentlyOnMainThread()) { + // The latch is released from the main thread, and waiting from the main thread will always timeout. + // This situation has only been observed when navigating out of a submenu and not changing tabs. + // and for those cases the nav bar did not change anyways so it's safe to return. + Logger.printDebug(() -> "Cannot block main thread waiting for nav button. Using last known navbar button status."); + return; } try { Logger.printDebug(() -> "Waiting for navbar button latch"); - if (latch.await(1000, TimeUnit.MILLISECONDS)) { - Logger.printDebug(() -> "Waiting complete"); - return true; + if (latch.await(LATCH_AWAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) { + Logger.printDebug(() -> "Latch waiting complete"); + return; } - Logger.printDebug(() -> "Get navigation button wait timed out"); - navButtonLatch = null; + + // Timeout occurred, and a normal event when pressing the physical back button + // does not change navigation tabs. + releaseNavButtonLatch(); // Prevent other threads from waiting for no reason. + Logger.printDebug(() -> "Latch wait timed out"); + } catch (InterruptedException ex) { Logger.printException(() -> "Wait interrupted", ex); // Will never happen. } - - return false; - } - - /** - * Injection point. - */ - public static void searchBarResultsViewLoaded(View searchbarResults) { - searchBarResultsRef = new WeakReference<>(searchbarResults); - } - - /** - * @return If the search bar is on screen. This includes if the player - * is on screen and the search results are behind the player (and not visible). - * Detecting the search is covered by the player can be done by checking {@link PlayerType#isMaximizedOrFullscreen()}. - */ - public static boolean isSearchBarActive() { - View searchbarResults = searchBarResultsRef.get(); - return searchbarResults != null && searchbarResults.getParent() != null; } /** @@ -108,9 +145,9 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { String lastEnumName = lastYTNavigationEnumName; for (NavigationButton button : NavigationButton.values()) { - if (button.ytEnumName.equals(lastEnumName)) {; + if (button.ytEnumName.equals(lastEnumName)) { Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); - button.imageViewRef = new WeakReference<>(navigationButtonGroup); + button.buttonLayoutRef = new WeakReference<>(navigationButtonGroup); navigationTabCreatedCallback(button, navigationButtonGroup); return; } @@ -149,17 +186,15 @@ public static void navigationImageResourceTabLoaded(View view) { public static void navigationTabSelected(View navButtonImageView, boolean isSelected) { try { for (NavigationButton button : NavigationButton.values()) { - View buttonView = button.imageViewRef.get(); + View buttonView = button.buttonLayoutRef.get(); + if (buttonView == navButtonImageView) { if (isSelected) { - if (NavigationButton.selectedNavigationButton != button) { - Logger.printDebug(() -> "Changed to navigation button: " + button); - NavigationButton.selectedNavigationButton = button; - } - + NavigationButton.selectedNavigationButton = button; // Wake up any threads waiting to return the currently selected nav button. releaseNavButtonLatch(); + Logger.printDebug(() -> "Changed to navigation button: " + button); } else if (NavigationButton.selectedNavigationButton == button) { NavigationButton.selectedNavigationButton = null; Logger.printDebug(() -> "Navigated away from button: " + button); @@ -241,23 +276,24 @@ public enum NavigationButton { * * All code calling this method should handle a null return value. * + * Due to issues with how YT processes hardware back button events, + * this patch uses workarounds that can cause this method to take up to 50ms. + * * @return The active navigation tab. * If the user is in the upload video UI, this returns tab currently selected * on screen (whatever tab the user was on before tapping the upload nav button). */ @Nullable public static NavigationButton getSelectedNavigationButton() { - if (waitForLatchIfNeed()) { - return selectedNavigationButton; - } - return null; // Latch wait timed out, and it's unclear which tab is selected. + waitForNavButtonLatchIfNeed(); + return selectedNavigationButton; } /** * YouTube enum name for this tab. */ private final String ytEnumName; - private volatile WeakReference imageViewRef = new WeakReference<>(null); + private volatile WeakReference buttonLayoutRef = new WeakReference<>(null); NavigationButton(String ytEnumName) { this.ytEnumName = ytEnumName; From c169b9f332dbfdf1e70bf1ee1d9bd60578ae3073 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 10 Apr 2024 00:04:33 +0400 Subject: [PATCH 4/5] refactor: Simplify --- .../youtube/shared/NavigationBar.java | 86 +++++++++++-------- 1 file changed, 48 insertions(+), 38 deletions(-) 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 4c02724906..01b44b7996 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.IdentityHashMap; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -50,29 +52,37 @@ public static boolean isSearchBarActive() { * How long to wait for the set nav button latch to be released. Maximum wait time must * be as small as possible while still allowing enough time for the nav bar to update. * - * YT calls it's back button handlers out of order, and litho starts - * filtering the previous screen before the navigation bar is updated. + * YT calls it's back button handlers out of order, + * and litho starts filtering before the navigation bar is updated. * - * Fixing this situation and not needlessly wait requires somehow detecting if a back button - * key-press will cause a tab change. Typically after pressing the back button, the time - * between the first litho event and the when the nav button is updated is about 10-20ms. + * Fixing this situation and not needlessly waiting requires somehow + * detecting if a back button key-press will cause a tab change. * - * Using 50-100ms here should be enough time and not noticeable, since YT typically takes - * 100-200ms (or more) just to update the view anyways. + * Typically after pressing the back button, the time between the first litho event and + * when the nav button is updated is about 10-20ms. Using 50-100ms here should be enough time + * and not noticeable, since YT typically takes 100-200ms (or more) to update the view anyways. * * This issue can also be avoided on a patch by patch basis, by avoiding calls to * {@link NavigationButton#getSelectedNavigationButton()} unless absolutely necessary. */ - private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 50; + private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 75; /** * Used as a workaround to fix the issue of YT calling back button handlers out of order. * Used to hold calls to {@link NavigationButton#getSelectedNavigationButton()} * until the current navigation button can be determined. + * + * Only used when the hardware back button is pressed. */ @Nullable private static volatile CountDownLatch navButtonLatch; + /** + * Map of nav button layout views to Enum type. + * No synchronization is needed, and this is always accessed from the main thread. + */ + private static final Map viewToButtonMap = new IdentityHashMap<>(); + static { // On app startup litho can start before the navigation bar is initialized. // Force it to wait until the nav bar is updated. @@ -100,15 +110,15 @@ private static void waitForNavButtonLatchIfNeed() { if (Utils.isCurrentlyOnMainThread()) { // The latch is released from the main thread, and waiting from the main thread will always timeout. // This situation has only been observed when navigating out of a submenu and not changing tabs. - // and for those cases the nav bar did not change anyways so it's safe to return. + // and for that use case the nav bar does not change so it's safe to return here. Logger.printDebug(() -> "Cannot block main thread waiting for nav button. Using last known navbar button status."); return; } try { - Logger.printDebug(() -> "Waiting for navbar button latch"); + Logger.printDebug(() -> "Latch wait started"); if (latch.await(LATCH_AWAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) { - Logger.printDebug(() -> "Latch waiting complete"); + Logger.printDebug(() -> "Latch wait complete"); return; } @@ -118,15 +128,16 @@ private static void waitForNavButtonLatchIfNeed() { Logger.printDebug(() -> "Latch wait timed out"); } catch (InterruptedException ex) { - Logger.printException(() -> "Wait interrupted", ex); // Will never happen. + Logger.printException(() -> "Latch wait interrupted failure", ex); // Will never happen. } } /** * Last YT navigation enum loaded. Not necessarily the active navigation tab. + * Always accessed from the main thread. */ @Nullable - private static volatile String lastYTNavigationEnumName; + private static String lastYTNavigationEnumName; /** * Injection point. @@ -147,7 +158,7 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { for (NavigationButton button : NavigationButton.values()) { if (button.ytEnumName.equals(lastEnumName)) { Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); - button.buttonLayoutRef = new WeakReference<>(navigationButtonGroup); + viewToButtonMap.put(navigationButtonGroup, button); navigationTabCreatedCallback(button, navigationButtonGroup); return; } @@ -185,29 +196,28 @@ public static void navigationImageResourceTabLoaded(View view) { */ public static void navigationTabSelected(View navButtonImageView, boolean isSelected) { try { - for (NavigationButton button : NavigationButton.values()) { - View buttonView = button.buttonLayoutRef.get(); - - if (buttonView == navButtonImageView) { - if (isSelected) { - NavigationButton.selectedNavigationButton = button; - // Wake up any threads waiting to return the currently selected nav button. - releaseNavButtonLatch(); - - Logger.printDebug(() -> "Changed to navigation button: " + button); - } else if (NavigationButton.selectedNavigationButton == button) { - NavigationButton.selectedNavigationButton = null; - Logger.printDebug(() -> "Navigated away from button: " + button); - } - return; + NavigationButton button = viewToButtonMap.get(navButtonImageView); + + if (button == null) { // An unknown tab was selected. + // Show a toast only if debug mode is enabled. + if (BaseSettings.DEBUG.get()) { + Logger.printException(() -> "Unknown navigation view selected: " + navButtonImageView); } + + NavigationButton.selectedNavigationButton = null; + return; } - if (BaseSettings.DEBUG.get()) { - // An unknown tab was selected. Only show a toast is debug mode is enabled. - Logger.printException(() -> "Unknown navigation view selected: " + navButtonImageView); + if (isSelected) { + NavigationButton.selectedNavigationButton = button; + Logger.printDebug(() -> "Changed to navigation button: " + button); + + // Release any threads waiting for the selected nav button. + releaseNavButtonLatch(); + } else if (NavigationButton.selectedNavigationButton == button) { + NavigationButton.selectedNavigationButton = null; + Logger.printDebug(() -> "Navigated away from button: " + button); } - NavigationButton.selectedNavigationButton = null; } catch (Exception ex) { Logger.printException(() -> "navigationTabSelected failure", ex); } @@ -276,12 +286,13 @@ public enum NavigationButton { * * All code calling this method should handle a null return value. * - * Due to issues with how YT processes hardware back button events, - * this patch uses workarounds that can cause this method to take up to 50ms. + * Due to issues with how YT processes physical back button events, + * this patch uses workarounds that can cause this method to take up to 75ms + * if the device back button was recently pressed. * * @return The active navigation tab. - * If the user is in the upload video UI, this returns tab currently selected - * on screen (whatever tab the user was on before tapping the upload nav button). + * If the user is in the upload video UI, this returns tab that is still visually + * selected on screen (whatever tab the user was on before tapping the upload button). */ @Nullable public static NavigationButton getSelectedNavigationButton() { @@ -293,7 +304,6 @@ public static NavigationButton getSelectedNavigationButton() { * YouTube enum name for this tab. */ private final String ytEnumName; - private volatile WeakReference buttonLayoutRef = new WeakReference<>(null); NavigationButton(String ytEnumName) { this.ytEnumName = ytEnumName; From d30b8a65f930728eb10ade9d4c9ec284807e49df Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 10 Apr 2024 00:53:12 +0400 Subject: [PATCH 5/5] refactor --- .../patches/components/LayoutComponentsFilter.java | 2 +- .../integrations/youtube/shared/NavigationBar.java | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) 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 c59f64fecc..f0e560e40a 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 @@ -369,7 +369,7 @@ public static void hideShowMoreButton(View view) { private static boolean hideShelves() { // If the player is opened while library is selected, - // then still filter any recommendations below the player. + // then filter any recommendations below the player. if (PlayerType.getCurrent().isMaximizedOrFullscreen() // Or if the search is active while library is selected, then also filter. || NavigationBar.isSearchBarActive()) { 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 01b44b7996..58c2ec0b64 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,8 +8,8 @@ import androidx.annotation.Nullable; import java.lang.ref.WeakReference; -import java.util.IdentityHashMap; import java.util.Map; +import java.util.WeakHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -81,7 +81,7 @@ public static boolean isSearchBarActive() { * Map of nav button layout views to Enum type. * No synchronization is needed, and this is always accessed from the main thread. */ - private static final Map viewToButtonMap = new IdentityHashMap<>(); + private static final Map viewToButtonMap = new WeakHashMap<>(); static { // On app startup litho can start before the navigation bar is initialized. @@ -101,7 +101,7 @@ private static void releaseNavButtonLatch() { } } - private static void waitForNavButtonLatchIfNeed() { + private static void waitForNavButtonLatchIfNeeded() { CountDownLatch latch = navButtonLatch; if (latch == null) { return; @@ -118,6 +118,7 @@ private static void waitForNavButtonLatchIfNeed() { try { Logger.printDebug(() -> "Latch wait started"); if (latch.await(LATCH_AWAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) { + // Back button changed the navigation tab. Logger.printDebug(() -> "Latch wait complete"); return; } @@ -296,7 +297,7 @@ public enum NavigationButton { */ @Nullable public static NavigationButton getSelectedNavigationButton() { - waitForNavButtonLatchIfNeed(); + waitForNavButtonLatchIfNeeded(); return selectedNavigationButton; }