From ffc3437843c24af255d2a0dda9930d2843cac4b6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:29:23 +0400 Subject: [PATCH] fix(YouTube - Hide Shorts components): Correctly hide Shorts if navigation tab is changed using device back button (#611) --- .../patches/AlternativeThumbnailsPatch.java | 72 +++--- .../components/KeywordContentFilter.java | 34 ++- .../components/LayoutComponentsFilter.java | 23 +- .../patches/components/ShortsFilter.java | 23 +- .../youtube/shared/NavigationBar.java | 206 +++++++++++++++--- 5 files changed, 255 insertions(+), 103 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 888649f6..df7aab9f 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.LinkedHashMap; 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 final class AlternativeThumbnailsPatch { */ private static volatile long timeToResumeDeArrowAPICalls; - /** - * Used only for debug logging. - */ - private static volatile EnumSetting currentOptionSetting; - static { dearrowApiUri = validateSettings(); final int port = dearrowApiUri.getPort(); @@ -162,23 +152,38 @@ public final class AlternativeThumbnailsPatch { 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(); } - if (NavigationButton.HOME.isSelected()) { - return ALT_THUMBNAIL_HOME; + + // 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. } - if (NavigationButton.SUBSCRIPTIONS.isSelected() || NavigationButton.NOTIFICATIONS.isSelected()) { - return ALT_THUMBNAIL_SUBSCRIPTIONS; + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + // Unknown tab, treat as the home tab; + return homeOption; + } + 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; } /** @@ -256,14 +261,7 @@ public final class AlternativeThumbnailsPatch { */ 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 ed6e63f0..95b9f466 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,35 @@ 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"); - 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; } - if (NavigationButton.SUBSCRIPTIONS.isSelected()) { - logNavigationState("Subscription tab"); - return Settings.HIDE_SUBSCRIPTIONS_BUTTON.get(); + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; } // 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 ea1369a0..f0e560e4 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; @@ -366,13 +368,18 @@ public final class LayoutComponentsFilter extends Filter { } private static boolean hideShelves() { + // If the player is opened while library is selected, + // 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()) { + 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. - return !NavigationBar.NavigationButton.libraryOrYouTabIsSelected() - // 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 5b32d7a1..bd5a32d7 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,30 @@ public final class ShortsFilter extends Filter { // 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()) { - 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; } - if (NavigationBar.NavigationButton.SUBSCRIPTIONS.isSelected()) { - return Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; + } + 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 2655a60d..58c2ec0b 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,21 +2,29 @@ package app.revanced.integrations.youtube.shared; 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; import androidx.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; +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") public final class NavigationBar { + // + // Search bar + // + private static volatile WeakReference searchBarResultsRef = new WeakReference<>(null); /** @@ -36,11 +44,101 @@ public final class NavigationBar { return searchbarResults != null && searchbarResults.getParent() != null; } + // + // Navigation bar buttons + // + /** - * Last YT navigation enum loaded. Not necessarily the active navigation tab. + * 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 before the navigation bar is updated. + * + * Fixing this situation and not needlessly waiting 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 + * 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 = 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 String lastYTNavigationEnumName; + 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 WeakHashMap<>(); + + static { + // On app startup litho can start before the navigation bar is initialized. + // Force it to wait until the nav bar is updated. + createNavButtonLatch(); + } + + private static void createNavButtonLatch() { + navButtonLatch = new CountDownLatch(1); + } + + private static void releaseNavButtonLatch() { + CountDownLatch latch = navButtonLatch; + if (latch != null) { + navButtonLatch = null; + latch.countDown(); + } + } + + private static void waitForNavButtonLatchIfNeeded() { + CountDownLatch latch = navButtonLatch; + if (latch == null) { + 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 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(() -> "Latch wait started"); + if (latch.await(LATCH_AWAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) { + // Back button changed the navigation tab. + Logger.printDebug(() -> "Latch wait complete"); + return; + } + + // 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(() -> "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 String lastYTNavigationEnumName; /** * Injection point. @@ -57,21 +155,16 @@ public final class NavigationBar { 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; - } + Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); + viewToButtonMap.put(navigationButtonGroup, button); + 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 +192,46 @@ public final class NavigationBar { } } + /** + * Injection point. + */ + public static void navigationTabSelected(View navButtonImageView, boolean isSelected) { + try { + 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 (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); + } + } catch (Exception ex) { + Logger.printException(() -> "navigationTabSelected failure", ex); + } + } + + /** + * Injection point. + */ + public static void onBackPressed(Activity activity) { + Logger.printDebug(() -> "Back button pressed"); + createNavButtonLatch(); + } + /** @noinspection EmptyMethod*/ private static void navigationTabCreatedCallback(NavigationButton button, View tabView) { // Code is added during patching. @@ -109,8 +242,7 @@ public final class NavigationBar { 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 +277,43 @@ public final class NavigationBar { // 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. + * + * 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 create new video UI, this returns NULL. + * 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() { - for (NavigationButton button : values()) { - if (button.isSelected()) return button; - } - 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(); + waitForNavButtonLatchIfNeeded(); + return selectedNavigationButton; } /** * YouTube enum name for this tab. */ private final String ytEnumName; - 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; } } }