diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ConversionContext.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ConversionContext.java new file mode 100644 index 0000000000..ea1f94fc54 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ConversionContext.java @@ -0,0 +1,13 @@ +package app.revanced.extension.shared; + +public final class ConversionContext { + /** + * Interface to use obfuscated methods. + */ + public interface ContextInterface { + // Methods implemented by patch. + StringBuilder patch_getPathBuilder(); + + String patch_getIdentifier(); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index 18c6959aa4..6f90da0a8a 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -376,7 +376,7 @@ public class Utils { /** * Checks if a specific app package is installed and enabled on the device. * - * @param packageName The application package name to check (e.g., "app.morphe.android.apps.youtube.music"). + * @param packageName The application package name to check (e.g., "app.revanced.android.apps.youtube.music"). * @return True if the package is installed and enabled, false otherwise. */ public static boolean isPackageEnabled(String packageName) { @@ -396,6 +396,18 @@ public class Utils { return false; } } + + public static boolean startsWithAny(String value, String...targets) { + if (isNotEmpty(value)) { + for (String string : targets) { + if (isNotEmpty(string) && value.startsWith(string)) { + return true; + } + } + } + return false; + } + public interface MatchFilter { boolean matches(T object); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java index dc60f5d7da..1af697236c 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java @@ -5,12 +5,10 @@ import androidx.annotation.Nullable; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.Collections; import java.util.List; -import java.util.Map; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.StringTrieSearch; @@ -123,11 +121,6 @@ public final class LithoFilterPatch { */ private static final boolean EXTRACT_IDENTIFIER_FROM_BUFFER = false; - /** - * Turns on additional logging, used for development purposes only. - */ - public static final boolean DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER = false; - /** * String suffix for components. * Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b" @@ -146,19 +139,6 @@ public final class LithoFilterPatch { */ private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>(); - /** - * Identifier to protocol buffer mapping. Only used for 20.22+. - * Thread local is needed because filtering is multithreaded and each thread can load - * a different component with the same identifier. - */ - private static final ThreadLocal> identifierToBufferThread = new ThreadLocal<>(); - - /** - * Global shared buffer. Used only if the buffer is not found in the ThreadLocal. - */ - private static final Map identifierToBufferGlobal - = Collections.synchronizedMap(createIdentifierToBufferMap()); - private static final StringTrieSearch pathSearchTree = new StringTrieSearch(); private static final StringTrieSearch identifierSearchTree = new StringTrieSearch(); @@ -211,126 +191,11 @@ public final class LithoFilterPatch { } } - private static Map createIdentifierToBufferMap() { - // It's unclear how many items should be cached. This is a guess. - return Utils.createSizeRestrictedMap(100); - } - - /** - * Helper function that differs from {@link Character#isDigit(char)} - * as this only matches ascii and not Unicode numbers. - */ - private static boolean isAsciiNumber(byte character) { - return '0' <= character && character <= '9'; - } - - private static boolean isAsciiLowerCaseLetter(byte character) { - return 'a' <= character && character <= 'z'; - } - - /** - * Injection point. Called off the main thread. - * Targets 20.22+ - */ - public static void setProtoBuffer(byte[] buffer) { - if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) { - StringBuilder builder = new StringBuilder(); - LithoFilterParameters.findAsciiStrings(builder, buffer); - Logger.printDebug(() -> "New buffer: " + builder); - } - - // The identifier always seems to start very close to the buffer start. - // Highest identifier start index ever observed is 50, with most around 30 to 40. - // The buffer can be very large with up to 200kb has been observed, - // so the search is restricted to only the start. - final int maxBufferStartIndex = 500; // 10x expected upper bound. - - // Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of - // unique characters, but it seems to be slower since the extra overhead of checking the - // bad character array negates any performance gain of skipping a few extra subsearches. - int emlIndex = -1; - final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length; - final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength); - for (int i = 0; i < lastBufferIndexToCheckFrom; i++) { - boolean match = true; - for (int j = 0; j < emlStringLength; j++) { - if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) { - match = false; - break; - } - } - if (match) { - emlIndex = i; - break; - } - } - - if (emlIndex < 0) { - // Buffer is not used for creating a new litho component. - if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) { - Logger.printDebug(() -> "Could not find eml index"); - } - return; - } - - int startIndex = emlIndex - 1; - while (startIndex > 0) { - final byte character = buffer[startIndex]; - int startIndexFinal = startIndex; - if (isAsciiLowerCaseLetter(character) || isAsciiNumber(character) || character == '_') { - // Valid character for the first path element. - startIndex--; - } else { - startIndex++; - break; - } - } - - // Strip away any numbers on the start of the identifier, which can - // be from random data in the buffer before the identifier starts. - while (true) { - final byte character = buffer[startIndex]; - if (isAsciiNumber(character)) { - startIndex++; - } else { - break; - } - } - - // Find the pipe character after the identifier. - int endIndex = -1; - for (int i = emlIndex, length = buffer.length; i < length; i++) { - if (buffer[i] == '|') { - endIndex = i; - break; - } - } - if (endIndex < 0) { - if (BaseSettings.DEBUG.get()) { - Logger.printException(() -> "Debug: Could not find buffer identifier"); - } - return; - } - - String identifier = new String(buffer, startIndex, endIndex - startIndex, StandardCharsets.US_ASCII); - if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) { - Logger.printDebug(() -> "Found buffer for identifier: " + identifier); - } - identifierToBufferGlobal.put(identifier, buffer); - - Map map = identifierToBufferThread.get(); - if (map == null) { - map = createIdentifierToBufferMap(); - identifierToBufferThread.set(map); - } - map.put(identifier, buffer); - } - /** * Injection point. Called off the main thread. * Targets 20.21 and lower. */ - public static void setProtoBuffer(@Nullable ByteBuffer buffer) { + public static void setProtobufBuffer(@Nullable ByteBuffer buffer) { if (buffer == null || !buffer.hasArray()) { // It appears the buffer can be cleared out just before the call to #filter() // Ignore this null value and retain the last buffer that was set. @@ -347,44 +212,18 @@ public final class LithoFilterPatch { /** * Injection point. */ - public static boolean isFiltered(String identifier, @Nullable String accessibilityId, - @Nullable String accessibilityText, StringBuilder pathBuilder) { + public static boolean isFiltered(ContextInterface contextInterface, @Nullable byte[] bytes, + @Nullable String accessibilityId, @Nullable String accessibilityText) { try { + String identifier = contextInterface.patch_getIdentifier(); + StringBuilder pathBuilder = contextInterface.patch_getPathBuilder(); if (identifier.isEmpty() || pathBuilder.length() == 0) { return false; } - byte[] buffer = null; - if (EXTRACT_IDENTIFIER_FROM_BUFFER) { - final int pipeIndex = identifier.indexOf('|'); - if (pipeIndex >= 0) { - // If the identifier contains no pipe, then it's not an ".eml" identifier - // and the buffer is not uniquely identified. Typically, this only happens - // for subcomponents where buffer filtering is not used. - String identifierKey = identifier.substring(0, pipeIndex); - - var map = identifierToBufferThread.get(); - if (map != null) { - buffer = map.get(identifierKey); - } - - if (buffer == null) { - // Buffer for thread local not found. Use the last buffer found from any thread. - buffer = identifierToBufferGlobal.get(identifierKey); - - if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER && buffer == null) { - // No buffer is found for some components, such as - // shorts_lockup_cell.eml on channel profiles. - // For now, just ignore this and filter without a buffer. - if (BaseSettings.DEBUG.get()) { - Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier); - } - } - } - } - } else { - buffer = bufferThreadLocal.get(); - } + byte[] buffer = EXTRACT_IDENTIFIER_FROM_BUFFER + ? bytes + : bufferThreadLocal.get(); // Potentially the buffer may have been null or never set up until now. // Use an empty buffer so the litho id/path filters that do not use a buffer still work. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LayoutReloadObserverPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LayoutReloadObserverPatch.java new file mode 100644 index 0000000000..95108882c6 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LayoutReloadObserverPatch.java @@ -0,0 +1,49 @@ +package app.revanced.extension.youtube.patches; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.shared.PlayerType; + +/** + * Here is an unintended behavior: + *

+ * 1. The user does not hide Shorts in the Subscriptions tab, but hides them otherwise. + * 2. Goes to the Subscriptions tab and scrolls to where Shorts is. + * 3. Opens a regular video. + * 4. Minimizes the video and turns off the screen. + * 5. Turns the screen on and maximizes the video. + * 6. Shorts belonging to related videos are not hidden. + *

+ * Here is an explanation of this special issue: + *

+ * When the user minimizes the video, turns off the screen, and then turns it back on, + * the components below the player are reloaded, and at this moment the PlayerType is [WATCH_WHILE_MINIMIZED]. + * (Shorts belonging to related videos are also reloaded) + * Since the PlayerType is [WATCH_WHILE_MINIMIZED] at this moment, the navigation tab is checked. + * (Even though PlayerType is [WATCH_WHILE_MINIMIZED], this is a Shorts belonging to a related video) + *

+ * As a workaround for this special issue, if a video actionbar is detected, which is one of the components below the player, + * it is treated as being in the same state as [WATCH_WHILE_MAXIMIZED]. + */ +@SuppressWarnings("unused") +public class LayoutReloadObserverPatch { + private static final String COMPACTIFY_VIDEO_ACTION_BAR_PREFIX = "compactify_video_action_bar.e"; + private static final String VIDEO_ACTION_BAR_PREFIX = "video_action_bar.e"; + public static final AtomicBoolean isActionBarVisible = new AtomicBoolean(false); + + public static void onLazilyConvertedElementLoaded(@NonNull String identifier, + @NonNull List treeNodeResultList) { + if (!Utils.startsWithAny(identifier, COMPACTIFY_VIDEO_ACTION_BAR_PREFIX, VIDEO_ACTION_BAR_PREFIX)) { + return; + } + + if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED && + isActionBarVisible.compareAndSet(false, true)) { + Utils.runOnMainThreadDelayed(() -> isActionBarVisible.compareAndSet(true, false), 500); + } + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LazilyConvertedElementPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LazilyConvertedElementPatch.java new file mode 100644 index 0000000000..1fb4737ab6 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LazilyConvertedElementPatch.java @@ -0,0 +1,33 @@ +package app.revanced.extension.youtube.patches; + +import java.util.List; + +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.ConversionContext.ContextInterface; + + +@SuppressWarnings("unused") +public class LazilyConvertedElementPatch { + private static final String LAZILY_CONVERTED_ELEMENT = "LazilyConvertedElement"; + + /** + * Injection point. + */ + public static void onTreeNodeResultLoaded(ContextInterface contextInterface, List treeNodeResultList) { + if (treeNodeResultList == null || treeNodeResultList.isEmpty()) { + return; + } + String firstElement = treeNodeResultList.get(0).toString(); + if (!LAZILY_CONVERTED_ELEMENT.equals(firstElement)) { + return; + } + String identifier = contextInterface.patch_getIdentifier(); + if (Utils.isNotEmpty(identifier)) { + onLazilyConvertedElementLoaded(identifier, treeNodeResultList); + } + } + + private static void onLazilyConvertedElementLoaded(String identifier, List treeNodeResultList) { + // Code added by patch. + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PlayerControlsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PlayerControlsPatch.java index 482c0c3c49..8f5ed8cc8b 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PlayerControlsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/PlayerControlsPatch.java @@ -54,6 +54,6 @@ public class PlayerControlsPatch { // noinspection EmptyMethod private static void fullscreenButtonVisibilityChanged(boolean isVisible) { - // Code added during patching. + // Code added by patch. } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java index beaa3eac82..4f351c1e77 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java @@ -19,24 +19,25 @@ import app.revanced.extension.shared.Utils; import app.revanced.extension.youtube.patches.litho.ReturnYouTubeDislikeFilter; import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.youtube.shared.PlayerType; /** * Handles all interaction of UI patch components. - * + *

* Known limitation: * The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed. * This is because it modifies the dislikes text synchronously, and if the RYD fetch has * not completed yet then the UI will be temporarily frozen. - * + *

* A (yet to be implemented) solution that fixes this problem. Any one of: * - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously. * - Find a way to force Litho to rebuild it's component tree, - * and use that hook to force the shorts dislikes to update after the fetch is completed. + * and use that hook to force the shorts dislikes to update after the fetch is completed. * - Hook into the dislikes button image view, and replace the dislikes thumb down image with a - * generated image of the number of dislikes, then update the image asynchronously. This Could - * also be used for the regular video player to give a better UI layout and completely remove - * the need for the Rolling Number patches. + * generated image of the number of dislikes, then update the image asynchronously. This Could + * also be used for the regular video player to give a better UI layout and completely remove + * the need for the Rolling Number patches. */ @SuppressWarnings("unused") public class ReturnYouTubeDislikePatch { @@ -100,7 +101,7 @@ public class ReturnYouTubeDislikePatch { /** * Injection point. - * + *

* Logs if new litho text layout is used. */ public static boolean useNewLithoTextCreation(boolean useNewLithoTextCreation) { @@ -113,28 +114,28 @@ public class ReturnYouTubeDislikePatch { /** * Injection point. - * + *

* For Litho segmented buttons and Litho Shorts player. */ @NonNull - public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, - @NonNull CharSequence original) { - return onLithoTextLoaded(conversionContext, original, false); + public static CharSequence onLithoTextLoaded(ContextInterface contextInterface, + CharSequence original) { + return onLithoTextLoaded(contextInterface, original, false); } /** * Called when a litho text component is initially created, * and also when a Span is later reused again (such as scrolling off/on screen). - * + *

* This method is sometimes called on the main thread, but it is usually called _off_ the main thread. * This method can be called multiple times for the same UI element (including after dislikes was added). * - * @param original Original char sequence was created or reused by Litho. + * @param original Original char sequence was created or reused by Litho. * @param isRollingNumber If the span is for a Rolling Number. * @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes. */ @NonNull - private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, + private static CharSequence onLithoTextLoaded(ContextInterface contextInterface, @NonNull CharSequence original, boolean isRollingNumber) { try { @@ -142,17 +143,16 @@ public class ReturnYouTubeDislikePatch { return original; } - String conversionContextString = conversionContext.toString(); - - if (Settings.RYD_ENABLED.get()) { // FIXME: Remove this. - Logger.printDebug(() -> "RYD conversion context: " + conversionContext); - } - - if (isRollingNumber && !conversionContextString.contains("video_action_bar.e")) { + String identifier = contextInterface.patch_getIdentifier(); + if (isRollingNumber && (identifier == null || !identifier.contains("video_action_bar.e"))) { return original; } - if (conversionContextString.contains("segmented_like_dislike_button.e")) { + StringBuilder pathBuilder = contextInterface.patch_getPathBuilder(); + String path = pathBuilder.toString(); + + if (path.contains("segmented_like_dislike_button.e")) { + // Regular video. ReturnYouTubeDislike videoData = currentVideoData; if (videoData == null) { @@ -169,13 +169,11 @@ public class ReturnYouTubeDislikePatch { return original; // No need to check for Shorts in the context. } - if (Utils.containsAny(conversionContextString, - "|shorts_dislike_button.e", "|reel_dislike_button.e")) { + if (Utils.containsAny(path, "|shorts_dislike_button.e", "|reel_dislike_button.e")) { return getShortsSpan(original, true); } - if (Utils.containsAny(conversionContextString, - "|shorts_like_button.e", "|reel_like_button.e")) { + if (Utils.containsAny(path, "|shorts_like_button.e", "|reel_like_button.e")) { if (!Utils.containsNumber(original)) { Logger.printDebug(() -> "Replacing hidden likes count"); return getShortsSpan(original, false); @@ -230,10 +228,9 @@ public class ReturnYouTubeDislikePatch { /** * Injection point. */ - public static String onRollingNumberLoaded(@NonNull Object conversionContext, - @NonNull String original) { + public static String onRollingNumberLoaded(ContextInterface contextInterface, String original) { try { - CharSequence replacement = onLithoTextLoaded(conversionContext, original, true); + CharSequence replacement = onLithoTextLoaded(contextInterface, original, true); String replacementString = replacement.toString(); if (!replacementString.equals(original)) { @@ -248,7 +245,7 @@ public class ReturnYouTubeDislikePatch { /** * Injection point. - * + *

* Called for all usage of Rolling Number. * Modifies the measured String text width to include the left separator and padding, if needed. */ @@ -489,7 +486,7 @@ public class ReturnYouTubeDislikePatch { /** * Injection point. - * + *

* Called when the user likes or dislikes. * * @param vote int that matches {@link Vote#value} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java index d2288867f0..6b2590e904 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/DescriptionComponentsFilter.java @@ -139,22 +139,14 @@ public final class DescriptionComponentsFilter extends Filter { // If the description panel is opened in a Shorts, PlayerType is 'HIDDEN', // so 'PlayerType.getCurrent().isMaximizedOrFullscreen()' does not guarantee that the description panel is open. // Instead, use the engagement id to check if the description panel is opened. - if (!EngagementPanel.isDescription() - // The user can minimize the player while the engagement panel is open. - // - // In this case, the engagement panel is treated as open. - // (If the player is dismissed, the engagement panel is considered closed) - // - // Therefore, the following exceptions can occur: - // 1. The user opened a regular video and opened the description panel. - // 2. The 'horizontalShelf' elements were hidden. - // 3. The user minimized the player. - // 4. The user manually refreshed the library tab without dismissing the player. - // 5. Since the engagement panel is treated as open, the history shelf is filtered. - // - // To handle these exceptions, filtering is not performed even when the player is minimized. - || PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED - ) { + if (!EngagementPanel.isDescription()) { + return false; + } + + // PlayerType when the description panel is opened: NONE, HIDDEN, + // WATCH_WHILE_MAXIMIZED, WATCH_WHILE_FULLSCREEN, WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN. + PlayerType playerType = PlayerType.getCurrent(); + if (!playerType.isNoneOrHidden() && !playerType.isMaximizedOrFullscreen()) { return false; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/HorizontalShelvesFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/HorizontalShelvesFilter.java new file mode 100644 index 0000000000..9e99122778 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/HorizontalShelvesFilter.java @@ -0,0 +1,102 @@ +package app.revanced.extension.youtube.patches.litho; + +import static app.revanced.extension.youtube.patches.LayoutReloadObserverPatch.isActionBarVisible; + +import app.revanced.extension.shared.patches.litho.Filter; +import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; +import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.EngagementPanel; +import app.revanced.extension.youtube.shared.NavigationBar; +import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; +import app.revanced.extension.youtube.shared.PlayerType; + +@SuppressWarnings("unused") +final class HorizontalShelvesFilter extends Filter { + private final ByteArrayFilterGroupList descriptionBuffers = new ByteArrayFilterGroupList(); + private final ByteArrayFilterGroupList generalBuffers = new ByteArrayFilterGroupList(); + + public HorizontalShelvesFilter() { + StringFilterGroup horizontalShelves = new StringFilterGroup(null, "horizontal_shelf.e"); + addPathCallbacks(horizontalShelves); + + descriptionBuffers.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_ATTRIBUTES_SECTION, + // May no longer work on v20.31+, even though the component is still there. + "cell_video_attribute" + ), + new ByteArrayFilterGroup( + Settings.HIDE_FEATURED_PLACES_SECTION, + "yt_fill_experimental_star", + "yt_fill_star" + ), + new ByteArrayFilterGroup( + Settings.HIDE_GAMING_SECTION, + "yt_outline_experimental_gaming", + "yt_outline_gaming" + ), + new ByteArrayFilterGroup( + Settings.HIDE_MUSIC_SECTION, + "yt_outline_experimental_audio", + "yt_outline_audio" + ), + new ByteArrayFilterGroup( + Settings.HIDE_QUIZZES_SECTION, + "post_base_wrapper_slim" + ) + ); + + generalBuffers.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_CREATOR_STORE_SHELF, + "shopping_item_card_list" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYABLES, + "FEmini_app_destination" + ), + new ByteArrayFilterGroup( + Settings.HIDE_TICKET_SHELF, + "ticket_item.e" + ) + ); + } + + private boolean hideShelves() { + if (!Settings.HIDE_HORIZONTAL_SHELVES.get()) { + return false; + } + // Must check player type first, as search bar can be active behind the player. + if (PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get()) { + return false; + } + // Must check second, as search can be from any tab. + if (NavigationBar.isSearchBarActive()) { + return true; + } + return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY; + } + + @Override + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (contentIndex != 0) { + return false; + } + if (generalBuffers.check(buffer).isFiltered()) { + return true; + } + if (EngagementPanel.isDescription()) { + PlayerType playerType = PlayerType.getCurrent(); + // PlayerType when the description panel is opened: NONE, HIDDEN, + // WATCH_WHILE_MAXIMIZED, WATCH_WHILE_FULLSCREEN, WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN. + if (!playerType.isMaximizedOrFullscreen() && !playerType.isNoneOrHidden()) { + return false; + } + return descriptionBuffers.check(buffer).isFiltered(); + } + return hideShelves(); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java index dda1f46e95..5a00090230 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/LayoutComponentsFilter.java @@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; -import android.util.Pair; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -16,16 +15,12 @@ import android.widget.TextView; import androidx.annotation.Nullable; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicReference; -import app.revanced.extension.shared.ByteTrieSearch; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.StringTrieSearch; import app.revanced.extension.shared.Utils; -import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; @@ -38,10 +33,6 @@ import app.revanced.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class LayoutComponentsFilter extends Filter { - private static final StringTrieSearch mixPlaylistsContextExceptions = new StringTrieSearch( - "V.ED", // Playlist browseId. - "java.lang.ref.WeakReference" - ); private static final ByteArrayFilterGroup mixPlaylistsBufferExceptions = new ByteArrayFilterGroup( null, "cell_description_body", @@ -84,11 +75,6 @@ public final class LayoutComponentsFilter extends Filter { private final StringFilterGroup chipBar; private final StringFilterGroup channelProfile; private final StringFilterGroupList channelProfileGroupList; - private final StringFilterGroup horizontalShelves; - private final ByteArrayFilterGroup playablesBuffer; - private final ByteArrayFilterGroup ticketShelfBuffer; - private final ByteArrayFilterGroup playerShoppingShelfBuffer; - private final ByteTrieSearch descriptionSearch; public LayoutComponentsFilter() { exceptions.addPatterns( @@ -264,12 +250,6 @@ public final class LayoutComponentsFilter extends Filter { "mini_game_card.e" ); - // Playable horizontal shelf header. - playablesBuffer = new ByteArrayFilterGroup( - null, - "FEmini_app_destination" - ); - final var quickActions = new StringFilterGroup( Settings.HIDE_QUICK_ACTIONS, "quick_actions" @@ -317,7 +297,7 @@ public final class LayoutComponentsFilter extends Filter { ); final var forYouShelf = new StringFilterGroup( - Settings.HIDE_FOR_YOU_SHELF, + Settings.HIDE_HORIZONTAL_SHELVES, "mixed_content_shelf" ); @@ -361,51 +341,6 @@ public final class LayoutComponentsFilter extends Filter { ) ); - horizontalShelves = new StringFilterGroup( - null, // Setting is checked in isFiltered() - "horizontal_video_shelf.e", - "horizontal_shelf.e", - "horizontal_shelf_inline.e", - "horizontal_tile_shelf.e" - ); - - ticketShelfBuffer = new ByteArrayFilterGroup( - null, - "ticket_item.e" - ); - - playerShoppingShelfBuffer = new ByteArrayFilterGroup( - null, - "shopping_item_card_list" - ); - - // Work around for unique situation where filtering is based on the setting, - // but it must not fall over to other filters if the setting is _not_ enabled. - // This is only needed for the horizontal shelf that is used so extensively everywhere. - descriptionSearch = new ByteTrieSearch(); - List.of( - new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_star"), - new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_experimental_star"), - new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_gaming"), - new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_experimental_gaming"), - new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_audio"), - new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_experimental_audio"), - new Pair<>(Settings.HIDE_QUIZZES_SECTION, "post_base_wrapper_slim"), - // May no longer work on v20.31+, even though the component is still there. - new Pair<>(Settings.HIDE_ATTRIBUTES_SECTION, "cell_video_attribute") - ).forEach(pair -> { - BooleanSetting setting = pair.first; - descriptionSearch.addPattern(pair.second.getBytes(StandardCharsets.UTF_8), - (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { - //noinspection unchecked - AtomicReference hide = (AtomicReference) callbackParameter; - hide.set(setting.get()); - return true; - } - ); - } - ); - addPathCallbacks( artistCard, audioTrackButton, @@ -422,7 +357,6 @@ public final class LayoutComponentsFilter extends Filter { emergencyBox, expandableMetadata, forYouShelf, - horizontalShelves, imageShelf, infoPanel, latestPosts, @@ -484,47 +418,6 @@ public final class LayoutComponentsFilter extends Filter { && joinMembershipButton.check(buffer).isFiltered(); } - // Horizontal shelves are used everywhere in the app. And to prevent the generic "hide shelves" - // from incorrectly hiding other stuff that has its own hide filters, - // the more specific shelf filters must check first _and_ they must halt falling over - // to other filters if the buffer matches but the setting is off. - if (matchedGroup == horizontalShelves) { - if (contentIndex != 0) return false; - - AtomicReference descriptionFilterResult = new AtomicReference<>(null); - if (descriptionSearch.matches(buffer, descriptionFilterResult)) { - return descriptionFilterResult.get(); - } - - // Check if others are off before searching. - final boolean hideShelves = Settings.HIDE_HORIZONTAL_SHELVES.get(); - final boolean hideTickets = Settings.HIDE_TICKET_SHELF.get(); - final boolean hidePlayables = Settings.HIDE_PLAYABLES.get(); - final boolean hidePlayerShoppingShelf = Settings.HIDE_CREATOR_STORE_SHELF.get(); - if (!hideShelves && !hideTickets && !hidePlayables && !hidePlayerShoppingShelf) - return false; - - if (ticketShelfBuffer.check(buffer).isFiltered()) return hideTickets; - if (playablesBuffer.check(buffer).isFiltered()) return hidePlayables; - if (playerShoppingShelfBuffer.check(buffer).isFiltered()) - return hidePlayerShoppingShelf; - - // 20.31+ when exiting fullscreen after watching for a while or when resuming the app, - // then sometimes the buffer isn't correct and the player shopping shelf is shown. - // If filtering reaches this point then there are no more shelves that could be in the player. - // If shopping shelves are set to hidden and the player is active, then assume - // it's the shopping shelf. - if (hidePlayerShoppingShelf) { - PlayerType type = PlayerType.getCurrent(); - if (type == PlayerType.WATCH_WHILE_MAXIMIZED || type == PlayerType.WATCH_WHILE_FULLSCREEN - || type == PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN) { - return true; - } - } - - return hideShelves && hideShelves(); - } - if (matchedGroup == chipBar) { return contentIndex == 0 && NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY; } @@ -536,7 +429,7 @@ public final class LayoutComponentsFilter extends Filter { * Injection point. * Called from a different place then the other filters. */ - public static boolean filterMixPlaylists(Object conversionContext, @Nullable byte[] buffer) { + public static boolean filterMixPlaylists(@Nullable byte[] buffer) { // Edit: This hook may no longer be needed, and mix playlist filtering // might be possible using the existing litho filters. try { @@ -551,13 +444,7 @@ public final class LayoutComponentsFilter extends Filter { if (mixPlaylists.check(buffer).isFiltered() // Prevent hiding the description of some videos accidentally. - && !mixPlaylistsBufferExceptions.check(buffer).isFiltered() - // Prevent playlist items being hidden, if a mix playlist is present in it. - // Check last since it requires creating a context string. - // - // FIXME: The conversion context passed in does not always generate a valid toString. - // This string check may no longer be needed, or the patch may be broken. - && !mixPlaylistsContextExceptions.matches(conversionContext.toString())) { + && !mixPlaylistsBufferExceptions.check(buffer).isFiltered()) { Logger.printDebug(() -> "Filtered mix playlist"); return true; } @@ -757,31 +644,6 @@ public final class LayoutComponentsFilter extends Filter { : original; } - private static boolean hideShelves() { - // Horizontal shelves are used for music/game links in video descriptions, - // such as https://youtube.com/watch?v=W8kI1na3S2M - if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { - return false; - } - - // Must check search bar after player type, since search results - // can be in the background behind an open player. - if (NavigationBar.isSearchBarActive()) { - return true; - } - - // Do not hide if the navigation back button is visible, - // otherwise the content shelves in the explore/music/courses pages are hidden. - if (NavigationBar.isBackButtonVisible()) { - return false; - } - - // 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 NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY; - } - /** * Injection point. */ @@ -919,8 +781,8 @@ public final class LayoutComponentsFilter extends Filter { /** * Injection point. * - * @param typedString Keywords typed in the search bar. - * @return Whether the setting is enabled and the typed string is empty. + * @param typedString Keywords typed in the search bar. + * @return Whether the setting is enabled and the typed string is empty. */ public static boolean hideYouMayLikeSection(String typedString) { return Settings.HIDE_YOU_MAY_LIKE_SECTION.get() @@ -932,13 +794,13 @@ public final class LayoutComponentsFilter extends Filter { /** * Injection point. * - * @param searchTerm This class contains information related to search terms. - * The {@code toString()} method of this class overrides the search term. - * @param endpoint Endpoint related with the search term. - * For search history, this value is: - * '/complete/deleteitems?client=youtube-android-pb&delq=${searchTerm}&deltok=${token}'. - * For search suggestions, this value is null or empty. - * @return Whether search term is a search history or not. + * @param searchTerm This class contains information related to search terms. + * The {@code toString()} method of this class overrides the search term. + * @param endpoint Endpoint related with the search term. + * For search history, this value is: + * '/complete/deleteitems?client=youtube-android-pb&delq=${searchTerm}&deltok=${token}'. + * For search suggestions, this value is null or empty. + * @return Whether search term is a search history or not. */ public static boolean isSearchHistory(Object searchTerm, String endpoint) { boolean isSearchHistory = endpoint != null && endpoint.contains("/delete"); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java index b01d8db393..ecbe75a259 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ShortsFilter.java @@ -1,26 +1,22 @@ package app.revanced.extension.youtube.patches.litho; +import static app.revanced.extension.youtube.patches.LayoutReloadObserverPatch.isActionBarVisible; import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; import android.view.View; -import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.patches.litho.FilterGroup.*; import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList; -import app.revanced.extension.shared.patches.litho.LithoFilterPatch; -import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.patches.litho.FilterGroupList.StringFilterGroupList; import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; import java.lang.ref.WeakReference; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import app.revanced.extension.shared.Logger; -import app.revanced.extension.youtube.patches.VersionCheckPatch; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.EngagementPanel; import app.revanced.extension.youtube.shared.NavigationBar; @@ -34,16 +30,6 @@ public final class ShortsFilter extends Filter { "reel_action_bar.", // Regular Shorts. "reels_player_overlay_layout." // Shorts ads. }; - private static final Map REEL_ACTION_BUTTONS_MAP = new HashMap<>() { - { - // Like button and Dislike button can be hidden with Litho filter. - // put(0, Settings.HIDE_SHORTS_LIKE_BUTTON); - // put(1, Settings.HIDE_SHORTS_DISLIKE_BUTTON); - put(2, Settings.HIDE_SHORTS_COMMENTS_BUTTON); - put(3, Settings.HIDE_SHORTS_SHARE_BUTTON); - put(4, Settings.HIDE_SHORTS_REMIX_BUTTON); - } - }; private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.e"; /** @@ -90,8 +76,8 @@ public final class ShortsFilter extends Filter { private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList(); private final StringFilterGroup shortsActionBar; - private final StringFilterGroup videoActionButton; - private final ByteArrayFilterGroupList videoActionButtonBuffer = new ByteArrayFilterGroupList(); + private final StringFilterGroup shortsActionButton; + private final StringFilterGroupList shortsActionButtonGroupList = new StringFilterGroupList(); public ShortsFilter() { // @@ -289,7 +275,7 @@ public final class ShortsFilter extends Filter { "yt_outline_template_add_" ); - videoActionButton = new StringFilterGroup( + shortsActionButton = new StringFilterGroup( null, // Can be any of: // button.eml @@ -308,36 +294,26 @@ public final class ShortsFilter extends Filter { shortsCompactFeedVideo, shelfHeaderPath, joinButton, subscribeButton, paidPromotionLabel, livePreview, suggestedAction, pausedOverlayButtons, channelBar, infoPanel, previewComment, autoDubbedLabel, fullVideoLinkLabel, videoTitle, useSoundButton, soundButton, stickers, - reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton + reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton, shortsActionBar ); - // Legacy hiding of Shorts action buttons. Because of 20.31+ buffer changes - // it's currently not possible to hide these using buffer filtering. - // See alternative hiding strategy in hideActionButtons(). - if (!VersionCheckPatch.IS_20_22_OR_GREATER) { - addPathCallbacks(shortsActionBar); - - // - // All other action buttons. - // - videoActionButtonBuffer.addAll( - new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_COMMENTS_BUTTON, - "reel_comment_button", - "youtube_shorts_comment_outline" - ), - new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_SHARE_BUTTON, - "reel_share_button", - "youtube_shorts_share_outline" - ), - new ByteArrayFilterGroup( - Settings.HIDE_SHORTS_REMIX_BUTTON, - "reel_remix_button", - "youtube_shorts_remix_outline" - ) - ); - } + // + // All other action buttons. + // + shortsActionButtonGroupList.addAll( + new StringFilterGroup( + Settings.HIDE_SHORTS_COMMENTS_BUTTON, + "id.reel_comment_button" + ), + new StringFilterGroup( + Settings.HIDE_SHORTS_SHARE_BUTTON, + "id.reel_share_button" + ), + new StringFilterGroup( + Settings.HIDE_SHORTS_REMIX_BUTTON, + "id.reel_remix_button" + ) + ); // // Suggested actions. @@ -482,8 +458,10 @@ public final class ShortsFilter extends Filter { // Video action buttons (comment, share, remix) have the same path. // Like and dislike are separate path filters and don't require buffer searching. if (matchedGroup == shortsActionBar) { - return videoActionButton.check(path).isFiltered() - && videoActionButtonBuffer.check(buffer).isFiltered(); + if (shortsActionButton.check(path).isFiltered()) { + return shortsActionButtonGroupList.check(accessibility).isFiltered(); + } + return false; } if (matchedGroup == suggestedAction) { @@ -525,7 +503,7 @@ public final class ShortsFilter extends Filter { } // Must check player type first, as search bar can be active behind the player. - if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { + if (PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get()) { return EngagementPanel.isDescription() ? hideVideoDescription // Player video description panel opened. : hideHome; // For now, consider Shorts under video player the same as the home feed. @@ -555,58 +533,6 @@ public final class ShortsFilter extends Filter { }; } - /** - * Injection point. - *

- * Hide action buttons by index. - *

- * Regular video action buttons vary in order by video, country, and account. - * Therefore, hiding buttons by index may hide unintended buttons. - *

- * Shorts action buttons are almost always in the same order. - * (From top to bottom: Like, Dislike, Comment, Share, Remix). - * Therefore, we can hide Shorts action buttons by index. - * - * @param pathBuilder Same as pathBuilder used in {@link LithoFilterPatch}. - * @param treeNodeResultList List containing Litho components. - */ - public static void hideActionButtons(StringBuilder pathBuilder, List treeNodeResultList) { - try { - if (pathBuilder == null || pathBuilder.length() == 0 || treeNodeResultList == null) { - return; - } - int size = treeNodeResultList.size(); - - // The minimum size of the target List is 4. - if (size < 4) { - return; - } - String path = pathBuilder.toString(); - - if (!Utils.containsAny(path, REEL_ACTION_BAR_PATHS) - // Regular Shorts: [ComponentType, ComponentType, ComponentType, ComponentType, ComponentType] - // Shorts ads: [ComponentType, ComponentType, ComponentType, ComponentType] (No Remix button) - || !COMPONENT_TYPE.equals(treeNodeResultList.get(0).toString())) { - return; - } - // Removing elements without iterating through the list in reverse order will throw an exception. - for (int i = size - 1; i > -1; i--) { - // treeNodeResult is each button. - Object treeNodeResult = treeNodeResultList.get(i); - if (treeNodeResult != null) { - BooleanSetting setting = REEL_ACTION_BUTTONS_MAP.get(i); - if (setting != null && setting.get()) { - int finalI = i; - Logger.printDebug(() -> "Hiding action button by index: " + finalI + ", key: " + setting.key); - treeNodeResultList.remove(i); - } - } - } - } catch (Exception ex) { - Logger.printException(() -> "hideActionButtons failed", ex); - } - } - /** * Injection point. */ diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index 5bc9bfc6e4..89ba33d934 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -147,7 +147,6 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_CHANNEL_TAB = new BooleanSetting("revanced_hide_channel_tab", FALSE); public static final StringSetting HIDE_CHANNEL_TAB_FILTER_STRINGS = new StringSetting("revanced_hide_channel_tab_filter_strings", "", true, parent(HIDE_CHANNEL_TAB)); public static final BooleanSetting HIDE_COMMUNITY_BUTTON = new BooleanSetting("revanced_hide_community_button", TRUE); - public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", FALSE); public static final BooleanSetting HIDE_JOIN_BUTTON = new BooleanSetting("revanced_hide_join_button", FALSE); public static final BooleanSetting HIDE_LINKS_PREVIEW = new BooleanSetting("revanced_hide_links_preview", TRUE); public static final BooleanSetting HIDE_MEMBERS_SHELF = new BooleanSetting("revanced_hide_members_shelf", TRUE); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt index 555dbfaeaa..95315a86ed 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt @@ -138,5 +138,6 @@ enum class PlayerType { fun isMaximizedOrFullscreen(): Boolean { return this == WATCH_WHILE_MAXIMIZED || this == WATCH_WHILE_FULLSCREEN + || this == WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN } } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/litho/filter/LithoFilterPatch.kt index ebb0edc8a6..8e14dcfcb8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/litho/filter/LithoFilterPatch.kt @@ -1,25 +1,16 @@ package app.revanced.patches.music.misc.litho.filter import app.revanced.patcher.extensions.addInstruction -import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patches.music.misc.extension.sharedExtensionPatch -import app.revanced.patches.music.shared.conversionContextToStringMethod import app.revanced.patches.shared.misc.litho.filter.EXTENSION_CLASS_DESCRIPTOR import app.revanced.patches.shared.misc.litho.filter.lithoFilterPatch -import app.revanced.patches.shared.misc.litho.filter.protobufBufferReferenceLegacyMethod -import app.revanced.util.indexOfFirstInstructionOrThrow -import com.android.tools.smali.dexlib2.Opcode +import app.revanced.patches.shared.misc.litho.filter.protobufBufferReferenceMethod val lithoFilterPatch = lithoFilterPatch( - componentCreateInsertionIndex = { - // No supported version clobbers p2 so we can just do our things before the return instruction. - indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT) - }, - getConversionContextToStringMethod = BytecodePatchContext::conversionContextToStringMethod::get, - insertProtobufHook = { - protobufBufferReferenceLegacyMethod.addInstruction( + insertLegacyProtobufHook = { + protobufBufferReferenceMethod.addInstruction( 0, - "invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V", + "invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtobufBuffer(Ljava/nio/ByteBuffer;)V", ) }, ) { diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/context/ConversionContextPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/context/ConversionContextPatch.kt new file mode 100644 index 0000000000..f5752a1a1c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/context/ConversionContextPatch.kt @@ -0,0 +1,154 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.shared.misc.litho.context + + +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableClassDef +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable +import app.revanced.patcher.after +import app.revanced.patcher.allOf +import app.revanced.patcher.classDef +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.field +import app.revanced.patcher.firstClassDef +import app.revanced.patcher.firstMethodDeclaratively +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.returnType +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch +import app.revanced.util.findFieldFromToString +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod + +internal const val EXTENSION_CONTEXT_INTERFACE = + $$"Lapp/revanced/extension/shared/ConversionContext$ContextInterface;" + +internal lateinit var conversionContextClassDef: MutableClassDef + +val conversionContextPatch = bytecodePatch( + description = "Hooks the method to use the conversion context in an extension.", +) { + dependsOn(sharedExtensionPatch()) + + apply { + conversionContextClassDef = conversionContextToStringMethod.classDef + + val identifierField = conversionContextToStringMethod + .findFieldFromToString(IDENTIFIER_PROPERTY) + val stringBuilderField = conversionContextClassDef + .fields.single { field -> field.type == "Ljava/lang/StringBuilder;" } + + // The conversionContext class can be used as is in most versions. + if (conversionContextClassDef.superclass == "Ljava/lang/Object;") { + arrayOf( + identifierField, + stringBuilderField + ).map { + + } + conversionContextClassDef.apply { + // Add interface and helper methods to allow extension code to call obfuscated methods. + interfaces += EXTENSION_CONTEXT_INTERFACE + + arrayOf( + Triple( + "patch_getIdentifier", + "Ljava/lang/String;", + identifierField + ), + Triple( + "patch_getPathBuilder", + "Ljava/lang/StringBuilder;", + stringBuilderField + ) + ).forEach { (interfaceMethodName, interfaceMethodReturnType, classFieldReference) -> + ImmutableMethod( + type, + interfaceMethodName, + listOf(), + interfaceMethodReturnType, + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + addInstructions( + 0, + """ + iget-object v0, p0, $classFieldReference + return-object v0 + """ + ) + }.let(methods::add) + + } + } + } else { + // In some special versions, such as YouTube 20.41, it inherits from an abstract class, + // in which case a helper method is added to the abstract class. + + // Since fields cannot be accessed directly in an abstract class, abstract methods are linked. + val stringBuilderMethodName = conversionContextClassDef.firstMethodDeclaratively { + parameterTypes() + returnType("Ljava/lang/String;") + instructions( + allOf(Opcode.IGET_OBJECT(), field { this == identifierField }), + after(Opcode.RETURN_OBJECT()), + ) + }.name + + val identifierMethodName = conversionContextClassDef.firstMethodDeclaratively { + parameterTypes() + returnType("Ljava/lang/StringBuilder;") + instructions( + allOf(Opcode.IGET_OBJECT(), field { this == stringBuilderField }), + after(Opcode.RETURN_OBJECT()), + ) + }.name + + conversionContextClassDef = firstClassDef(conversionContextClassDef.superclass!!) + + conversionContextClassDef.apply { + // Add interface and helper methods to allow extension code to call obfuscated methods. + interfaces += EXTENSION_CONTEXT_INTERFACE + + arrayOf( + Triple( + "patch_getIdentifier", + "Ljava/lang/String;", + identifierMethodName + ), + Triple( + "patch_getPathBuilder", + "Ljava/lang/StringBuilder;", + stringBuilderMethodName + ) + ).forEach { (interfaceMethodName, interfaceMethodReturnType, classMethodName) -> + ImmutableMethod( + type, + interfaceMethodName, + listOf(), + interfaceMethodReturnType, + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + addInstructions( + 0, + """ + invoke-virtual {p0}, $type->$classMethodName()$interfaceMethodReturnType + move-result-object v0 + return-object v0 + """ + ) + }.let(methods::add) + } + } + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/context/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/context/Fingerprints.kt new file mode 100644 index 0000000000..a45f71ec89 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/context/Fingerprints.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.shared.misc.litho.context + +import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.name +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType + +internal const val IDENTIFIER_PROPERTY = ", identifierProperty=" + + +internal val BytecodePatchContext.conversionContextToStringMethod by gettingFirstImmutableMethodDeclaratively( + ", widthConstraint=", + ", heightConstraint=", + ", templateLoggerFactory=", + ", rootDisposableContainer=", + IDENTIFIER_PROPERTY +) { + name("toString") + parameterTypes() + returnType("Ljava/lang/String;") + instructions("ConversionContext{"(String::contains)) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt index 9e128382e8..b559e0bac1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/Fingerprints.kt @@ -1,9 +1,13 @@ package app.revanced.patches.shared.misc.litho.filter import app.revanced.patcher.* +import app.revanced.patcher.firstMethodComposite +import app.revanced.patcher.instructions import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal val BytecodePatchContext.accessibilityIdMethodMatch by composingFirstMethod { @@ -16,29 +20,59 @@ internal val BytecodePatchContext.accessibilityIdMethodMatch by composingFirstMe ) } -internal fun BytecodePatchContext.getAccessibilityTextMethodMatch(accessibilityIdMethod: MethodReference) = firstMethodComposite { - returnType("V") - custom { - // 'public final synthetic' or 'public final bridge synthetic'. - AccessFlags.SYNTHETIC.isSet(accessFlags) +internal fun BytecodePatchContext.getAccessibilityTextMethodMatch(accessibilityIdMethod: MethodReference) = + firstMethodComposite { + returnType("V") + custom { + // 'public final synthetic' or 'public final bridge synthetic'. + AccessFlags.SYNTHETIC.isSet(accessFlags) + } + instructions( + allOf( + Opcode.INVOKE_INTERFACE(), + method { parameterTypes.isEmpty() && returnType == "Ljava/lang/String;" } + ), + afterAtMost(5, method { this == accessibilityIdMethod }) + ) } + + +context(_: BytecodePatchContext) +internal fun ClassDef.getEmptyComponentMethod() = firstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returnType("L") + parameterTypes("L") +} + +internal val BytecodePatchContext.emptyComponentParentMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR) + parameterTypes() + instructions("EmptyComponent"()) +} + +fun BytecodePatchContext.getComponentCreateMethodMatch(accessibilityIdMethod: MethodReference) = firstMethodComposite { + returnType("L") instructions( - allOf( - Opcode.INVOKE_INTERFACE(), - method { parameterTypes.isEmpty() && returnType == "Ljava/lang/String;" } + Opcode.IF_EQZ(), + afterAtMost( + 5, + allOf(Opcode.CHECK_CAST(), type(accessibilityIdMethod.definingClass)) ), - afterAtMost(5, method { this == accessibilityIdMethod }) + Opcode.RETURN_OBJECT(), + "Element missing correct type extension"(), + "Element missing type"() ) } + internal val BytecodePatchContext.lithoFilterInitMethod by gettingFirstMethodDeclaratively { definingClass("/LithoFilterPatch;") accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) } -internal val BytecodePatchContext.protobufBufferReferenceMethodMatch by composingFirstMethod { +internal val BytecodePatchContext.protobufBufferEncodeMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("V") - parameterTypes("[B") + returnType("[B") + parameterTypes() var methodDefiningClass = "" custom { @@ -51,32 +85,17 @@ internal val BytecodePatchContext.protobufBufferReferenceMethodMatch by composin Opcode.IGET_OBJECT(), field { definingClass == methodDefiningClass && type == "Lcom/google/android/libraries/elements/adl/UpbMessage;" }, ), - method { definingClass == "Lcom/google/android/libraries/elements/adl/UpbMessage;" && name == "jniDecode" }, + method { definingClass == "Lcom/google/android/libraries/elements/adl/UpbMessage;" && name == "jniEecode" }, ) } -/** - * Matches a method that use the protobuf of our component. - */ -internal val BytecodePatchContext.protobufBufferReferenceLegacyMethod by gettingFirstMethodDeclaratively { +internal val BytecodePatchContext.protobufBufferReferenceMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") parameterTypes("I", "Ljava/nio/ByteBuffer;") opcodes(Opcode.IPUT, Opcode.INVOKE_VIRTUAL, Opcode.MOVE_RESULT, Opcode.SUB_INT_2ADDR) } -internal val BytecodePatchContext.emptyComponentMethod by gettingFirstImmutableMethodDeclaratively { - accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR) - parameterTypes() - instructions("EmptyComponent"()) - custom { immutableClassDef.methods.filter { AccessFlags.STATIC.isSet(it.accessFlags) }.size == 1 } -} - -internal val BytecodePatchContext.componentCreateMethod by gettingFirstMethod( - "Element missing correct type extension", - "Element missing type", -) - internal val BytecodePatchContext.lithoThreadExecutorMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) parameterTypes("I", "I", "I") diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt index 29548aa469..b68ec03a2f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -2,34 +2,22 @@ package app.revanced.patches.shared.misc.litho.filter -import app.revanced.util.getFreeRegisterProvider import app.revanced.com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable -import app.revanced.patcher.afterAtMost -import app.revanced.patcher.allOf import app.revanced.patcher.classDef -import app.revanced.patcher.custom +import app.revanced.util.getFreeRegisterProvider import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.methodReference import app.revanced.patcher.extensions.removeInstructions -import app.revanced.patcher.extensions.typeReference -import app.revanced.patcher.firstImmutableClassDef -import app.revanced.patcher.firstMethodComposite +import app.revanced.patcher.firstClassDef import app.revanced.patcher.immutableClassDef -import app.revanced.patcher.instructions -import app.revanced.patcher.invoke -import app.revanced.patcher.method import app.revanced.patcher.patch.BytecodePatchBuilder import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.returnType import app.revanced.patches.shared.misc.extension.sharedExtensionPatch +import app.revanced.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE +import app.revanced.patches.shared.misc.litho.context.conversionContextPatch import app.revanced.util.addInstructionsAtControlFlowLabel -import app.revanced.util.findFieldFromToString -import app.revanced.util.indexOfFirstInstructionReversedOrThrow -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.immutable.value.ImmutableBooleanEncodedValue @@ -51,26 +39,20 @@ internal const val EXTENSION_CLASS_DESCRIPTOR = /** * A patch that allows to filter Litho components based on their identifier or path. * - * @param componentCreateInsertionIndex The index to insert the filtering code in the component create method. - * @param insertProtobufHook This method injects a setProtoBuffer call in the protobuf decoding logic. - * @param getConversionContextToStringMethod The getter of the conversion context toString method. + * @param insertLegacyProtobufHook Hook legacy protobuf buffer into the extension to be used for filtering for older versions of the app. * @param getExtractIdentifierFromBuffer Whether to extract the identifier from the protobuf buffer. * @param executeBlock The additional execution block of the patch. * @param block The additional block to build the patch. */ internal fun lithoFilterPatch( - componentCreateInsertionIndex: Method.() -> Int, - insertProtobufHook: BytecodePatchContext.() -> Unit, + insertLegacyProtobufHook: BytecodePatchContext.() -> Unit, executeBlock: BytecodePatchContext.() -> Unit = {}, - getConversionContextToStringMethod: BytecodePatchContext.() -> Method, getExtractIdentifierFromBuffer: () -> Boolean = { false }, block: BytecodePatchBuilder.() -> Unit = {}, ) = bytecodePatch( description = "Hooks the method which parses the bytes into a ComponentContext to filter components.", ) { - dependsOn( - sharedExtensionPatch(), - ) + dependsOn(sharedExtensionPatch(), conversionContextPatch) /** * The following patch inserts a hook into the method that parses the bytes into a ComponentContext. @@ -92,7 +74,7 @@ internal fun lithoFilterPatch( * class SomeOtherClass { * // Called before ComponentContextParser.parseComponent() method. * public void someOtherMethod(ByteBuffer byteBuffer) { - * ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch. + * ExtensionClass.setProtobugBuffer(byteBuffer); // Inserted by this patch. * ... * } * } @@ -134,45 +116,23 @@ internal fun lithoFilterPatch( } } - // Tell the extension whether to extract the identifier from the buffer. - if (getExtractIdentifierFromBuffer()) { - lithoFilterInitMethod.classDef.fields.first { it.name == "EXTRACT_IDENTIFIER_FROM_BUFFER" } - .initialValue = ImmutableBooleanEncodedValue.forBoolean(true).toMutable() - } + // region Pass the buffer into extension. - // Add an interceptor to steal the protobuf of our component. - insertProtobufHook() + insertLegacyProtobufHook() - // Hook the method that parses bytes into a ComponentContext. - // Allow the method to run to completion, and override the - // return value with an empty component if it should be filtered. - // It is important to allow the original code to always run to completion, - // otherwise high memory usage and poor app performance can occur. + // endregion - val conversionContextToStringMethod = getConversionContextToStringMethod() + // region Modify the create component method and + // if the component is filtered then return an empty component. - // Find the identifier/path fields of the conversion context. - val conversionContextIdentifierField = conversionContextToStringMethod - .findFieldFromToString("identifierProperty=") + val builderMethodDescriptor = + emptyComponentParentMethod.immutableClassDef.getEmptyComponentMethod() - val conversionContextPathBuilderField = conversionContextToStringMethod.immutableClassDef - .fields.single { field -> field.type == "Ljava/lang/StringBuilder;" } - - // Find class and methods to create an empty component. - val builderMethodDescriptor = emptyComponentMethod.immutableClassDef.methods.single { - // The only static method in the class. - method -> - AccessFlags.STATIC.isSet(method.accessFlags) - } - - val emptyComponentField = firstImmutableClassDef { - // Only one field that matches. - type == builderMethodDescriptor.returnType - }.fields.single() + val emptyComponentField = firstClassDef(builderMethodDescriptor.returnType).fields.single() // Find the method call that gets the value of 'buttonViewModel.accessibilityId'. val accessibilityIdMethod = accessibilityIdMethodMatch.let { - it.immutableMethod.getInstruction(it[0]).methodReference!! + it.method.getInstruction(it[0]).methodReference!! } // There's a method in the same class that gets the value of 'buttonViewModel.accessibilityText'. @@ -182,35 +142,26 @@ internal fun lithoFilterPatch( it.method.getInstruction(it[0]).methodReference } - componentCreateMethod.apply { - val insertIndex = componentCreateInsertionIndex() + getComponentCreateMethodMatch(accessibilityIdMethod).let { + val insertIndex = it[2] + val buttonViewModelIndex = it[1] + val nullCheckIndex = it[0] - // Directly access the class related with the buttonViewModel from this method. - // This is within 10 lines of insertIndex. - val buttonViewModelIndex = indexOfFirstInstructionReversedOrThrow(insertIndex) { - opcode == Opcode.CHECK_CAST && - typeReference?.type == accessibilityIdMethod.definingClass - } val buttonViewModelRegister = - getInstruction(buttonViewModelIndex).registerA + it.method.getInstruction(buttonViewModelIndex).registerA val accessibilityIdIndex = buttonViewModelIndex + 2 - // This is an index that checks if there is accessibility-related text. - // This is within 10 lines of buttonViewModelIndex. - val nullCheckIndex = indexOfFirstInstructionReversedOrThrow( - buttonViewModelIndex, Opcode.IF_EQZ - ) - - val registerProvider = getFreeRegisterProvider( + val registerProvider = it.method.getFreeRegisterProvider( insertIndex, 3, buttonViewModelRegister ) + val contextRegister = registerProvider.getFreeRegister() + val bufferRegister = registerProvider.getFreeRegister() val freeRegister = registerProvider.getFreeRegister() - val identifierRegister = registerProvider.getFreeRegister() - val pathRegister = registerProvider.getFreeRegister() + // Find a free register to store the accessibilityId and accessibilityText. // This is before the insertion index. - val accessibilityRegisterProvider = getFreeRegisterProvider( + val accessibilityRegisterProvider = it.method.getFreeRegisterProvider( nullCheckIndex, 2, registerProvider.getUsedAndUnAvailableRegisters() @@ -218,23 +169,31 @@ internal fun lithoFilterPatch( val accessibilityIdRegister = accessibilityRegisterProvider.getFreeRegister() val accessibilityTextRegister = accessibilityRegisterProvider.getFreeRegister() - addInstructionsAtControlFlowLabel( + it.method.addInstructionsAtControlFlowLabel( insertIndex, """ - move-object/from16 v$freeRegister, p2 # ConversionContext parameter - - # In YouTube 20.41 the field is the abstract superclass. - # Verify it's the expected subclass just in case. - instance-of v$identifierRegister, v$freeRegister, ${conversionContextToStringMethod.immutableClassDef.type} - if-eqz v$identifierRegister, :unfiltered - - iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField - iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField - invoke-static { v$identifierRegister, v$accessibilityIdRegister, v$accessibilityTextRegister, v$pathRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->isFiltered(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/StringBuilder;)Z + move-object/from16 v$bufferRegister, p3 + + # Verify it's the expected subclass just in case. + instance-of v$freeRegister, v$bufferRegister, ${protobufBufferEncodeMethod.definingClass} + if-eqz v$freeRegister, :empty_buffer + + check-cast v$bufferRegister, ${protobufBufferEncodeMethod.definingClass} + invoke-virtual { v$bufferRegister }, $protobufBufferEncodeMethod + move-result-object v$bufferRegister + goto :hook + + :empty_buffer + const/4 v$freeRegister, 0x0 + new-array v$bufferRegister, v$freeRegister, [B + + :hook + move-object/from16 v$contextRegister, p2 + invoke-static { v$contextRegister, v$bufferRegister, v$accessibilityIdRegister, v$accessibilityTextRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(${EXTENSION_CONTEXT_INTERFACE}[BLjava/lang/String;Ljava/lang/String;)Z move-result v$freeRegister if-eqz v$freeRegister, :unfiltered - # Return an empty component + # Return an empty component. move-object/from16 v$freeRegister, p1 invoke-static { v$freeRegister }, $builderMethodDescriptor move-result-object v$freeRegister @@ -247,7 +206,7 @@ internal fun lithoFilterPatch( ) // If there is text related to accessibility, get the accessibilityId and accessibilityText. - addInstructions( + it.method.addInstructions( accessibilityIdIndex, """ # Get accessibilityId @@ -262,7 +221,7 @@ internal fun lithoFilterPatch( // If there is no accessibility-related text, // both accessibilityId and accessibilityText use empty values. - addInstructions( + it.method.addInstructions( nullCheckIndex, """ const-string v$accessibilityIdRegister, "" @@ -271,6 +230,13 @@ internal fun lithoFilterPatch( ) } + if (getExtractIdentifierFromBuffer()) { + lithoFilterInitMethod.classDef.fields.first { it.name == "EXTRACT_IDENTIFIER_FROM_BUFFER" } + .initialValue = ImmutableBooleanEncodedValue.forBoolean(true).toMutable() + } + + // endregion + // TODO: Check if needed in music. // Change Litho thread executor to 1 thread to fix layout issue in unpatched YouTube. lithoThreadExecutorMethod.addInstructions( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt index c9239da9ba..dad11bba62 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt @@ -18,6 +18,7 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.contexthook.Endpoint import app.revanced.patches.youtube.misc.contexthook.addOSNameHook import app.revanced.patches.shared.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.layout.hide.shelves.hideHorizontalShelvesPatch import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch import app.revanced.patches.youtube.misc.engagement.addEngagementPanelIdHook import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch @@ -50,6 +51,7 @@ private val hideAdsResourcePatch = resourcePatch { addResourcesPatch, hookClientContextPatch, engagementPanelHookPatch, + hideHorizontalShelvesPatch ) apply { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt index c88d7a8bec..b950a21e67 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt @@ -102,6 +102,14 @@ internal val BytecodePatchContext.parseElementFromBufferMethodMatch by composing afterAtMost(1, Opcode.INVOKE_INTERFACE()), after(Opcode.MOVE_RESULT_OBJECT()), "Failed to parse Element"(String::startsWith), + allOf( + Opcode.INVOKE_STATIC(), + method { + returnType.startsWith("L") && parameterTypes.size == 1 + && parameterTypes[0].startsWith("L") + } + ), + afterAtMost(4, Opcode.RETURN_OBJECT()) ) } @@ -215,7 +223,9 @@ internal val BytecodePatchContext.searchBoxTypingStringMethodMatch by composingF parameterTypes("L") instructions( allOf(Opcode.IGET_OBJECT(), field { type == "Ljava/util/Collection;" }), - afterAtMost(5, method { toString() == "Ljava/util/ArrayList;->(Ljava/util/Collection;)V" }), + afterAtMost( + 5, + method { toString() == "Ljava/util/ArrayList;->(Ljava/util/Collection;)V" }), allOf(Opcode.IGET_OBJECT(), field { type == "Ljava/lang/String;" }), afterAtMost(5, method { toString() == "Ljava/lang/String;->isEmpty()Z" }), ResourceType.DIMEN("suggestion_category_divider_height") diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index ea7fd4c919..63a6275d31 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -12,6 +12,7 @@ import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.* import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.youtube.layout.hide.shelves.hideHorizontalShelvesPatch import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch @@ -19,6 +20,7 @@ import app.revanced.patches.youtube.misc.playservice.is_20_21_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.addInstructionsAtControlFlowLabel import app.revanced.util.findFreeRegister import app.revanced.util.findInstructionIndicesReversedOrThrow import app.revanced.util.getReference @@ -83,6 +85,7 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( versionCheckPatch, engagementPanelHookPatch, resourceMappingPatch, + hideHorizontalShelvesPatch, ), filterClasses = setOf( LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR, @@ -215,7 +218,6 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( ) ), SwitchPreference("revanced_hide_community_button"), - SwitchPreference("revanced_hide_for_you_shelf"), SwitchPreference("revanced_hide_join_button"), SwitchPreference("revanced_hide_links_preview"), SwitchPreference("revanced_hide_members_shelf"), @@ -273,27 +275,20 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( parseElementFromBufferMethodMatch.let { it.method.apply { - val startIndex = it[0] - val insertIndex = startIndex + 1 + val insertIndex = it[0] val byteArrayParameter = "p3" - val conversionContextRegister = - getInstruction(startIndex).registerA - val returnEmptyComponentInstruction = - instructions.last { it.opcode == Opcode.INVOKE_STATIC } + val returnEmptyComponentIndex = it[4] + val returnEmptyComponentInstruction = getInstruction(returnEmptyComponentIndex) + val returnEmptyComponentRegister = (returnEmptyComponentInstruction as FiveRegisterInstruction).registerC - val freeRegister = - findFreeRegister( - insertIndex, - conversionContextRegister, - returnEmptyComponentRegister - ) + val freeRegister = findFreeRegister(insertIndex, returnEmptyComponentRegister) - addInstructionsWithLabels( + addInstructionsAtControlFlowLabel( insertIndex, """ - invoke-static { v$conversionContextRegister, $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->filterMixPlaylists(Ljava/lang/Object;[B)Z + invoke-static { $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->filterMixPlaylists([B)Z move-result v$freeRegister if-eqz v$freeRegister, :show move-object v$returnEmptyComponentRegister, p1 # Required for 19.47 diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shelves/HideHorizontalShelvesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shelves/HideHorizontalShelvesPatch.kt new file mode 100644 index 0000000000..0e07c459be --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shelves/HideHorizontalShelvesPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.youtube.layout.hide.shelves + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.litho.observer.layoutReloadObserverPatch +import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch + +private const val FILTER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/litho/HorizontalShelvesFilter;" + +internal val hideHorizontalShelvesPatch = bytecodePatch { + dependsOn( + sharedExtensionPatch, + lithoFilterPatch, + playerTypeHookPatch, + navigationBarHookPatch, + engagementPanelHookPatch, + layoutReloadObserverPatch, + ) + + apply { + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt index d730022f92..6a63dee5cf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt @@ -7,21 +7,6 @@ import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef -internal val BytecodePatchContext.componentContextParserMethod by gettingFirstImmutableMethodDeclaratively { - returnType("L") - instructions( - "Failed to parse Element proto."(), - "Cannot read theme key from model."() - ) -} - -context(_: BytecodePatchContext) -internal fun ClassDef.getTreeNodeResultListMethod() = firstMethodDeclaratively { - accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) - returnType("Ljava/util/List;") - instructions(allOf(Opcode.INVOKE_STATIC(), method("nCopies"))) -} - internal val BytecodePatchContext.shortsBottomBarContainerMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt index be6e539361..5672db45ab 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt @@ -12,7 +12,6 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch -import app.revanced.patches.music.shared.conversionContextToStringMethod import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference @@ -21,6 +20,7 @@ import app.revanced.patches.shared.misc.litho.filter.addLithoFilter import app.revanced.patches.youtube.misc.engagement.engagementPanelHookPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.litho.observer.layoutReloadObserverPatch import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch import app.revanced.patches.youtube.misc.playservice.* import app.revanced.patches.youtube.misc.settings.PreferenceScreen @@ -146,11 +146,11 @@ private const val FILTER_CLASS_DESCRIPTOR = @Suppress("unused") val hideShortsComponentsPatch = bytecodePatch( name = "Hide Shorts components", - description = "Adds options to hide components related to Shorts. " + - "Patching version 20.21.37 or lower can hide more Shorts player button types." + description = "Adds options to hide components related to Shorts." ) { dependsOn( sharedExtensionPatch, + layoutReloadObserverPatch, lithoFilterPatch, hideShortsComponentsResourcePatch, resourceMappingPatch, @@ -204,42 +204,6 @@ val hideShortsComponentsPatch = bytecodePatch( // endregion - // region Hide action buttons. - - if (is_20_22_or_greater) { - componentContextParserMethod.immutableClassDef.getTreeNodeResultListMethod().apply { - val conversionContextPathBuilderField = - conversionContextToStringMethod.immutableClassDef - .fields.single { field -> field.type == "Ljava/lang/StringBuilder;" } - - val insertIndex = implementation!!.instructions.lastIndex - val listRegister = getInstruction(insertIndex).registerA - - val registerProvider = getFreeRegisterProvider(insertIndex, 2) - val freeRegister = registerProvider.getFreeRegister() - val pathRegister = registerProvider.getFreeRegister() - - addInstructionsAtControlFlowLabel( - insertIndex, - """ - move-object/from16 v$freeRegister, p2 - - # In YouTube 20.41 field is the abstract superclass. - # Verify it's the expected subclass just in case. - instance-of v$pathRegister, v$freeRegister, ${conversionContextToStringMethod.immutableClassDef} - if-eqz v$pathRegister, :ignore - - iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField - invoke-static { v$pathRegister, v$listRegister }, ${FILTER_CLASS_DESCRIPTOR}->hideActionButtons(Ljava/lang/StringBuilder;Ljava/util/List;)V - :ignore - nop - """ - ) - } - } - - // endregion - // region Hide the navigation bar. // Hook to get the pivotBar view. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt index b216bcb201..dc3bb3f103 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -16,12 +16,14 @@ import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPref import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.litho.filter.addLithoFilter import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE +import app.revanced.patches.shared.misc.litho.context.conversionContextClassDef +import app.revanced.patches.shared.misc.litho.context.conversionContextPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch import app.revanced.patches.youtube.misc.playservice.* import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch -import app.revanced.patches.youtube.shared.conversionContextToStringMethod import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateMethodMatch import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.revanced.patches.youtube.video.videoid.hookVideoId @@ -43,7 +45,6 @@ private const val EXTENSION_CLASS_DESCRIPTOR = private const val FILTER_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter;" -@Suppress("ObjectPropertyName") val returnYouTubeDislikePatch = bytecodePatch( name = "Return YouTube Dislike", description = "Adds an option to show the dislike count of videos with Return YouTube Dislike.", @@ -52,6 +53,7 @@ val returnYouTubeDislikePatch = bytecodePatch( settingsPatch, sharedExtensionPatch, addResourcesPatch, + conversionContextPatch, lithoFilterPatch, videoIdPatch, playerTypeHookPatch, @@ -126,16 +128,9 @@ val returnYouTubeDislikePatch = bytecodePatch( // This hook handles all situations, as it's where the created Spans are stored and later reused. // Find the field name of the conversion context. - val conversionContextClass = conversionContextToStringMethod.immutableClassDef - val textComponentConversionContextField = - textComponentConstructorMethod.immutableClassDef.fields.find { - it.type == conversionContextClass.type || - // 20.41+ uses superclass field type. - it.type == conversionContextClass.superclass - } ?: throw PatchException("Could not find conversion context field") - - val conversionContextPathBuilderField = conversionContextToStringMethod.immutableClassDef - .fields.single { field -> field.type == "Ljava/lang/StringBuilder;" } + val textComponentConversionContextField = textComponentConstructorMethod.immutableClassDef.fields.find { + it.type == conversionContextClassDef.type + } ?: throw PatchException("Could not find conversion context field") // Old pre 20.40 and lower hook. // 21.05 clobbers p0 (this) register. @@ -184,7 +179,7 @@ val returnYouTubeDislikePatch = bytecodePatch( # Copy conversion context. move-object/from16 v$conversionContext, p0 iget-object v$conversionContext, v$conversionContext, $textComponentConversionContextField - invoke-static { v$conversionContext, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + invoke-static { v$conversionContext, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(${EXTENSION_CONTEXT_INTERFACE}Ljava/lang/CharSequence;)Ljava/lang/CharSequence; move-result-object v$charSequenceRegister :ignore @@ -211,16 +206,15 @@ val returnYouTubeDislikePatch = bytecodePatch( val insertIndex = it[1] val charSequenceRegister = getInstruction(insertIndex).registerD - val conversionContextPathRegister = + val conversionContextRegister = findFreeRegister(insertIndex, charSequenceRegister) addInstructions( insertIndex, """ - move-object/from16 v$conversionContextPathRegister, p0 - iget-object v$conversionContextPathRegister, v$conversionContextPathRegister, $conversionContextField - iget-object v$conversionContextPathRegister, v$conversionContextPathRegister, $conversionContextPathBuilderField - invoke-static { v$conversionContextPathRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-object/from16 v$conversionContextRegister, p0 + iget-object v$conversionContextRegister, v$conversionContextRegister, $conversionContextField + invoke-static { v$conversionContextRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(${EXTENSION_CONTEXT_INTERFACE}Ljava/lang/CharSequence;)Ljava/lang/CharSequence; move-result-object v$charSequenceRegister """ ) @@ -261,7 +255,7 @@ val returnYouTubeDislikePatch = bytecodePatch( insertIndex, """ iget-object v$freeRegister, v$charSequenceInstanceRegister, $charSequenceFieldReference - invoke-static { v$conversionContextRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberLoaded(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; + invoke-static { v$conversionContextRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberLoaded(${EXTENSION_CONTEXT_INTERFACE}Ljava/lang/String;)Ljava/lang/String; move-result-object v$freeRegister iput-object v$freeRegister, v$charSequenceInstanceRegister, $charSequenceFieldReference """, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt index 4813ed99ca..ca51c638bd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt @@ -3,6 +3,9 @@ package app.revanced.patches.youtube.misc.litho.filter import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method +import kotlin.properties.ReadOnlyProperty internal val BytecodePatchContext.lithoComponentNameUpbFeatureFlagMethod by gettingFirstMethodDeclaratively { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt index 772e5b6efc..3fc0a4bcce 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt @@ -3,49 +3,25 @@ package app.revanced.patches.youtube.misc.litho.filter import app.revanced.patcher.extensions.addInstruction -import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patches.youtube.shared.conversionContextToStringMethod import app.revanced.patches.shared.misc.litho.filter.EXTENSION_CLASS_DESCRIPTOR import app.revanced.patches.shared.misc.litho.filter.lithoFilterPatch -import app.revanced.patches.shared.misc.litho.filter.protobufBufferReferenceLegacyMethod -import app.revanced.patches.shared.misc.litho.filter.protobufBufferReferenceMethodMatch +import app.revanced.patches.shared.misc.litho.filter.protobufBufferReferenceMethod import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.* -import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.insertLiteralOverride import app.revanced.util.returnLate -import com.android.tools.smali.dexlib2.Opcode val lithoFilterPatch = lithoFilterPatch( - componentCreateInsertionIndex = { - if (is_19_17_or_greater) { - indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT) - } else { - // 19.16 clobbers p2 so must check at start of the method and not at the return index. - 0 + insertLegacyProtobufHook = { + if (!is_20_22_or_greater) { + // Non-native buffer. + protobufBufferReferenceMethod.addInstruction( + 0, + "invoke-static { p2 }, ${EXTENSION_CLASS_DESCRIPTOR}->setProtobufBuffer(Ljava/nio/ByteBuffer;)V", + ) } }, - insertProtobufHook = { - if (is_20_22_or_greater) { - // Hook method that bridges between UPB buffer native code and FB Litho. - // Method is found in 19.25+, but is forcefully turned off for 20.21 and lower. - protobufBufferReferenceMethodMatch.let { - // Hook the buffer after the call to jniDecode(). - it.method.addInstruction( - it[-1] + 1, - "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer([B)V", - ) - } - } - - // Legacy non-native buffer. - protobufBufferReferenceLegacyMethod.addInstruction( - 0, - "invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V", - ) - }, - getConversionContextToStringMethod = BytecodePatchContext::conversionContextToStringMethod::get, - getExtractIdentifierFromBuffer = { is_20_21_or_greater }, + getExtractIdentifierFromBuffer = { is_20_22_or_greater }, executeBlock = { // region A/B test of new Litho native code. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/lazily/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/lazily/Fingerprints.kt new file mode 100644 index 0000000000..3da09f9ebc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/lazily/Fingerprints.kt @@ -0,0 +1,39 @@ +package app.revanced.patches.youtube.misc.litho.lazily + +import app.revanced.patcher.accessFlags +import app.revanced.patcher.allOf +import app.revanced.patcher.definingClass +import app.revanced.patcher.firstMethodDeclaratively +import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.method +import app.revanced.patcher.name +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef + +internal val BytecodePatchContext.componentContextParserMethod by gettingFirstMethodDeclaratively { + returnType("L") + instructions( + "Failed to parse Element proto."(), + "Cannot read theme key from model."() + ) +} + +context(_: BytecodePatchContext) +internal fun ClassDef.getTreeNodeResultListMethod() = firstMethodDeclaratively { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returnType("Ljava/util/List;") + instructions( + allOf(Opcode.INVOKE_STATIC(), method { name == "nCopies" }) + ) +} + +internal val BytecodePatchContext.lazilyConvertedElementPatchMethod by gettingFirstMethodDeclaratively { + name("onLazilyConvertedElementLoaded") + definingClass(EXTENSION_CLASS_DESCRIPTOR) + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/lazily/LazilyConvertedElementHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/lazily/LazilyConvertedElementHookPatch.kt new file mode 100644 index 0000000000..a08f1bf510 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/lazily/LazilyConvertedElementHookPatch.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.youtube.misc.litho.lazily + +import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod +import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.instructions +import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE +import app.revanced.patches.shared.misc.litho.context.conversionContextPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.getFreeRegisterProvider +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/LazilyConvertedElementPatch;" + +private lateinit var lazilyConvertedElementLoadedMethod: MutableMethod + +internal val lazilyConvertedElementHookPatch = bytecodePatch( + description = "Hooks the LazilyConvertedElement tree node lists to the extension." +) { + dependsOn( + sharedExtensionPatch, + conversionContextPatch + ) + + apply { + componentContextParserMethod.immutableClassDef.getTreeNodeResultListMethod().apply { + val insertIndex = instructions.lastIndex + val listRegister = getInstruction(insertIndex).registerA + + val registerProvider = getFreeRegisterProvider(insertIndex, 1) + val freeRegister = registerProvider.getFreeRegister() + + addInstructionsAtControlFlowLabel( + insertIndex, + """ + move-object/from16 v$freeRegister, p2 + invoke-static { v$freeRegister, v$listRegister }, $EXTENSION_CLASS_DESCRIPTOR->onTreeNodeResultLoaded(${EXTENSION_CONTEXT_INTERFACE}Ljava/util/List;)V + """ + ) + } + + lazilyConvertedElementLoadedMethod = lazilyConvertedElementPatchMethod + } +} + +internal fun hookTreeNodeResult(descriptor: String) = + lazilyConvertedElementLoadedMethod.addInstruction( + 0, + "invoke-static { p0, p1 }, $descriptor(Ljava/lang/String;Ljava/util/List;)V" + ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/observer/LayoutReloadObserverPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/observer/LayoutReloadObserverPatch.kt new file mode 100644 index 0000000000..98ed92f7a2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/observer/LayoutReloadObserverPatch.kt @@ -0,0 +1,24 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.misc.litho.observer + +import app.revanced.patches.youtube.misc.litho.lazily.hookTreeNodeResult +import app.revanced.patches.youtube.misc.litho.lazily.lazilyConvertedElementHookPatch +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/LayoutReloadObserverPatch;" + +val layoutReloadObserverPatch = bytecodePatch( + description = "Hooks a method to detect in the extension when the RecyclerView at the bottom of the player is redrawn.", +) { + dependsOn( + sharedExtensionPatch, + lazilyConvertedElementHookPatch + ) + + apply { + hookTreeNodeResult("$EXTENSION_CLASS_DESCRIPTOR->onLazilyConvertedElementLoaded") + } +} diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 6c05ad9dae..aa0a6cd445 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -271,6 +271,7 @@ However, enabling this will also log some user data such as your IP address." @@ -468,10 +469,6 @@ However, enabling this will also log some user data such as your IP address."Hide Community button Community button is hidden Community button is shown - - Hide \'For You\' shelf - For You shelf is hidden - For You shelf is shown Hide Join button Join button is hidden @@ -1856,7 +1853,6 @@ Video playback with AV1 may stutter or drop frames." Library Liked music Playlists - Podcasts Search Subscriptions