diff --git a/CHANGELOG.md b/CHANGELOG.md index bfebce7e69..6ea2c0d35e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [6.1.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.1.0...v6.1.1-dev.1) (2026-03-19) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Set `ANDROID_REEL` client as default ([#6878](https://github.com/ReVanced/revanced-patches/issues/6878)) ([a9aeb32](https://github.com/ReVanced/revanced-patches/commit/a9aeb325de1160262c4db9b4b60c6c5e39730620)) + # [6.1.0](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.1.0) (2026-03-18) diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeHeaderPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeHeaderPatch.java new file mode 100644 index 0000000000..74d9f458d3 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeHeaderPatch.java @@ -0,0 +1,50 @@ +package app.revanced.extension.music.patches; + +import java.util.Objects; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.ResourceType; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.music.settings.Settings; + +public class ChangeHeaderPatch { + public enum HeaderLogo { + DEFAULT(null), + REVANCED("revanced_header_dark"), + CUSTOM("revanced_header_custom_dark"); + + private final String drawableName; + + HeaderLogo(String drawableName) { + this.drawableName = drawableName; + } + + private Integer getDrawableId() { + if (drawableName == null) { + return null; + } + + int id = Utils.getResourceIdentifier(ResourceType.DRAWABLE, drawableName); + if (id == 0) { + Logger.printException(() -> + "Header drawable not found: " + drawableName + ); + Settings.HEADER_LOGO.resetToDefault(); + return null; + } + + return id; + } + } + + /** + * Injection point. + */ + @SuppressWarnings("unused") + public static int getHeaderDrawableId(int original) { + return Objects.requireNonNullElse( + Settings.HEADER_LOGO.get().getDrawableId(), + original + ); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeStartPagePatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeStartPagePatch.java new file mode 100644 index 0000000000..db3622f91b --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeStartPagePatch.java @@ -0,0 +1,98 @@ +package app.revanced.extension.music.patches; + +import static java.lang.Boolean.TRUE; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public final class ChangeStartPagePatch { + private static final String SHORTCUT_ACTION = "com.google.android.youtube.music.action.shortcut"; + private static final String SHORTCUT_CLASS_DESCRIPTOR = "com.google.android.apps.youtube.music.activities.InternalMusicActivity"; + private static final String SHORTCUT_TYPE = "com.google.android.youtube.music.action.shortcut_type"; + private static final String SHORTCUT_ID_SEARCH = "Eh4IBRDTnQEYmgMiEwiZn+H0r5WLAxVV5OcDHcHRBmPqpd25AQA="; + private static final int SHORTCUT_TYPE_SEARCH = 1; + + + public enum StartPage { + DEFAULT("", null), + CHARTS("FEmusic_charts", TRUE), + EXPLORE("FEmusic_explore", TRUE), + HISTORY("FEmusic_history", TRUE), + LIBRARY("FEmusic_library_landing", TRUE), + PLAYLISTS("FEmusic_liked_playlists", TRUE), + PODCASTS("FEmusic_non_music_audio", TRUE), + SUBSCRIPTIONS("FEmusic_library_corpus_artists", TRUE), + EPISODES_FOR_LATER("VLSE", TRUE), + LIKED_MUSIC("VLLM", TRUE), + SEARCH("", false); + + @NonNull + final String id; + + @Nullable + final Boolean isBrowseId; + + StartPage(@NonNull String id, @Nullable Boolean isBrowseId) { + this.id = id; + this.isBrowseId = isBrowseId; + } + + private boolean isBrowseId() { + return TRUE.equals(isBrowseId); + } + } + + private static final String ACTION_MAIN = "android.intent.action.MAIN"; + + public static String overrideBrowseId(@Nullable String original) { + var startPage = Settings.CHANGE_START_PAGE.get(); + + if (!startPage.isBrowseId()) { + return original; + } + + if (!"FEmusic_home".equals(original)) { + return original; + } + + String overrideBrowseId = startPage.id; + if (overrideBrowseId.isEmpty()) { + return original; + } + + Logger.printDebug(() -> "Changing browseId to: " + startPage.name()); + return overrideBrowseId; + } + + public static void overrideIntentActionOnCreate(@NonNull Activity activity, + @Nullable Bundle savedInstanceState) { + if (savedInstanceState != null) return; + + var startPage = Settings.CHANGE_START_PAGE.get(); + if (startPage != StartPage.SEARCH) return; + + var originalIntent = activity.getIntent(); + if (originalIntent == null) return; + + if (ACTION_MAIN.equals(originalIntent.getAction())) { + Logger.printDebug(() -> "Cold start: Launching search activity directly"); + var searchIntent = new Intent(); + + searchIntent.setAction(SHORTCUT_ACTION); + searchIntent.setClassName(activity, SHORTCUT_CLASS_DESCRIPTOR); + searchIntent.setPackage(activity.getPackageName()); + searchIntent.putExtra(SHORTCUT_TYPE, SHORTCUT_TYPE_SEARCH); + searchIntent.putExtra(SHORTCUT_ACTION, SHORTCUT_ID_SEARCH); + + activity.startActivity(searchIntent); + } + } +} \ No newline at end of file diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/ForciblyEnableMiniplayerPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/ForciblyEnableMiniplayerPatch.java new file mode 100644 index 0000000000..8b6b9c10a1 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/ForciblyEnableMiniplayerPatch.java @@ -0,0 +1,13 @@ +package app.revanced.extension.music.patches; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class ForciblyEnableMiniplayerPatch { + /** + * Injection point + */ + public static boolean enableForcedMiniplayerPatch(boolean original) { + return Settings.FORCIBLY_ENABLE_MINIPLAYER.get() || original; + } +} \ No newline at end of file diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java index 7decd29b8a..b8fcf48ea5 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -2,6 +2,8 @@ package app.revanced.extension.music.settings; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; +import static app.revanced.extension.music.patches.ChangeHeaderPatch.*; +import static app.revanced.extension.music.patches.ChangeStartPagePatch.*; import static app.revanced.extension.shared.settings.Setting.parent; import app.revanced.extension.shared.settings.YouTubeAndMusicSettings; @@ -16,6 +18,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true); // General + public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true); public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, true); public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true); public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_music_hide_history_button", FALSE, true); @@ -28,9 +31,11 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true); public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true); + public static final EnumSetting HEADER_LOGO = new EnumSetting<>("revanced_header_logo", HeaderLogo.DEFAULT, true); // Player public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true); + public static final BooleanSetting FORCIBLY_ENABLE_MINIPLAYER = new BooleanSetting("revanced_music_forcibly_enable_miniplayer", FALSE, true); public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true); // Miscellaneous 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..8233d8eab8 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ConversionContext.java @@ -0,0 +1,21 @@ +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(); + + default boolean isHomeFeedOrRelatedVideo() { + return toString().contains("horizontalCollectionSwipeProtector=null"); + } + + default boolean isSubscriptionOrLibrary() { + return toString().contains("heightConstraint=null"); + } + } +} 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 cf65db8a4c..a8dba1ded3 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 @@ -358,6 +358,41 @@ public class Utils { return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName)); } + /** + * Checks if a specific app package is installed and enabled on the device. + * + * @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) { + Context context = getContext(); + if (context == null || !isNotEmpty(packageName)) { + return false; + } + + try { + PackageManager pm = context.getPackageManager(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return pm.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0)).enabled; + } else { + return pm.getApplicationInfo(packageName, 0).enabled; + } + } catch (PackageManager.NameNotFoundException e) { + 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/SanitizeSharingLinksPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java index b0bcbc6f04..32ff22c38c 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java @@ -11,6 +11,7 @@ public final class SanitizeSharingLinksPatch { private static final LinkSanitizer sanitizer = new LinkSanitizer( "si", + "is", // New (localized?) tracking parameter. "feature" // Old tracking parameter name, and may be obsolete. ); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java index beb623a799..176414aaba 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java @@ -12,6 +12,8 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import app.revanced.extension.shared.ConversionContext; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.StringTrieSearch; import app.revanced.extension.shared.Utils; @@ -172,14 +174,21 @@ public final class CustomFilter extends Filter { if (!groups.isEmpty()) { CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]); - Logger.printDebug(()-> "Using Custom filters: " + Arrays.toString(groupsArray)); + Logger.printDebug(() -> "Using Custom filters: " + Arrays.toString(groupsArray)); addPathCallbacks(groupsArray); } } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered( + ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { // All callbacks are custom filter groups. CustomFilterGroup custom = (CustomFilterGroup) matchedGroup; diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java index b34ca9bdd7..fe1fb66725 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java @@ -1,5 +1,7 @@ package app.revanced.extension.shared.patches.litho; +import app.revanced.extension.shared.ConversionContext; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; @@ -9,16 +11,16 @@ import java.util.List; /** * Filters litho based components. - * + *

* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} * and {@link #addPathCallbacks(StringFilterGroup...)}. - * + *

* To filter {@link FilterContentType#PROTOBUFFER} or {@link FilterContentType#ACCESSIBILITY}, first add a callback to * either an identifier or a path. - * Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Then inside {@link #isFiltered(ContextInterface, String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) * or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern). - * + *

* All callbacks must be registered before the constructor completes. */ public abstract class Filter { @@ -42,7 +44,7 @@ public abstract class Filter { public final List pathCallbacks = new ArrayList<>(); /** - * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Adds callbacks to {@link #isFiltered(ContextInterface, String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * if any of the groups are found. */ protected final void addIdentifierCallbacks(StringFilterGroup... groups) { @@ -50,7 +52,7 @@ public abstract class Filter { } /** - * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Adds callbacks to {@link #isFiltered(ContextInterface, String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * if any of the groups are found. */ protected final void addPathCallbacks(StringFilterGroup... groups) { @@ -64,6 +66,7 @@ public abstract class Filter { *

* Method is called off the main thread. * + * @param contextInterface The interface to get the Litho conversion context. * @param identifier Litho identifier. * @param accessibility Accessibility string, or an empty string if not present for the component. * @param buffer Protocol buffer. @@ -72,8 +75,8 @@ public abstract class Filter { * @param contentIndex Matched index of the identifier or path. * @return True if the litho component should be filtered out. */ - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, String identifier, String accessibility, String path, byte[] buffer, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { return true; } } 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 e1b329ee54..76e80fbd91 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; @@ -65,23 +63,48 @@ public final class LithoFilterPatch { final int minimumAscii = 32; // 32 = space character final int maximumAscii = 126; // 127 = delete character final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include. + // Logger ignores text past 4096 bytes on each line. Must wrap lines otherwise logging is clipped. + final int preferredLineLength = 3000; // Preferred length before wrapping on next substring. + final int maxLineLength = 3300; // Hard limit to line wrap in the middle of substring. String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering. final int length = buffer.length; + final int lastIndex = length - 1; int start = 0; - int end = 0; - while (end < length) { - int value = buffer[end]; - if (value < minimumAscii || value > maximumAscii || end == length - 1) { - if (end - start >= minimumAsciiStringLength) { - for (int i = start; i < end; i++) { + int currentLineLength = 0; + + for (int end = 0; end < length; end++) { + final int value = buffer[end]; + final boolean isAscii = (value >= minimumAscii && value <= maximumAscii); + final boolean atEnd = (end == lastIndex); + + if (!isAscii || atEnd) { + int wordEnd = end + ((atEnd && isAscii) ? 1 : 0); + + if (wordEnd - start >= minimumAsciiStringLength) { + for (int i = start; i < wordEnd; i++) { builder.append((char) buffer[i]); + currentLineLength++; + + // Hard line limit. Hard wrap the current substring to next logger line. + if (currentLineLength >= maxLineLength) { + builder.append('\n'); + currentLineLength = 0; + } } + + // Wrap after substring if over preferred limit. + if (currentLineLength >= preferredLineLength) { + builder.append('\n'); + currentLineLength = 0; + } + builder.append(delimitingCharacter); + currentLineLength++; } + start = end + 1; } - end++; } } } @@ -123,11 +146,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 +164,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 +216,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 +237,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/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java index 53a980e3c2..e3cb9b5b78 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -2,7 +2,7 @@ package app.revanced.extension.shared.settings; import static app.revanced.extension.shared.StringRef.str; -import android.content.Context; +import android.app.Activity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -122,18 +122,18 @@ public abstract class Setting { /** * Called after all settings have been imported. */ - void settingsImported(@Nullable Context context); + void settingsImported(@Nullable Activity context); /** * Called after all settings have been exported. */ - void settingsExported(@Nullable Context context); + void settingsExported(@Nullable Activity context); } private static final List importExportCallbacks = new ArrayList<>(); /** - * Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}. + * Adds a callback for {@link #importFromJSON(Activity, String)} and {@link #exportToJson(Activity)}. */ public static void addImportExportCallback(ImportExportCallback callback) { importExportCallbacks.add(Objects.requireNonNull(callback)); @@ -413,7 +413,7 @@ public abstract class Setting { json.put(importExportKey, value); } - public static String exportToJson(@Nullable Context alertDialogContext) { + public static String exportToJson(@Nullable Activity alertDialogContext) { try { JSONObject json = new JSONObject(); for (Setting setting : allLoadedSettingsSorted()) { @@ -439,11 +439,17 @@ public abstract class Setting { String export = json.toString(0); - // Remove the outer JSON braces to make the output more compact, - // and leave less chance of the user forgetting to copy it - return export.substring(2, export.length() - 2); + if (export.startsWith("{") && export.endsWith("}")) { + // Remove the outer JSON braces to make the output more compact, + // and leave less chance of the user forgetting to copy it + export = export.substring(1, export.length() - 1); + } + + export = export.replaceAll("^\\n+", "").replaceAll("\\n+$", ""); + + return export + ","; } catch (JSONException e) { - Logger.printException(() -> "Export failure", e); // should never happen + Logger.printException(() -> "Export failure", e); // Should never happen return ""; } } @@ -451,10 +457,16 @@ public abstract class Setting { /** * @return if any settings that require a reboot were changed. */ - public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) { + public static boolean importFromJSON(Activity alertDialogContext, String settingsJsonString) { try { - if (!settingsJsonString.matches("[\\s\\S]*\\{")) { - settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces + settingsJsonString = settingsJsonString.trim(); + + if (settingsJsonString.endsWith(",")) { + settingsJsonString = settingsJsonString.substring(0, settingsJsonString.length() - 1); + } + + if (!settingsJsonString.trim().startsWith("{")) { + settingsJsonString = "{\n" + settingsJsonString + "\n}"; // Restore outer JSON braces } JSONObject json = new JSONObject(settingsJsonString); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java index a515471a00..7e47a5e8c0 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -3,24 +3,38 @@ package app.revanced.extension.shared.settings.preference; import static app.revanced.extension.shared.StringRef.str; import android.annotation.SuppressLint; +import android.app.Activity; import android.app.Dialog; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.os.SystemClock; +import android.preference.EditTextPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; import android.preference.PreferenceGroup; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.preference.SwitchPreference; -import android.preference.EditTextPreference; -import android.preference.ListPreference; import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.ListView; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Objects; +import java.util.Scanner; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.ResourceType; @@ -33,6 +47,36 @@ import app.revanced.extension.shared.ui.CustomDialog; @SuppressWarnings("deprecation") public abstract class AbstractPreferenceFragment extends PreferenceFragment { + private static class DebouncedListView extends ListView { + private long lastClick; + + public DebouncedListView(Context context) { + super(context); + + setId(android.R.id.list); // Required so PreferenceFragment recognizes it. + + // Match the default layout params + setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + } + + @Override + public boolean performItemClick(View view, int position, long id) { + final long now = SystemClock.elapsedRealtime(); + if (now - lastClick < 500) { + return true; // Ignore fast double click. + } + lastClick = now; + + return super.performItemClick(view, position, id); + } + } + + @SuppressLint("StaticFieldLeak") + public static AbstractPreferenceFragment instance; + /** * Indicates that if a preference changes, * to apply the change from the Setting to the UI component. @@ -56,6 +100,12 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { @Nullable protected static CharSequence restartDialogTitle, restartDialogMessage, restartDialogButtonText, confirmDialogTitle; + private static final int READ_REQUEST_CODE = 42; + private static final int WRITE_REQUEST_CODE = 43; + private String existingSettings = ""; + + private EditText currentImportExportEditText; + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { if (updatingPreference) { @@ -198,8 +248,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { return listPref.getValue().equals(defaultValueString); } - throw new IllegalStateException("Must override method to handle " - + "preference type: " + pref.getClass()); + throw new IllegalStateException("Must override method to handle preference type: " + pref.getClass()); } /** @@ -332,10 +381,235 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { dialogPair.first.show(); } + /** + * Import / Export Subroutines + */ + @NonNull + private Button createDialogButton(Context context, String text, int marginLeft, int marginRight, View.OnClickListener listener) { + int height = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 36f, context.getResources().getDisplayMetrics()); + int paddingHorizontal = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 16f, context.getResources().getDisplayMetrics()); + float radius = android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 20f, context.getResources().getDisplayMetrics()); + + Button btn = new Button(context, null, 0); + btn.setText(text); + btn.setAllCaps(false); + btn.setTextSize(14); + btn.setSingleLine(true); + btn.setEllipsize(android.text.TextUtils.TruncateAt.END); + btn.setGravity(android.view.Gravity.CENTER); + btn.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); + btn.setTextColor(Utils.isDarkModeEnabled() ? android.graphics.Color.WHITE : android.graphics.Color.BLACK); + + android.graphics.drawable.GradientDrawable bg = new android.graphics.drawable.GradientDrawable(); + bg.setCornerRadius(radius); + bg.setColor(Utils.getCancelOrNeutralButtonBackgroundColor()); + btn.setBackground(bg); + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, height, 1.0f); + params.setMargins(marginLeft, 0, marginRight, 0); + btn.setLayoutParams(params); + btn.setOnClickListener(listener); + + return btn; + } + public void showImportExportTextDialog() { + try { + Activity context = getActivity(); + // Must set text before showing dialog, + // otherwise text is non-selectable if this preference is later reopened. + existingSettings = Setting.exportToJson(context); + currentImportExportEditText = getEditText(context); + + // Create a custom dialog with the EditText. + Pair dialogPair = CustomDialog.create( + context, + str("revanced_pref_import_export_title"), // Title. + null, // No message (EditText replaces it). + currentImportExportEditText, // Pass the EditText. + str("revanced_settings_save"), // OK button text. + () -> importSettingsText(context, currentImportExportEditText.getText().toString()), // OK button action. + () -> {}, // Cancel button action (dismiss only). + str("revanced_settings_import_copy"), // Neutral button (Copy) text. + () -> Utils.setClipboard(currentImportExportEditText.getText().toString()), // Neutral button (Copy) action. Show the user the settings in JSON format. + true // Dismiss dialog when onNeutralClick. + ); + + LinearLayout fileButtonsContainer = getLinearLayout(context); + int margin = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 4f, context.getResources().getDisplayMetrics()); + + Button btnExport = createDialogButton(context, str("revanced_settings_export_file"), 0, margin, v -> exportActivity()); + Button btnImport = createDialogButton(context, str("revanced_settings_import_file"), margin, 0, v -> importActivity()); + + fileButtonsContainer.addView(btnExport); + fileButtonsContainer.addView(btnImport); + + dialogPair.second.addView(fileButtonsContainer, 2); + + dialogPair.first.setOnDismissListener(d -> currentImportExportEditText = null); + + // If there are no settings yet, then show the on-screen keyboard and bring focus to + // the edit text. This makes it easier to paste saved settings after a reinstallation. + dialogPair.first.setOnShowListener(dialogInterface -> { + if (existingSettings.isEmpty() && currentImportExportEditText != null) { + currentImportExportEditText.postDelayed(() -> { + if (currentImportExportEditText != null) { + currentImportExportEditText.requestFocus(); + android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) imm.showSoftInput(currentImportExportEditText, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT); + } + }, 100); + } + }); + + // Show the dialog. + dialogPair.first.show(); + } catch (Exception ex) { + Logger.printException(() -> "showImportExportTextDialog failure", ex); + } + } + + @NonNull + private static LinearLayout getLinearLayout(Context context) { + LinearLayout fileButtonsContainer = new LinearLayout(context); + fileButtonsContainer.setOrientation(LinearLayout.HORIZONTAL); + LinearLayout.LayoutParams fbParams = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + + int marginTop = (int) android.util.TypedValue.applyDimension(android.util.TypedValue.COMPLEX_UNIT_DIP, 16f, context.getResources().getDisplayMetrics()); + fbParams.setMargins(0, marginTop, 0, 0); + fileButtonsContainer.setLayoutParams(fbParams); + return fileButtonsContainer; + } + + @NonNull + private EditText getEditText(Context context) { + EditText editText = new EditText(context); + editText.setText(existingSettings); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + editText.setAutofillHints((String) null); + } + editText.setInputType(android.text.InputType.TYPE_CLASS_TEXT | + android.text.InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | + android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE); + editText.setSingleLine(false); + editText.setTextSize(14); + return editText; + } + + public void exportActivity() { + try { + Setting.exportToJson(getActivity()); + + String formatDate = new java.text.SimpleDateFormat("yyyy-MM-dd", java.util.Locale.US).format(new java.util.Date()); + String fileName = "revanced_Settings_" + formatDate + ".txt"; + + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TITLE, fileName); + startActivityForResult(intent, WRITE_REQUEST_CODE); + } catch (Exception ex) { + Logger.printException(() -> "exportActivity failure", ex); + } + } + + public void importActivity() { + try { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, READ_REQUEST_CODE); + } catch (Exception ex) { + Logger.printException(() -> "importActivity failure", ex); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return new DebouncedListView(getActivity()); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == WRITE_REQUEST_CODE && resultCode == android.app.Activity.RESULT_OK && data != null) { + exportTextToFile(data.getData()); + } else if (requestCode == READ_REQUEST_CODE && resultCode == android.app.Activity.RESULT_OK && data != null) { + importTextFromFile(data.getData()); + } + } + + protected static void showLocalizedToast(String resourceKey, String fallbackMessage) { + if (Utils.getResourceIdentifier(ResourceType.STRING, resourceKey) != 0) { + Utils.showToastLong(str(resourceKey)); + } else { + Utils.showToastLong(fallbackMessage); + } + } + + private void exportTextToFile(android.net.Uri uri) { + try { + OutputStream out = getContext().getContentResolver().openOutputStream(uri); + if (out != null) { + String textToExport = existingSettings; + if (currentImportExportEditText != null) { + textToExport = currentImportExportEditText.getText().toString(); + } + out.write(textToExport.getBytes(StandardCharsets.UTF_8)); + out.close(); + + showLocalizedToast("revanced_settings_export_file_success", "Settings exported successfully"); + } + } catch (Exception e) { + showLocalizedToast("revanced_settings_export_file_failed", "Failed to export settings"); + Logger.printException(() -> "exportTextToFile failure", e); + } + } + + @SuppressWarnings("CharsetObjectCanBeUsed") + private void importTextFromFile(android.net.Uri uri) { + try { + InputStream in = getContext().getContentResolver().openInputStream(uri); + if (in != null) { + Scanner scanner = new Scanner(in, StandardCharsets.UTF_8.name()).useDelimiter("\\A"); + String result = scanner.hasNext() ? scanner.next() : ""; + in.close(); + + if (currentImportExportEditText != null) { + currentImportExportEditText.setText(result); + showLocalizedToast("revanced_settings_import_file_success", "Settings imported successfully, tap Save to apply"); + } else { + importSettingsText(getContext(), result); + } + } + } catch (Exception e) { + showLocalizedToast("revanced_settings_import_file_failed", "Failed to import settings"); + Logger.printException(() -> "importTextFromFile failure", e); + } + } + + private void importSettingsText(Context context, String replacementSettings) { + try { + existingSettings = Setting.exportToJson(null); + if (replacementSettings.equals(existingSettings)) { + return; + } + settingImportInProgress = true; + final boolean rebootNeeded = Setting.importFromJSON(getActivity(), replacementSettings); + if (rebootNeeded) { + showRestartDialog(context); + } + } catch (Exception ex) { + Logger.printException(() -> "importSettingsText failure", ex); + } finally { + settingImportInProgress = false; + } + } + @SuppressLint("ResourceType") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + instance = this; try { PreferenceManager preferenceManager = getPreferenceManager(); preferenceManager.setSharedPreferencesName(Setting.preferences.name); @@ -354,6 +628,9 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { @Override public void onDestroy() { + if (instance == this) { + instance = null; + } getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener); super.onDestroy(); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java index 1044ba424e..c187acf608 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java @@ -4,40 +4,13 @@ import static app.revanced.extension.shared.StringRef.str; import android.app.Dialog; import android.content.Context; -import android.os.Build; -import android.os.Bundle; -import android.preference.EditTextPreference; import android.preference.Preference; -import android.text.InputType; import android.util.AttributeSet; -import android.util.Pair; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; -import android.widget.LinearLayout; import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.Utils; -import app.revanced.extension.shared.settings.Setting; -import app.revanced.extension.shared.ui.CustomDialog; @SuppressWarnings({"unused", "deprecation"}) -public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { - - private String existingSettings; - - private void init() { - setSelectable(true); - - EditText editText = getEditText(); - editText.setTextIsSelectable(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - editText.setAutofillHints((String) null); - } - editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - editText.setTextSize(14); - - setOnPreferenceClickListener(this); - } +public class ImportExportPreference extends Preference implements Preference.OnPreferenceClickListener { public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); @@ -56,78 +29,20 @@ public class ImportExportPreference extends EditTextPreference implements Prefer init(); } + private void init() { + setOnPreferenceClickListener(this); + } + @Override public boolean onPreferenceClick(Preference preference) { try { - // Must set text before showing dialog, - // otherwise text is non-selectable if this preference is later reopened. - existingSettings = Setting.exportToJson(getContext()); - getEditText().setText(existingSettings); + if (AbstractPreferenceFragment.instance != null) { + AbstractPreferenceFragment.instance.showImportExportTextDialog(); + } } catch (Exception ex) { - Logger.printException(() -> "showDialog failure", ex); + Logger.printException(() -> "onPreferenceClick failure", ex); } + return true; } - - @Override - protected void showDialog(Bundle state) { - try { - Context context = getContext(); - EditText editText = getEditText(); - - // Create a custom dialog with the EditText. - Pair dialogPair = CustomDialog.create( - context, - str("revanced_pref_import_export_title"), // Title. - null, // No message (EditText replaces it). - editText, // Pass the EditText. - str("revanced_settings_import"), // OK button text. - () -> importSettings(context, editText.getText().toString()), // OK button action. - () -> {}, // Cancel button action (dismiss only). - str("revanced_settings_import_copy"), // Neutral button (Copy) text. - () -> { - // Neutral button (Copy) action. Show the user the settings in JSON format. - Utils.setClipboard(editText.getText()); - }, - true // Dismiss dialog when onNeutralClick. - ); - - // If there are no settings yet, then show the on screen keyboard and bring focus to - // the edit text. This makes it easier to paste saved settings after a reinstall. - dialogPair.first.setOnShowListener(dialogInterface -> { - if (existingSettings.isEmpty()) { - editText.postDelayed(() -> { - editText.requestFocus(); - - InputMethodManager inputMethodManager = (InputMethodManager) - editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); - }, 100); - } - }); - - // Show the dialog. - dialogPair.first.show(); - } catch (Exception ex) { - Logger.printException(() -> "showDialog failure", ex); - } - } - - private void importSettings(Context context, String replacementSettings) { - try { - if (replacementSettings.equals(existingSettings)) { - return; - } - AbstractPreferenceFragment.settingImportInProgress = true; - - final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings); - if (rebootNeeded) { - AbstractPreferenceFragment.showRestartDialog(context); - } - } catch (Exception ex) { - Logger.printException(() -> "importSettings failure", ex); - } finally { - AbstractPreferenceFragment.settingImportInProgress = false; - } - } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java index 9abd430719..837a5b9066 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java @@ -28,7 +28,7 @@ public enum ClientType { Build.VERSION.RELEASE, String.valueOf(Build.VERSION.SDK_INT), Build.ID, - "20.44.38", + "20.45.36", // This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay. // This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from ReVanced. // This means that the GVS server can strengthen its validation of the ANDROID_REEL client. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java index d7cf0faba1..240e9b9b08 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ChangeFormFactorPatch.java @@ -2,13 +2,10 @@ package app.revanced.extension.youtube.patches; import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; -import android.view.View; - import androidx.annotation.Nullable; -import java.util.Objects; +import java.util.Optional; -import app.revanced.extension.shared.Logger; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.NavigationBar; import app.revanced.extension.youtube.shared.PlayerType; @@ -20,7 +17,7 @@ public class ChangeFormFactorPatch { /** * Unmodified, and same as un-patched. */ - DEFAULT(null), + DEFAULT(null, false), /** *

          * Some changes include:
@@ -28,60 +25,56 @@ public class ChangeFormFactorPatch {
          * - watch history is missing.
          * - feed thumbnails fade in.
          */
-        UNKNOWN(0),
-        SMALL(1),
-        LARGE(2),
+        UNKNOWN(0, true),
+        SMALL(1, false),
+        LARGE(2, false),
         /**
          * Cars with 'Google built-in'.
          * Layout seems identical to {@link #UNKNOWN}
          * even when using an Android Automotive device.
          */
-        AUTOMOTIVE(3),
-        WEARABLE(4);
+        AUTOMOTIVE(3, true),
+        WEARABLE(4, true);
 
         @Nullable
         final Integer formFactorType;
+        final boolean isBroken;
 
-        FormFactor(@Nullable Integer formFactorType) {
+        FormFactor(@Nullable Integer formFactorType, boolean isBroken) {
             this.formFactorType = formFactorType;
+            this.isBroken = isBroken;
         }
     }
 
+    private static final FormFactor FORM_FACTOR = Settings.CHANGE_FORM_FACTOR.get();
+
     @Nullable
-    private static final Integer FORM_FACTOR_TYPE = Settings.CHANGE_FORM_FACTOR.get().formFactorType;
-    private static final boolean USING_AUTOMOTIVE_TYPE = Objects.requireNonNull(
-            FormFactor.AUTOMOTIVE.formFactorType).equals(FORM_FACTOR_TYPE);
+    private static final Integer FORM_FACTOR_TYPE = FORM_FACTOR.formFactorType;
+    private static final boolean IS_BROKEN_FORM_FACTOR = FORM_FACTOR.isBroken;
 
     /**
      * Injection point.
+     * 

+ * Called before {@link #replaceBrokenFormFactor(int)}. + * Called from all endpoints. */ - public static int getFormFactor(int original) { - if (FORM_FACTOR_TYPE == null) return original; - - if (USING_AUTOMOTIVE_TYPE) { - // Do not change if the player is opening or is opened, - // otherwise the video description cannot be opened. - PlayerType current = PlayerType.getCurrent(); - if (current.isMaximizedOrFullscreen() || current == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) { - Logger.printDebug(() -> "Using original form factor for player"); - return original; - } - - if (!NavigationBar.isSearchBarActive()) { - // Automotive type shows error 400 when opening a channel page and using some explore tab. - // This is a bug in unpatched YouTube that occurs on actual Android Automotive devices. - // Work around the issue by using the original form factor if not in search and the - // navigation back button is present. - if (NavigationBar.isBackButtonVisible()) { - Logger.printDebug(() -> "Using original form factor, as back button is visible without search present"); - return original; - } - - // Do not change library tab otherwise watch history is hidden. - // Do this check last since the current navigation button is required. - if (NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) { - return original; - } + public static int getUniversalFormFactor(int original) { + if (FORM_FACTOR_TYPE == null) { + return original; + } + if (IS_BROKEN_FORM_FACTOR + && !PlayerType.getCurrent().isMaximizedOrFullscreen() + && !NavigationBar.isSearchBarActive()) { + // Automotive type shows error 400 when opening a channel page and using some explore tab. + // This is a bug in unpatched YouTube that occurs on actual Android Automotive devices. + // Work around the issue by using the tablet form factor if not in search and the + // navigation back button is present. + if (NavigationBar.isBackButtonVisible() + // Do not change library tab otherwise watch history is hidden. + // Do this check last since the current navigation button is required. + || NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) { + // The form factor most similar to AUTOMOTIVE is LARGE, so it is replaced with LARGE. + return Optional.ofNullable(FormFactor.LARGE.formFactorType).orElse(original); } } @@ -90,16 +83,31 @@ public class ChangeFormFactorPatch { /** * Injection point. + *

+ * Called after {@link #getUniversalFormFactor(int)}. + * Called from the '/get_watch', '/guide', '/next' and '/reel' endpoints. + *

+ * The '/guide' endpoint relates to navigation buttons. + * If {@link #IS_BROKEN_FORM_FACTOR} is true in this endpoint, + * the explore button (which is deprecated) will be shown. + *

+ * The '/get_watch' and '/next' endpoints relate to elements below the player (channel bar, comments, related videos). + * If {@link #IS_BROKEN_FORM_FACTOR} is true in this endpoint, + * the video description panel will not open. + *

+ * The '/reel' endpoint relates to Shorts player. + * If {@link #IS_BROKEN_FORM_FACTOR} is true in this endpoint, + * the Shorts comment panel will not open. */ - public static void navigationTabCreated(NavigationButton button, View tabView) { - // On first startup of the app the navigation buttons are fetched and updated. - // If the user immediately opens the 'You' or opens a video, then the call to - // update the navigation buttons will use the non-automotive form factor - // and the explore tab is missing. - // Fixing this is not so simple because of the concurrent calls for the player and You tab. - // For now, always hide the explore tab. - if (USING_AUTOMOTIVE_TYPE && button == NavigationButton.EXPLORE) { - tabView.setVisibility(View.GONE); + public static int replaceBrokenFormFactor(int original) { + if (FORM_FACTOR_TYPE == null) { + return original; + } + if (IS_BROKEN_FORM_FACTOR) { + // The form factor most similar to AUTOMOTIVE is LARGE, so it is replaced with LARGE. + return Optional.ofNullable(FormFactor.LARGE.formFactorType).orElse(original); + } else { + return original; } } } \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableResumingShortsOnStartupPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableResumingShortsOnStartupPatch.java new file mode 100644 index 0000000000..5f42b36db6 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableResumingShortsOnStartupPatch.java @@ -0,0 +1,21 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class DisableResumingShortsOnStartupPatch { + + /** + * Injection point. + */ + public static boolean disableResumingShortsOnStartup() { + return Settings.DISABLE_RESUMING_SHORTS_ON_STARTUP.get(); + } + + /** + * Injection point. + */ + public static boolean disableResumingShortsOnStartup(boolean original) { + return original && !Settings.DISABLE_RESUMING_SHORTS_ON_STARTUP.get(); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch.java deleted file mode 100644 index 2e8c3cc06a..0000000000 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch.java +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.extension.youtube.patches; - -import app.revanced.extension.youtube.settings.Settings; - -@SuppressWarnings("unused") -public class DisableResumingStartupShortsPlayerPatch { - - /** - * Injection point. - */ - public static boolean disableResumingStartupShortsPlayer() { - return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get(); - } - - /** - * Injection point. - */ - public static boolean disableResumingStartupShortsPlayer(boolean original) { - return original && !Settings.DISABLE_RESUMING_SHORTS_PLAYER.get(); - } -} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch.java index 3d891a803b..00c5369b60 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/HideRelatedVideoOverlayPatch.java @@ -8,6 +8,6 @@ public final class HideRelatedVideoOverlayPatch { * Injection point. */ public static boolean hideRelatedVideoOverlay() { - return Settings.HIDE_RELATED_VIDEOS_OVERLAY.get(); + return Settings.HIDE_PLAYER_RELATED_VIDEOS_OVERLAY.get(); } } 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..6b43787d24 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/LayoutReloadObserverPatch.java @@ -0,0 +1,51 @@ +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; + } + + PlayerType playerType = PlayerType.getCurrent(); + if (playerType == PlayerType.WATCH_WHILE_MINIMIZED || playerType == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) { + if (isActionBarVisible.compareAndSet(false, true)) { + Utils.runOnMainThreadDelayed(() -> isActionBarVisible.compareAndSet(true, false), 250); + } + } + } +} 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/OverrideOpenInYouTubeMusicButtonPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/OverrideOpenInYouTubeMusicButtonPatch.java new file mode 100644 index 0000000000..8e12b40482 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/OverrideOpenInYouTubeMusicButtonPatch.java @@ -0,0 +1,60 @@ +package app.revanced.extension.youtube.patches; + + +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class OverrideOpenInYouTubeMusicButtonPatch { + + private static final String YOUTUBE_MUSIC_PACKAGE_NAME = "com.google.android.apps.youtube.music"; + + private static final Boolean overrideButton = Settings.OVERRIDE_OPEN_IN_YOUTUBE_MUSIC_BUTTON.get(); + + private static final String overridePackageName = getOverridePackageName(); + + @SuppressWarnings("SameReturnValue") + public static String getOverridePackageName() { + return ""; // Value is replaced during patching. + } + + public static @Nullable Intent overrideSetPackage(@Nullable Intent intent, @Nullable String packageName) { + if (intent == null || !overrideButton) return intent; + + if (YOUTUBE_MUSIC_PACKAGE_NAME.equals(packageName)) { + if (Utils.isNotEmpty(overridePackageName) && Utils.isPackageEnabled(overridePackageName)) { + return intent.setPackage(overridePackageName); + } + + return intent.setPackage(null); + } + + return intent.setPackage(packageName); + } + + public static @Nullable Intent overrideSetData(@Nullable Intent intent, @Nullable Uri uri) { + if (intent == null || uri == null || !overrideButton) return intent; + + String uriString = uri.toString(); + if (uriString.contains(YOUTUBE_MUSIC_PACKAGE_NAME)) { + if ("market".equals(uri.getScheme()) || uriString.contains("play.google.com/store/apps")) { + intent.setData(Uri.parse("https://music.youtube.com/")); + + if (Utils.isNotEmpty(overridePackageName) && Utils.isPackageEnabled(overridePackageName)) { + intent.setPackage(overridePackageName); + } else { + intent.setPackage(null); + } + + return intent; + } + } + + return intent.setData(uri); + } +} 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/ShortsAutoplayPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java index 905ca6dfa0..a6c2cab2c2 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java @@ -1,5 +1,7 @@ package app.revanced.extension.youtube.patches; +import static app.revanced.extension.youtube.patches.VersionCheckPatch.IS_21_10_OR_GREATER; + import android.app.Activity; import java.lang.ref.WeakReference; @@ -24,7 +26,12 @@ public class ShortsAutoplayPatch { /** * Pause playback after 1 play. */ - END_SCREEN; + END_SCREEN, + /** + * Play once, then advanced to the next Short. + * Only found in 21.10+ + */ + AUTO_ADVANCE; static void setYTEnumValue(Enum ytBehavior) { for (ShortsLoopBehavior rvBehavior : values()) { @@ -93,8 +100,12 @@ public class ShortsAutoplayPatch { autoplay = Settings.SHORTS_AUTOPLAY.get(); } + ShortsLoopBehavior autoPlayBehavior = IS_21_10_OR_GREATER + ? ShortsLoopBehavior.AUTO_ADVANCE + : ShortsLoopBehavior.SINGLE_PLAY; + Enum overrideBehavior = (autoplay - ? ShortsLoopBehavior.SINGLE_PLAY + ? autoPlayBehavior : ShortsLoopBehavior.REPEAT).ytEnumValue; if (overrideBehavior != null) { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VersionCheckPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VersionCheckPatch.java index 77d85737b5..9c2986a2f1 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VersionCheckPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VersionCheckPatch.java @@ -27,4 +27,6 @@ public class VersionCheckPatch { public static final boolean IS_20_31_OR_GREATER = isVersionOrGreater("20.31.00"); public static final boolean IS_20_37_OR_GREATER = isVersionOrGreater("20.37.00"); + + public static final boolean IS_21_10_OR_GREATER = isVersionOrGreater("21.10.00"); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java index 22447193c9..496ac92fbf 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java @@ -4,23 +4,22 @@ import app.revanced.extension.youtube.settings.Settings; @SuppressWarnings("unused") public class VideoAdsPatch { - - private static final boolean SHOW_VIDEO_ADS = !Settings.HIDE_VIDEO_ADS.get(); + private static final boolean HIDE_VIDEO_ADS = Settings.HIDE_VIDEO_ADS.get(); /** * Injection point. */ - public static boolean shouldShowAds() { - return SHOW_VIDEO_ADS; + public static boolean hideVideoAds() { + return HIDE_VIDEO_ADS; } /** * Injection point. */ - public static String hideShortsAds(String osName) { - return SHOW_VIDEO_ADS - ? osName - : "Android Automotive"; + public static String hideVideoAds(String osName) { + return HIDE_VIDEO_ADS + ? "Android Automotive" + : osName; } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java index cb262e0165..c30049cb33 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdsFilter.java @@ -12,6 +12,7 @@ import androidx.annotation.Nullable; import java.util.List; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; @@ -167,8 +168,14 @@ public final class AdsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (matchedGroup == buyMovieAd) { return contentIndex == 0 && buyMovieAdBuffer.check(buffer).isFiltered(); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java index a969c9b0e0..24a3b243ea 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/AdvancedVideoQualityMenuFilter.java @@ -1,5 +1,7 @@ package app.revanced.extension.youtube.patches.litho; +import app.revanced.extension.shared.ConversionContext; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch; @@ -21,8 +23,14 @@ public final class AdvancedVideoQualityMenuFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { isVideoQualityMenuVisible = true; return false; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java index 70330c843f..8afb75b5f9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/CommentsFilter.java @@ -1,5 +1,11 @@ package app.revanced.extension.youtube.patches.litho; +import androidx.annotation.NonNull; + +import java.util.List; + +import app.revanced.extension.shared.ConversionContext.ContextInterface; +import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.patches.litho.FilterGroup.*; import app.revanced.extension.youtube.settings.Settings; @@ -8,11 +14,10 @@ import app.revanced.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class CommentsFilter extends Filter { + private static final String CHIP_BAR_PATH_PREFIX = "chip_bar.e"; private static final String COMMENT_COMPOSER_PATH = "comment_composer.e"; private static final String VIDEO_LOCKUP_WITH_ATTACHMENT_PATH = "video_lockup_with_attachment.e"; - private final StringFilterGroup chipBar; - private final ByteArrayFilterGroup aiCommentsSummary; private final StringFilterGroup comments; private final StringFilterGroup emojiAndTimestampButtons; @@ -22,16 +27,6 @@ public final class CommentsFilter extends Filter { "live_chat_summary_banner.e" ); - chipBar = new StringFilterGroup( - Settings.HIDE_COMMENTS_AI_SUMMARY, - "chip_bar.e" - ); - - aiCommentsSummary = new ByteArrayFilterGroup( - null, - "yt_fill_spark_" - ); - var channelGuidelines = new StringFilterGroup( Settings.HIDE_COMMENTS_CHANNEL_GUIDELINES, "channel_guidelines_entry_banner" @@ -79,7 +74,6 @@ public final class CommentsFilter extends Filter { addPathCallbacks( channelGuidelines, chatSummary, - chipBar, commentsByMembers, comments, communityGuidelines, @@ -92,25 +86,44 @@ public final class CommentsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - if (matchedGroup == chipBar) { - // Playlist sort button uses same components and must only filter if the player is opened. - return PlayerType.getCurrent().isMaximizedOrFullscreen() - && aiCommentsSummary.check(buffer).isFiltered(); - } - + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (matchedGroup == comments) { if (path.startsWith(VIDEO_LOCKUP_WITH_ATTACHMENT_PATH)) { return Settings.HIDE_COMMENTS_SECTION_IN_HOME_FEED.get(); } return Settings.HIDE_COMMENTS_SECTION.get(); - } - - if (matchedGroup == emojiAndTimestampButtons) { + } else if (matchedGroup == emojiAndTimestampButtons) { return path.startsWith(COMMENT_COMPOSER_PATH); } return true; } + + /** + * Injection point. + */ + public static void sanitizeCommentsCategoryBar(@NonNull String identifier, + @NonNull List treeNodeResultList) { + try { + if (Settings.SANITIZE_COMMENTS_CATEGORY_BAR.get() + && identifier.startsWith(CHIP_BAR_PATH_PREFIX) + // Playlist sort button uses same components and must only filter if the player is opened. + && PlayerType.getCurrent().isMaximizedOrFullscreen() + ) { + int treeNodeResultListSize = treeNodeResultList.size(); + if (treeNodeResultListSize > 2) { + treeNodeResultList.subList(1, treeNodeResultListSize - 1).clear(); + } + } + } catch (Exception ex) { + Logger.printException(() -> "Failed to sanitize comment category bar", ex); + } + } } 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 9990aa46a4..416ef28f39 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 @@ -1,5 +1,6 @@ package app.revanced.extension.youtube.patches.litho; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.shared.patches.litho.FilterGroup.*; @@ -57,12 +58,13 @@ public final class DescriptionComponentsFilter extends Filter { playlistSectionGroupList.addAll( new ByteArrayFilterGroup( Settings.HIDE_EXPLORE_COURSE_SECTION, - "yt_outline_creator_academy", // For Disable bold icons. + "yt_outline_creator_academy", "yt_outline_experimental_graduation_cap" ), new ByteArrayFilterGroup( Settings.HIDE_EXPLORE_PODCAST_SECTION, - "FEpodcasts_destination" + "FEpodcasts_destination", + "yt_outline_experimental_podcast" ) ); @@ -132,28 +134,17 @@ public final class DescriptionComponentsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - // The description panel can be opened in both the regular player and Shorts. - // 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 - ) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { + // Immediately after the layout is refreshed, litho components are updated before the UI is drawn. + // In this case, EngagementPanel.isDescription() cannot be used, and isActionBarVisible.get() should be used. + if (!EngagementPanel.isDescription() && !(PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get() || ShortsPlayerState.isOpen())) { 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..6ed28b9b0d --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/HorizontalShelvesFilter.java @@ -0,0 +1,100 @@ +package app.revanced.extension.youtube.patches.litho; + +import static app.revanced.extension.youtube.patches.LayoutReloadObserverPatch.isActionBarVisible; + +import app.revanced.extension.shared.ConversionContext.ContextInterface; +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") +public 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(ContextInterface contextInterface) { + if (!Settings.HIDE_HORIZONTAL_SHELVES.get()) { + return false; + } + return contextInterface.isHomeFeedOrRelatedVideo() + || PlayerType.getCurrent().isMaximizedOrFullscreen() + || isActionBarVisible.get() + || NavigationBar.isSearchBarActive() + || NavigationBar.isBackButtonVisible() + || NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY; + } + + @Override + public boolean isFiltered(ContextInterface contextInterface, + 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 (descriptionBuffers.check(buffer).isFiltered()) { + return EngagementPanel.isDescription() || PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get() || ShortsPlayerState.isOpen(); + } + return hideShelves(contextInterface); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java index 4f18eb412d..e9477a1902 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/KeywordContentFilter.java @@ -12,6 +12,8 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import app.revanced.extension.shared.ConversionContext; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.ByteTrieSearch; @@ -47,7 +49,7 @@ public final class KeywordContentFilter extends Filter { /** * Strings found in the buffer for every video. Full strings should be specified. - * + *

* This list does not include every common buffer string, and this can be added/changed as needed. * Words must be entered with the exact casing as found in the buffer. */ @@ -122,7 +124,7 @@ public final class KeywordContentFilter extends Filter { /** * Path components to not filter. Cannot filter the buffer when these are present, * otherwise text in UI controls can be filtered as a keyword (such as using "Playlist" as a keyword). - * + *

* This is also a small performance improvement since * the buffer of the parent component was already searched and passed. */ @@ -156,10 +158,10 @@ public final class KeywordContentFilter extends Filter { * Rolling average of how many videos were filtered by a keyword. * Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER} * but a keyword is still hiding all videos. - * + *

* This check can still fail if some extra UI elements pass the keywords, * such as the video chapter preview or any other elements. - * + *

* To test this, add a filter that appears in all videos (such as 'ovd='), * and open the subscription feed. In practice this does not always identify problems * in the home feed and search, because the home feed has a finite amount of content and @@ -226,7 +228,7 @@ public final class KeywordContentFilter extends Filter { * @return If the string contains any characters from languages that do not use spaces between words. */ private static boolean isLanguageWithNoSpaces(String text) { - for (int i = 0, length = text.length(); i < length;) { + for (int i = 0, length = text.length(); i < length; ) { final int codePoint = text.codePointAt(i); Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint); @@ -277,7 +279,7 @@ public final class KeywordContentFilter extends Filter { /** * @return If the start and end indexes are not surrounded by other letters. - * If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word. + * If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word. */ private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) { final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex); @@ -296,7 +298,7 @@ public final class KeywordContentFilter extends Filter { /** * @return The UTF8 character point immediately before the index, - * or null if the bytes before the index is not a valid UTF8 character. + * or null if the bytes before the index is not a valid UTF8 character. */ @Nullable private static Integer getUtf8CodePointBefore(byte[] data, int index) { @@ -312,7 +314,7 @@ public final class KeywordContentFilter extends Filter { /** * @return The UTF8 character point at the index, - * or null if the index holds no valid UTF8 character. + * or null if the index holds no valid UTF8 character. */ @Nullable private static Integer getUtf8CodePointAt(byte[] data, int index) { @@ -528,7 +530,7 @@ public final class KeywordContentFilter extends Filter { } return switch (selectedNavButton) { - case HOME, EXPLORE -> hideHome; + case HOME -> hideHome; case SUBSCRIPTIONS -> hideSubscriptions; // User is in the Library or notifications. default -> false; @@ -556,8 +558,14 @@ public final class KeywordContentFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (contentIndex != 0 && matchedGroup == startsWithFilter) { return false; } 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..55c1e0fb22 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,13 @@ 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.ConversionContext.ContextInterface; 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 +34,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 +76,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( @@ -254,7 +241,7 @@ public final class LayoutComponentsFilter extends Filter { ); final var relatedVideos = new StringFilterGroup( - Settings.HIDE_RELATED_VIDEOS, + Settings.HIDE_QUICK_ACTIONS_RELATED_VIDEOS, "fullscreen_related_videos" ); @@ -264,12 +251,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 +298,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 +342,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 +358,6 @@ public final class LayoutComponentsFilter extends Filter { emergencyBox, expandableMetadata, forYouShelf, - horizontalShelves, imageShelf, infoPanel, latestPosts, @@ -445,8 +380,14 @@ public final class LayoutComponentsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { // This identifier is used not only in players but also in search results: // https://github.com/ReVanced/revanced-patches/issues/3245 // Until 2024, medical information panels such as Covid-19 also used this identifier and were shown in the search results. @@ -466,13 +407,8 @@ public final class LayoutComponentsFilter extends Filter { return channelProfileGroupList.check(accessibility).isFiltered(); } - if (matchedGroup == communityPosts - && NavigationBar.isBackButtonVisible() - && !NavigationBar.isSearchBarActive() - && PlayerType.getCurrent() != PlayerType.WATCH_WHILE_MAXIMIZED) { - // Allow community posts on channel profile page, - // or if viewing an individual channel in the feed. - return false; + if (matchedGroup == communityPosts) { + return contextInterface.isHomeFeedOrRelatedVideo() || contextInterface.isSubscriptionOrLibrary(); } if (exceptions.matches(path)) return false; // Exceptions are not filtered. @@ -484,47 +420,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 +431,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 +446,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; } @@ -593,6 +482,7 @@ public final class LayoutComponentsFilter extends Filter { * Injection point. */ public static boolean hideFloatingMicrophoneButton(final boolean original) { + // FIXME? Is this feature still relevant? When/where does this microphone appear? return original || Settings.HIDE_FLOATING_MICROPHONE_BUTTON.get(); } @@ -757,31 +647,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 +784,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 +797,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/PlaybackSpeedMenuFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlaybackSpeedMenuFilter.java index e1752f184b..e78fb0235d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlaybackSpeedMenuFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlaybackSpeedMenuFilter.java @@ -1,5 +1,6 @@ package app.revanced.extension.youtube.patches.litho; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.patches.litho.Filter; import app.revanced.extension.shared.patches.litho.FilterGroup.*; import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; @@ -38,8 +39,14 @@ public final class PlaybackSpeedMenuFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (matchedGroup == oldPlaybackMenuGroup) { isOldPlaybackSpeedMenuVisible = true; } else { diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java index c1edd9a0dc..bcccb9da90 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/PlayerFlyoutMenuItemsFilter.java @@ -1,5 +1,6 @@ package app.revanced.extension.youtube.patches.litho; +import app.revanced.extension.shared.ConversionContext.ContextInterface; 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; @@ -112,8 +113,14 @@ public final class PlayerFlyoutMenuItemsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (matchedGroup == videoQualityMenuFooter) { return true; } @@ -127,6 +134,9 @@ public final class PlayerFlyoutMenuItemsFilter extends Filter { return false; } + // 21.x+ fix. + if (path.contains("bottom_sheet_list_option.e")) return false; + return flyoutFilterGroupList.check(buffer).isFiltered(); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java index a9513f28e0..6fabeaa8b4 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/ReturnYouTubeDislikeFilter.java @@ -7,6 +7,8 @@ import androidx.annotation.Nullable; import java.util.LinkedHashSet; import java.util.Map; +import app.revanced.extension.shared.ConversionContext; +import app.revanced.extension.shared.ConversionContext.ContextInterface; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.TrieSearch; import app.revanced.extension.shared.Utils; @@ -19,14 +21,14 @@ import app.revanced.extension.youtube.settings.Settings; /** * Searches for video IDs in the proto buffer of Shorts dislike. - * + *

* Because multiple litho dislike spans are created in the background * (and also anytime litho refreshes the components, which is somewhat arbitrary), * that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()} * unreliable to determine which video ID a Shorts litho span belongs to. - * + *

* But the correct video ID does appear in the protobuffer just before a Shorts litho span is created. - * + *

* Once a way to asynchronously update litho text is found, this strategy will no longer be needed. */ public final class ReturnYouTubeDislikeFilter extends Filter { @@ -88,8 +90,14 @@ public final class ReturnYouTubeDislikeFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) { return false; } 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 462cd8c2e1..94b69c6cae 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,25 @@ package app.revanced.extension.youtube.patches.litho; +import static app.revanced.extension.shared.ConversionContext.*; +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.ConversionContext; 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; @@ -30,20 +29,6 @@ import app.revanced.extension.youtube.shared.PlayerType; public final class ShortsFilter extends Filter { private static final boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get(); private static final String COMPONENT_TYPE = "ComponentType"; - private static final String[] REEL_ACTION_BAR_PATHS = { - "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"; /** @@ -71,11 +56,7 @@ public final class ShortsFilter extends Filter { private final StringFilterGroup shortsCompactFeedVideo; private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer; private final StringFilterGroup channelProfile; - private final ByteArrayFilterGroup channelProfileShelfHeaderBuffer; - private final StringFilterGroup useSoundButton; - private final ByteArrayFilterGroup useSoundButtonBuffer; - private final StringFilterGroup useTemplateButton; - private final ByteArrayFilterGroup useTemplateButtonBuffer; + private final ByteArrayFilterGroup channelProfileShelfHeader; private final StringFilterGroup autoDubbedLabel; private final StringFilterGroup subscribeButton; @@ -90,9 +71,12 @@ public final class ShortsFilter extends Filter { private final StringFilterGroup suggestedAction; private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList(); + private final StringFilterGroup useButtons; + private final ByteArrayFilterGroupList useButtonsBuffer = 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() { // @@ -112,7 +96,7 @@ public final class ShortsFilter extends Filter { "shorts_pivot_item" ); - channelProfileShelfHeaderBuffer = new ByteArrayFilterGroup( + channelProfileShelfHeader = new ByteArrayFilterGroup( Settings.HIDE_SHORTS_CHANNEL, "Shorts" ); @@ -270,32 +254,7 @@ public final class ShortsFilter extends Filter { ) ); - useSoundButton = new StringFilterGroup( - Settings.HIDE_SHORTS_USE_SOUND_BUTTON, - // First filter needed for "Use this sound" that can appear when viewing Shorts - // through the "Short remixing this video" section. - "floating_action_button.e", - // Second filter needed for "Use this sound" that can appear below the video title. - REEL_METAPANEL_PATH - ); - - useSoundButtonBuffer = new ByteArrayFilterGroup( - null, - "yt_outline_camera_" - ); - - useTemplateButton = new StringFilterGroup( - Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON, - // Second filter needed for "Use this template" that can appear below the video title. - REEL_METAPANEL_PATH - ); - - useTemplateButtonBuffer = new ByteArrayFilterGroup( - null, - "yt_outline_template_add_" - ); - - videoActionButton = new StringFilterGroup( + shortsActionButton = new StringFilterGroup( null, // Can be any of: // button.eml @@ -305,6 +264,26 @@ public final class ShortsFilter extends Filter { "button.e" ); + useButtons = new StringFilterGroup( + null, + REEL_PLAYER_OVERLAY_PATH, + REEL_METAPANEL_PATH, + "floating_action_button.e" + ); + + useButtonsBuffer.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_USE_SOUND_BUTTON, + "yt_outline_camera_", + "yt_outline_experimental_camera_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON, + "yt_outline_template_add_", + "yt_outline_experimental_template_add_" + ) + ); + suggestedAction = new StringFilterGroup( null, "suggested_action.e" @@ -313,37 +292,27 @@ public final class ShortsFilter extends Filter { addPathCallbacks( shortsCompactFeedVideo, shelfHeaderPath, joinButton, subscribeButton, paidPromotionLabel, livePreview, suggestedAction, pausedOverlayButtons, channelBar, infoPanel, previewComment, - autoDubbedLabel, fullVideoLinkLabel, videoTitle, useSoundButton, soundButton, stickers, - reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton + autoDubbedLabel, fullVideoLinkLabel, videoTitle, soundButton, stickers, useButtons, + 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. @@ -434,8 +403,14 @@ public final class ShortsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (contentType == FilterContentType.IDENTIFIER) { if (matchedGroup == shelfHeaderIdentifier) { // Shelf header reused in history/channel/etc. @@ -443,8 +418,12 @@ public final class ShortsFilter extends Filter { if (contentIndex != 0) { return false; } - } - if (matchedGroup == channelProfile) { + // Check ConversationContext to not hide shelf header in channel profile + // This value does not exist in the shelf header in the channel profile + if (!contextInterface.isHomeFeedOrRelatedVideo()) { + return false; + } + } else if (matchedGroup == channelProfile) { return true; } @@ -463,16 +442,16 @@ public final class ShortsFilter extends Filter { return reelCarouselBuffer.check(buffer).isFiltered(); } - if (matchedGroup == useSoundButton) { - return useSoundButtonBuffer.check(buffer).isFiltered(); - } - - if (matchedGroup == useTemplateButton) { - return useTemplateButtonBuffer.check(buffer).isFiltered(); - } - if (matchedGroup == shortsCompactFeedVideo) { - return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(buffer).isFiltered(); + return shouldHideShortsFeedItems() + && shortsCompactFeedVideoBuffer.check(buffer).isFiltered() + // The litho path of the feed video is 'video_lockup_with_attachment.e'. + // It appears shortsCompactFeedVideoBuffer is used after 20 seconds during autoplay in the feed in YouTube 20.44.38. + // If the Shorts shelf is hidden on the Home feed, the video in the feed will be hidden after 20 seconds have passed since autoplay began in the feed. + // + // When a video is autoplaying in the feed, no new components are drawn on the screen. + // Therefore, filtering is skipped when the current PlayerType is [INLINE_MINIMAL]. + && PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL; } if (matchedGroup == shelfHeaderPath) { @@ -481,8 +460,10 @@ public final class ShortsFilter extends Filter { if (contentIndex != 0) { return false; } - if (!channelProfileShelfHeaderBuffer.check(buffer).isFiltered()) { - return false; + // Check ConversationContext to not hide shelf header in channel profile + // This value does not exist in the shelf header in the channel profile + if (!contextInterface.isHomeFeedOrRelatedVideo()) { + return channelProfileShelfHeader.check(buffer).isFiltered(); } return shouldHideShortsFeedItems(); @@ -491,8 +472,14 @@ 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 == useButtons) { + return path.contains("|button.e") && useButtonsBuffer.check(buffer).isFiltered(); } if (matchedGroup == suggestedAction) { @@ -534,7 +521,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. @@ -557,65 +544,13 @@ public final class ShortsFilter extends Filter { } return switch (selectedNavButton) { - case HOME, EXPLORE -> hideHome; + case HOME -> hideHome; case SUBSCRIPTIONS -> hideSubscriptions; case LIBRARY -> hideHistory; default -> false; }; } - /** - * 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/patches/litho/VideoActionButtonsFilter.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter.java index 7d1e1cca71..4b02de9a53 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/litho/VideoActionButtonsFilter.java @@ -1,5 +1,7 @@ package app.revanced.extension.youtube.patches.litho; +import app.revanced.extension.shared.ConversionContext; +import app.revanced.extension.shared.ConversionContext.ContextInterface; 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; @@ -158,8 +160,14 @@ public final class VideoActionButtonsFilter extends Filter { } @Override - public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + String path, + byte[] buffer, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { if (matchedGroup == likeSubscribeGlow) { return path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX) || path.startsWith(COMPACTIFY_VIDEO_ACTION_BAR_PATH); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 1cd584d617..8ce3f2a1c0 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -11,8 +11,9 @@ import app.revanced.extension.youtube.patches.VideoInformation; import app.revanced.extension.youtube.patches.VideoInformation.*; import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.shared.ShortsPlayerState; +import j$.util.Optional; -@SuppressWarnings("unused") +@SuppressWarnings({"rawtypes", "unused"}) public class RememberVideoQualityPatch { private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI; @@ -66,6 +67,25 @@ public class RememberVideoQualityPatch { } } + /** + * Injection point. + *

+ * Overrides the initial video quality to not follow the 'Video quality preferences' in YouTube settings. + * (e.g. 'Auto (recommended)' - 360p/480p, 'Higher picture quality' - 720p/1080p...) + * If the maximum video quality available is 1080p and the default video quality is 2160p, + * 1080p is used as an initial video quality. + *

+ * Called before {@link #newVideoStarted(VideoInformation.PlaybackController)}. + */ + public static Optional getInitialVideoQuality(Optional optional) { + int preferredQuality = getDefaultQualityResolution(); + if (preferredQuality != VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) { + Logger.printDebug(() -> "initialVideoQuality: " + preferredQuality); + return Optional.of(preferredQuality); + } + return optional; + } + /** * Injection point. * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java index 767ea7d7c3..c72a967412 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -60,6 +60,11 @@ public class CustomPlaybackSpeedPatch { */ private static final float PROGRESS_BAR_VALUE_SCALE = 100; + /** + * Disable tap and hold speed, true when TAP_AND_HOLD_SPEED is 0. + */ + private static final boolean DISABLE_TAP_AND_HOLD_SPEED; + /** * Tap and hold speed. */ @@ -76,9 +81,9 @@ public class CustomPlaybackSpeedPatch { private static final float customPlaybackSpeedsMin, customPlaybackSpeedsMax; /** - * The last time the old playback menu was forcefully called. + * The last time the playback menu was forcefully called. */ - private static volatile long lastTimeOldPlaybackMenuInvoked; + private static volatile long lastTimePlaybackMenuInvoked; /** * Formats speeds to UI strings. @@ -96,7 +101,12 @@ public class CustomPlaybackSpeedPatch { speedFormatter.setMaximumFractionDigits(2); final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get(); - if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) { + DISABLE_TAP_AND_HOLD_SPEED = holdSpeed == 0; + + if (DISABLE_TAP_AND_HOLD_SPEED) { + // A value for handling exceptions, but this is not used. + TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.defaultValue; + } else if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) { TAP_AND_HOLD_SPEED = holdSpeed; } else { showInvalidCustomSpeedToast(); @@ -108,6 +118,22 @@ public class CustomPlaybackSpeedPatch { customPlaybackSpeedsMax = customPlaybackSpeeds[customPlaybackSpeeds.length - 1]; } + /** + * Injection point. + * Called before {@link #getTapAndHoldSpeed()} + */ + public static boolean disableTapAndHoldSpeed(boolean original) { + return !DISABLE_TAP_AND_HOLD_SPEED && original; + } + + /** + * Injection point. + */ + public static boolean useNewFlyoutMenu(boolean useNewFlyout) { + // If using old speed turn off A/B flyout that breaks old playback speed menu. + return useNewFlyout && !Settings.RESTORE_OLD_SPEED_MENU.get(); + } + /** * Injection point. */ @@ -212,6 +238,15 @@ public class CustomPlaybackSpeedPatch { return false; } + // This method is sometimes used multiple times. + // To prevent this, ignore method reuse within 1 second. + final long now = System.currentTimeMillis(); + if (now - lastTimePlaybackMenuInvoked < 1000) { + Logger.printDebug(() -> "Ignoring call to hideLithoMenuAndShowSpeedMenu"); + return true; + } + lastTimePlaybackMenuInvoked = now; + // Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView. // This only shows in phone layout. var touchInsidedView = parentView4th.getChildAt(0); @@ -235,16 +270,8 @@ public class CustomPlaybackSpeedPatch { } public static void showOldPlaybackSpeedMenu() { - // This method is sometimes used multiple times. - // To prevent this, ignore method reuse within 1 second. - final long now = System.currentTimeMillis(); - if (now - lastTimeOldPlaybackMenuInvoked < 1000) { - Logger.printDebug(() -> "Ignoring call to showOldPlaybackSpeedMenu"); - return; - } - lastTimeOldPlaybackMenuInvoked = now; - // Rest of the implementation added by patch. + Logger.printDebug(() -> "showOldPlaybackSpeedMenu"); } /** 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 d69ed54e2d..18a557d67f 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); @@ -188,8 +187,8 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND = new BooleanSetting("revanced_hide_player_control_buttons_background", FALSE, true); public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS = new BooleanSetting("revanced_hide_player_previous_next_buttons", FALSE, true); public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE); - public static final BooleanSetting HIDE_RELATED_VIDEOS_OVERLAY = new BooleanSetting("revanced_hide_related_videos_overlay", FALSE, true); - public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE); + public static final BooleanSetting HIDE_PLAYER_RELATED_VIDEOS_OVERLAY = new BooleanSetting("revanced_hide_player_related_videos_overlay", FALSE, true); + public static final BooleanSetting HIDE_QUICK_ACTIONS_RELATED_VIDEOS = new BooleanSetting("revanced_hide_quick_actions_related_videos", FALSE); public static final BooleanSetting HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_subscribers_community_guidelines", TRUE); public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE); public static final BooleanSetting HIDE_VIDEO_TITLE = new BooleanSetting("revanced_hide_video_title", FALSE); @@ -228,6 +227,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE); public static final BooleanSetting HIDE_COMMENTS_SECTION_IN_HOME_FEED = new BooleanSetting("revanced_hide_comments_section_in_home_feed", FALSE, parentNot(HIDE_COMMENTS_SECTION)); public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE); + public static final BooleanSetting SANITIZE_COMMENTS_CATEGORY_BAR = new BooleanSetting("revanced_sanitize_comments_category_bar", FALSE); // Description public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE); @@ -296,6 +296,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE, "revanced_remove_viewer_discretion_dialog_user_dialog_message"); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message"); + public static final BooleanSetting OVERRIDE_OPEN_IN_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_open_in_youtube_music_button", TRUE, true); public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.DEFAULT, true); public static final BooleanSetting CHANGE_START_PAGE_ALWAYS = new BooleanSetting("revanced_change_start_page_always", FALSE, true, new ChangeStartPageTypeAvailability()); @@ -323,7 +324,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true); // Shorts - public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", FALSE); + public static final BooleanSetting DISABLE_RESUMING_SHORTS_ON_STARTUP = new BooleanSetting("revanced_disable_resuming_shorts_on_startup", FALSE); public static final BooleanSetting DISABLE_SHORTS_BACKGROUND_PLAYBACK = new BooleanSetting("revanced_shorts_disable_background_playback", FALSE); public static final EnumSetting SHORTS_PLAYER_TYPE = new EnumSetting<>("revanced_shorts_player_type", ShortsPlayerType.SHORTS_PLAYER); public static final BooleanSetting HIDE_SHORTS_AI_BUTTON = new BooleanSetting("revanced_hide_shorts_ai_button", FALSE); @@ -391,7 +392,7 @@ public class Settings extends YouTubeAndMusicSettings { public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true); public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true, "revanced_spoof_device_dimensions_user_dialog_message"); - public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS)); + public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS)); public static final BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("revanced_spoof_video_streams_av1", FALSE, true, "revanced_spoof_video_streams_av1_user_dialog_message", new SpoofClientAv1Availability()); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java index 8f15b79c51..6b06a76de7 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java @@ -310,10 +310,6 @@ public final class NavigationBar { * This tab will never be in a selected state, even if the Create video UI is on screen. */ CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"), - /** - * Only shown to automotive layout. - */ - EXPLORE("TAB_EXPLORE"), SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"), /** * Notifications tab. Only present when 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/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java index e3a4c31ad9..26d8afa715 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -2,8 +2,8 @@ package app.revanced.extension.youtube.sponsorblock; import static app.revanced.extension.shared.StringRef.str; +import android.app.Activity; import android.app.Dialog; -import android.content.Context; import android.util.Pair; import android.util.Patterns; import android.widget.LinearLayout; @@ -34,12 +34,12 @@ public class SponsorBlockSettings { public static final Setting.ImportExportCallback SB_IMPORT_EXPORT_CALLBACK = new Setting.ImportExportCallback() { @Override - public void settingsImported(@Nullable Context context) { + public void settingsImported(@Nullable Activity context) { SegmentCategory.loadAllCategoriesFromSettings(); SponsorBlockPreferenceGroup.settingsImported = true; } @Override - public void settingsExported(@Nullable Context context) { + public void settingsExported(@Nullable Activity context) { showExportWarningIfNeeded(context); } }; @@ -184,16 +184,16 @@ public class SponsorBlockSettings { /** * Export the categories using flatten JSON (no embedded dictionaries or arrays). */ - private static void showExportWarningIfNeeded(@Nullable Context dialogContext) { + private static void showExportWarningIfNeeded(@Nullable Activity activity) { Utils.verifyOnMainThread(); initialize(); // If user has a SponsorBlock user ID then show a warning. - if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateID() + if (activity != null && SponsorBlockSettings.userHasSBPrivateID() && !Settings.SB_HIDE_EXPORT_WARNING.get()) { // Create the custom dialog. Pair dialogPair = CustomDialog.create( - dialogContext, + activity, null, // No title. str("revanced_sb_settings_revanced_export_user_id_warning"), // Message. null, // No EditText. @@ -205,11 +205,7 @@ public class SponsorBlockSettings { true // Dismiss dialog when onNeutralClick. ); - // Set dialog as non-cancelable. - dialogPair.first.setCancelable(false); - - // Show the dialog. - dialogPair.first.show(); + Utils.showDialog(activity, dialogPair.first, false, null); } } diff --git a/extensions/youtube/stub/src/main/java/j$/util/Optional.java b/extensions/youtube/stub/src/main/java/j$/util/Optional.java new file mode 100644 index 0000000000..3f2bb9773e --- /dev/null +++ b/extensions/youtube/stub/src/main/java/j$/util/Optional.java @@ -0,0 +1,18 @@ +package j$.util; + +public final class Optional { + + /** + * Returns an {@code Optional} describing the given non-{@code null} + * value. + * + * @param value the value to describe, which must be non-{@code null} + * @param the type of the value + * @return an {@code Optional} with the value present + * @throws NullPointerException if value is {@code null} + */ + public static Optional of(T value) { + return null; + } + +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index d65f6857e0..abde35e11e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,4 +4,4 @@ org.gradle.parallel = true android.useAndroidX = true android.uniquePackageNames = false kotlin.code.style = official -version = 6.1.0 +version = 6.1.1-dev.1 diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt index daa52011fb..816c0db484 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt @@ -29,6 +29,7 @@ val hideMusicVideoAdsPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt index 96aac10c51..bcf633cc67 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt @@ -21,6 +21,7 @@ val enableExclusiveAudioPlaybackPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt index 2e227b60b1..17cc829921 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt @@ -32,6 +32,7 @@ val permanentRepeatPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt index ff6b5326a5..da72569219 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt @@ -6,8 +6,8 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.music.misc.extension.sharedExtensionPatch import app.revanced.patches.music.misc.gms.Constants.MUSIC_MAIN_ACTIVITY_NAME import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME -import app.revanced.patches.music.misc.gms.musicActivityOnCreateMethod import app.revanced.patches.music.misc.settings.PreferenceScreen +import app.revanced.patches.music.shared.mainActivityOnCreateMethod import app.revanced.patches.shared.layout.branding.EXTENSION_CLASS_DESCRIPTOR import app.revanced.patches.shared.layout.branding.baseCustomBrandingPatch import app.revanced.patches.shared.misc.mapping.ResourceType @@ -61,11 +61,10 @@ val customBrandingPatch = baseCustomBrandingPatch( originalAppPackageName = MUSIC_PACKAGE_NAME, isYouTubeMusic = true, numberOfPresetAppNames = 5, - getMainActivityOnCreate = { musicActivityOnCreateMethod }, + getMainActivityOnCreate = { mainActivityOnCreateMethod }, mainActivityName = MUSIC_MAIN_ACTIVITY_NAME, activityAliasNameWithIntents = MUSIC_MAIN_ACTIVITY_NAME, preferenceScreen = PreferenceScreen.GENERAL, - block = { dependsOn(sharedExtensionPatch, disableSplashAnimationPatch) @@ -75,6 +74,7 @@ val customBrandingPatch = baseCustomBrandingPatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/header/ChangeHeaderPatch.kt new file mode 100644 index 0000000000..5952682118 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/header/ChangeHeaderPatch.kt @@ -0,0 +1,79 @@ +package app.revanced.patches.music.layout.branding.header + +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.wideLiteral +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.misc.settings.PreferenceScreen +import app.revanced.patches.shared.layout.branding.header.changeHeaderPatch +import app.revanced.patches.shared.misc.mapping.ResourceType +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.util.forEachInstructionAsSequence +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private val targetResourceDirectoryNames = mapOf( + "drawable-hdpi" to "121x36 px", + "drawable-xhdpi" to "160x48 px", + "drawable-xxhdpi" to "240x72 px", + "drawable-xxxhdpi" to "320x96 px" +) + +private val variants = arrayOf("dark") + +private val logoResourceNames = arrayOf( + "revanced_header_minimal", + "revanced_header_rounded", +) + +private val headerDrawableNames = arrayOf( + "action_bar_logo_ringo2", + "ytm_logo_ringo2" +) + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/ChangeHeaderPatch;" + +private val changeHeaderBytecodePatch = bytecodePatch { + dependsOn(resourceMappingPatch) + + apply { + headerDrawableNames.forEach { drawableName -> + val drawableId = ResourceType.DRAWABLE[drawableName] + + forEachInstructionAsSequence({ _, method, instruction, index -> + if (instruction.wideLiteral != drawableId) return@forEachInstructionAsSequence null + + val register = method.getInstruction(index).registerA + + return@forEachInstructionAsSequence index to register + }) { method, (index, register) -> + method.addInstructions( + index + 1, + """ + invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->getHeaderDrawableId(I)I + move-result v$register + """, + ) + } + } + } +} + +@Suppress("unused") +val changeHeaderPatch = changeHeaderPatch( + targetResourceDirectoryNames = targetResourceDirectoryNames, + changeHeaderBytecodePatch = changeHeaderBytecodePatch, + logoResourceNames = logoResourceNames, + variants = variants, + preferenceScreen = PreferenceScreen.GENERAL, + compatiblePackages = arrayOf( + "com.google.android.apps.youtube.music" to setOf( + "7.29.52", + "8.10.52", + "8.37.56", + "8.40.54", + "8.44.54" + ), + ), + resourcesAppId = "music", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt index 17a544f89f..3e0bd9b714 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/buttons/HideButtons.kt @@ -52,6 +52,7 @@ val hideButtonsPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt index aa944fee58..adfb03adba 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt @@ -35,6 +35,7 @@ val hideCategoryBarPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt index e44f4cc0f5..67c64dbcf9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/hide/general/HideLayoutComponentsPatch.kt @@ -15,7 +15,8 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( "7.29.52", "8.10.52", "8.37.56", - "8.40.54" + "8.40.54", + "8.44.54" ) ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColor.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/ChangeMiniplayerColor.kt similarity index 96% rename from patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColor.kt rename to patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/ChangeMiniplayerColor.kt index d8f78efe2f..974130d15f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/ChangeMiniplayerColor.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/ChangeMiniplayerColor.kt @@ -1,6 +1,6 @@ @file:Suppress("SpellCheckingInspection") -package app.revanced.patches.music.layout.miniplayercolor +package app.revanced.patches.music.layout.miniplayer import app.revanced.patcher.accessFlags import app.revanced.patcher.extensions.getInstruction @@ -43,11 +43,12 @@ val changeMiniplayerColorPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) apply { - addResources("music", "layout.miniplayercolor.changeMiniplayerColor") + addResources("music", "layout.miniplayer.changeMiniplayerColor") PreferenceScreen.PLAYER.addPreferences( SwitchPreference("revanced_music_change_miniplayer_color"), diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/Fingerprints.kt similarity index 77% rename from patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/Fingerprints.kt rename to patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/Fingerprints.kt index cd621ba5ed..552dde0c3d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayercolor/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/Fingerprints.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.music.layout.miniplayercolor +package app.revanced.patches.music.layout.miniplayer import app.revanced.patcher.* import app.revanced.patcher.patch.BytecodePatchContext @@ -28,3 +28,10 @@ internal val ClassDef.switchToggleColorMethodMatch by ClassDefComposing.composin Opcode.IGET, ) } + +internal val BytecodePatchContext.minimizedPlayerMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("L", "L") + instructions("w_st"()) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/ForciblyEnableMiniplayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/ForciblyEnableMiniplayerPatch.kt new file mode 100644 index 0000000000..4fa3fa0677 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/miniplayer/ForciblyEnableMiniplayerPatch.kt @@ -0,0 +1,68 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.music.layout.miniplayer + +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.music.misc.extension.sharedExtensionPatch +import app.revanced.patches.music.misc.settings.PreferenceScreen +import app.revanced.patches.music.misc.settings.settingsPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/ForciblyEnableMiniplayerPatch;" + +@Suppress("unused") +val forciblyEnableMiniplayerPatch = bytecodePatch( + name = "Forcibly enable miniplayer", + description = "Adds an option to forcibly enable the miniplayer when switching between music videos, podcasts, or songs." +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.apps.youtube.music"( + "7.29.52", + "8.10.52", + "8.37.56", + "8.40.54", + "8.44.54" + ), + ) + + apply { + addResources("music", "layout.miniplayer.forciblyEnableMiniplayer") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_music_forcibly_enable_miniplayer") + ) + + minimizedPlayerMethod.apply { + val invokeIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && methodReference?.name == "booleanValue" + } + + val moveResultIndex = invokeIndex + 1 + val moveResultInstr = getInstruction(moveResultIndex) + val targetRegister = moveResultInstr.registerA + + addInstructions( + moveResultIndex + 1, + """ + invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->forciblyEnableMiniplayerPatch(Z)Z + move-result v$targetRegister + """ + ) + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt index 5ff1997f61..b6bf36caec 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/navigationbar/NavigationBarPatch.kt @@ -57,6 +57,7 @@ val navigationBarPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt index 3ec4d3af44..9b2ad25548 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt @@ -33,6 +33,7 @@ val hideGetMusicPremiumPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/startpage/ChangeStartPagePatch.kt new file mode 100644 index 0000000000..db6eaa806d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/startpage/ChangeStartPagePatch.kt @@ -0,0 +1,114 @@ +package app.revanced.patches.music.layout.startpage + +import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.extensions.fieldReference +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.instructions +import app.revanced.patcher.extensions.string +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.music.misc.extension.sharedExtensionPatch +import app.revanced.patches.music.misc.settings.PreferenceScreen +import app.revanced.patches.music.misc.settings.settingsPatch +import app.revanced.patches.music.shared.mainActivityOnCreateMethod +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionReversed +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/ChangeStartPagePatch;" + +val changeStartPagePatch = bytecodePatch( + name = "Change start page", + description = "Adds an option to set which page the app opens in instead of the homepage.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch + ) + + compatibleWith( + "com.google.android.apps.youtube.music"( + "7.29.52", + "8.10.52", + "8.37.56", + "8.40.54", + "8.44.54" + ) + ) + + apply { + addResources("music", "layout.startpage.changeStartPagePatch") + + PreferenceScreen.GENERAL.addPreferences( + PreferenceCategory( + titleKey = null, + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory", + preferences = setOf( + ListPreference( + key = "revanced_change_start_page", + tag = "app.revanced.extension.shared.settings.preference.SortedListPreference" + ) + ) + ) + ) + + coldStartUpMethodMatch.let { match -> + match.method.apply { + val defaultBrowseIdIndex = match[-1] + + val browseIdIndex = indexOfFirstInstructionReversed(defaultBrowseIdIndex) { + opcode == Opcode.IGET_OBJECT && fieldReference?.type == "Ljava/lang/String;" + } + + if (browseIdIndex != -1) { + val browseIdRegister = + getInstruction(browseIdIndex).registerA + addInstructions( + browseIdIndex + 1, + """ + invoke-static/range { v$browseIdRegister .. v$browseIdRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideBrowseId(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$browseIdRegister + """ + ) + } else { + instructions.mapIndexedNotNull { index, instr -> + if (instr.opcode == Opcode.RETURN_OBJECT) index else null + }.reversed().forEach { returnIndex -> + val returnRegister = + getInstruction(returnIndex).registerA + + addInstructions( + returnIndex, + """ + invoke-static/range { v$returnRegister .. v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideBrowseId(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$returnRegister + """ + ) + } + } + } + } + + mainActivityOnCreateMethod.apply { + val p0 = implementation!!.registerCount - 2 + val p1 = p0 + 1 + + addInstruction( + 0, + "invoke-static/range { v$p0 .. v$p1 }, " + + "$EXTENSION_CLASS_DESCRIPTOR->" + + "overrideIntentActionOnCreate(Landroid/app/Activity;Landroid/os/Bundle;)V" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/startpage/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/startpage/Fingerprints.kt new file mode 100644 index 0000000000..77d8e2c8a9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/startpage/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.music.layout.startpage + +import app.revanced.patcher.composingFirstMethod +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType + +internal val BytecodePatchContext.coldStartUpMethodMatch by composingFirstMethod { + returnType("Ljava/lang/String;") + parameterTypes() + instructions( + "FEmusic_library_sideloaded_tracks"(), + "FEmusic_home"() + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt index 39a78c3d30..4ef55d417d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/theme/ThemePatch.kt @@ -7,7 +7,8 @@ import app.revanced.patches.shared.layout.theme.baseThemeResourcePatch import app.revanced.patches.shared.layout.theme.darkThemeBackgroundColorOption import app.revanced.patches.shared.misc.settings.overrideThemeColors -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/music/patches/theme/ThemePatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/music/patches/theme/ThemePatch;" @Suppress("unused") val themePatch = baseThemePatch( @@ -33,7 +34,8 @@ val themePatch = baseThemePatch( "7.29.52", "8.10.52", "8.37.56", - "8.40.54" + "8.40.54", + "8.44.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/Fingerprints.kt index 6fb1748eac..7f8f0577e8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/Fingerprints.kt @@ -7,14 +7,13 @@ import app.revanced.patcher.invoke import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.returnType +import app.revanced.patcher.strings import com.android.tools.smali.dexlib2.iface.ClassDef -internal val BytecodePatchContext.checkCertificateMethod by gettingFirstMethodDeclaratively( - "X509", -) { +internal val BytecodePatchContext.checkCertificateMethod by gettingFirstMethodDeclaratively { returnType("Z") - parameterTypes("Ljava/lang/String;") - instructions("Failed to get certificate"(String::contains)) + parameterTypes("L") + strings("X509", "isPartnerSHAFingerprint") } internal val BytecodePatchContext.searchMediaItemsConstructorMethod by gettingFirstMethodDeclaratively( diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt index e5bb4eed22..d074d39dd6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/UnlockAndroidAutoMediaBrowserPatch.kt @@ -20,6 +20,7 @@ val unlockAndroidAutoMediaBrowserPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/audio/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/audio/ForceOriginalAudioPatch.kt index 4fae08ecfb..6913e0c959 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/audio/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/audio/ForceOriginalAudioPatch.kt @@ -22,7 +22,8 @@ val forceOriginalAudioPatch = forceOriginalAudioPatch( "7.29.52", "8.10.52", "8.37.56", - "8.40.54" + "8.40.54", + "8.44.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt index cbe36736ec..80034583d5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -21,6 +21,7 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt index 77edc32b42..8007a5e6d8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/EnableDebuggingPatch.kt @@ -21,6 +21,7 @@ val enableDebuggingPatch = enableDebuggingPatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt index f3eb9b08ee..86ae70b8e8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt @@ -15,7 +15,8 @@ val checkWatchHistoryDomainNameResolutionPatch = checkWatchHistoryDomainNameReso "7.29.52", "8.10.52", "8.37.56", - "8.40.54" + "8.40.54", + "8.44.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Fingerprints.kt deleted file mode 100644 index faa6de9ff6..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Fingerprints.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.patches.music.misc.gms - -import app.revanced.patcher.* -import app.revanced.patcher.patch.BytecodePatchContext - -internal val BytecodePatchContext.musicActivityOnCreateMethod by gettingFirstMethodDeclaratively { - name("onCreate") - definingClass("/MusicActivity;") - returnType("V") - parameterTypes("Landroid/os/Bundle;") -} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt index d424f1685e..57f5894cd4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt @@ -10,6 +10,7 @@ import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME import app.revanced.patches.music.misc.settings.PreferenceScreen import app.revanced.patches.music.misc.settings.settingsPatch +import app.revanced.patches.music.shared.mainActivityOnCreateMethod import app.revanced.patches.shared.castContextFetchMethod import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch import app.revanced.patches.shared.misc.settings.preference.IntentPreference @@ -23,7 +24,7 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch( toPackageName = REVANCED_MUSIC_PACKAGE_NAME, getPrimeMethod = { primeMethod }, getEarlyReturnMethods = setOf(BytecodePatchContext::castContextFetchMethod::get), - getMainActivityOnCreateMethodToGetInsertIndex = BytecodePatchContext::musicActivityOnCreateMethod::get to { 0 }, + getMainActivityOnCreateMethodToGetInsertIndex = BytecodePatchContext::mainActivityOnCreateMethod::get to { 0 }, extensionPatch = sharedExtensionPatch, gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, ) { @@ -34,6 +35,7 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch( "8.10.52", "8.37.56", "8.40.54", + "8.44.54" ), ) } 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/music/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatch.kt index 6aa5863fa2..47e21cdb42 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/privacy/SanitizeSharingLinksPatch.kt @@ -18,7 +18,8 @@ val sanitizeSharingLinksPatch = sanitizeSharingLinksPatch( "7.29.52", "8.10.52", "8.37.56", - "8.40.54" + "8.40.54", + "8.44.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt index c4c6764e54..7eea0373e5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/spoof/SpoofVideoStreamsPatch.kt @@ -3,7 +3,7 @@ package app.revanced.patches.music.misc.spoof import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.music.misc.extension.sharedExtensionPatch -import app.revanced.patches.music.misc.gms.musicActivityOnCreateMethod +import app.revanced.patches.music.shared.mainActivityOnCreateMethod import app.revanced.patches.music.misc.settings.PreferenceScreen import app.revanced.patches.music.misc.settings.settingsPatch import app.revanced.patches.music.playservice.* @@ -14,7 +14,7 @@ import app.revanced.patches.shared.misc.spoof.spoofVideoStreamsPatch val spoofVideoStreamsPatch = spoofVideoStreamsPatch( extensionClassDescriptor = "Lapp/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch;", - getMainActivityOnCreateMethod = { musicActivityOnCreateMethod }, + getMainActivityOnCreateMethod = { mainActivityOnCreateMethod }, fixMediaFetchHotConfig = { is_7_16_or_greater }, fixMediaFetchHotConfigAlternative = { is_8_11_or_greater && !is_8_15_or_greater }, fixParsePlaybackResponseFeatureFlag = { is_7_33_or_greater }, @@ -33,7 +33,8 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch( "7.29.52", "8.10.52", "8.37.56", - "8.40.54" + "8.40.54", + "8.44.54" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/header/ChangeHeaderPatch.kt new file mode 100644 index 0000000000..d03d18feb6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/header/ChangeHeaderPatch.kt @@ -0,0 +1,160 @@ +package app.revanced.patches.shared.layout.branding.header + +import app.revanced.patcher.patch.Package +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.util.ResourceGroup +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.util.copyResources +import java.io.File + +internal const val CUSTOM_HEADER_RESOURCE_NAME = "revanced_header_custom" + +@Suppress("unused") +fun changeHeaderPatch( + targetResourceDirectoryNames: Map, + changeHeaderBytecodePatch: Patch, + vararg compatiblePackages: Package, + variants: Array, + logoResourceNames: Array, + preferenceScreen: BasePreferenceScreen.Screen, + resourcesAppId: String, + applyBlock: ResourcePatchContext.() -> Unit = {}, +): Patch { + val customHeaderResourceFileNames = variants.map { variant -> + "${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png" + }.toTypedArray() + + return resourcePatch( + name = "Change header", + description = "Adds an option to change the header logo in the top left corner of the app.", + ) { + dependsOn(addResourcesPatch, changeHeaderBytecodePatch) + + compatibleWith(packages = compatiblePackages) + + val custom by stringOption( + name = "Custom header logo", + description = """ + Folder with images to use as a custom header logo. + + The folder must contain one or more of the following folders, depending on the DPI of the device: + ${targetResourceDirectoryNames.keys.joinToString("\n") { "- $it" }} + + Each of the folders must contain all of the following files: + ${customHeaderResourceFileNames.joinToString("\n")} + + The image dimensions must be as follows: + ${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")} + """.trimIndentMultiline(), + ) + + apply { + addResources(resourcesAppId, "layout.branding.header.changeHeaderPatch") + + preferenceScreen.addPreferences( + if (custom == null) { + ListPreference("revanced_header_logo") + } else { + ListPreference( + key = "revanced_header_logo", + entriesKey = "revanced_header_logo_custom_entries", + entryValuesKey = "revanced_header_logo_custom_entry_values", + ) + }, + ) + + logoResourceNames.forEach { logo -> + variants.forEach { variant -> + copyResources( + "change-header", + ResourceGroup( + "drawable", + logo + "_" + variant + ".xml", + ), + ) + } + } + + // Copy custom template. Images are only used if settings + // are imported and a custom header is enabled. + targetResourceDirectoryNames.keys.forEach { dpi -> + variants.forEach { variant -> + copyResources( + "change-header", + ResourceGroup( + dpi, + resources = customHeaderResourceFileNames, + ), + ) + } + } + + applyBlock() + + // Copy user provided images last, so if an exception is thrown due to bad input. + if (custom != null) { + val customFile = File(custom!!.trim()) + if (!customFile.exists()) { + throw PatchException( + "The custom header path cannot be found: " + + customFile.absolutePath, + ) + } + + if (!customFile.isDirectory) { + throw PatchException( + "The custom header path must be a folder: " + + customFile.absolutePath, + ) + } + + var copiedFiles = false + + // For each source folder, copy the files to the target resource directories. + customFile.listFiles { file -> + file.isDirectory && file.name in targetResourceDirectoryNames + }!!.forEach { dpiSourceFolder -> + val targetDpiFolder = get("res").resolve(dpiSourceFolder.name) + if (!targetDpiFolder.exists()) { + // Should never happen. + throw IllegalStateException("Resource not found: $dpiSourceFolder") + } + + val customFiles = dpiSourceFolder.listFiles { file -> + file.isFile && file.name in customHeaderResourceFileNames + }!! + + if (customFiles.isNotEmpty() && customFiles.size != variants.size) { + throw PatchException( + "Both light/dark mode images " + + "must be specified but only found: " + customFiles.map { it.name }, + ) + } + + customFiles.forEach { imgSourceFile -> + val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name) + imgSourceFile.copyTo(target = imgTargetFile, overwrite = true) + + copiedFiles = true + } + } + + if (!copiedFiles) { + throw PatchException( + "Expected to find directories and files: " + + customHeaderResourceFileNames.contentToString() + + "\nBut none were found in the provided option file path: " + customFile.absolutePath, + ) + } + } + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt index 8c720c76e0..f8b14b13d8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/theme/BaseThemePatch.kt @@ -62,7 +62,10 @@ internal val darkThemeBackgroundColorOption = stringOption( default = "@android:color/black", values = mapOf( "Pure black" to "@android:color/black", - "Material You" to "@android:color/system_neutral1_900", + "Material You (Neutral)" to "@android:color/system_neutral1_900", + "Material You - Primary" to "@android:color/system_accent1_800", + "Material You - Secondary" to "@android:color/system_accent2_800", + "Material You - Tertiary" to "@android:color/system_accent3_800", "Classic (old YouTube)" to "#212121", "Catppuccin (Mocha)" to "#181825", "Dark pink" to "#290025", 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..3999511e10 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 == "jniEncode" }, ) } -/** - * 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..67d9bb79d4 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 { @@ -94,7 +96,9 @@ val hideAdsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -212,6 +216,7 @@ val hideAdsPatch = bytecodePatch( setOf( Endpoint.BROWSE, Endpoint.SEARCH, + Endpoint.NEXT, ).forEach { endpoint -> addOSNameHook( endpoint, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/Fingerprints.kt index 2565c0e79f..b4f7f04ce8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/Fingerprints.kt @@ -1,9 +1,18 @@ package app.revanced.patches.youtube.ad.video import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType internal val BytecodePatchContext.loadVideoAdsMethod by gettingFirstMethodDeclaratively( "TriggerBundle doesn't have the required metadata specified by the trigger ", "Ping migration no associated ping bindings for activated trigger: ", ) + +internal val BytecodePatchContext.playerBytesAdLayoutMethod by gettingFirstMethodDeclaratively( + "Bootstrapped layout construction resulted in non PlayerBytesLayout. PlayerAds count: " +) { + returnType("V") + parameterTypes("L") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt index f6d244f353..2b3501bb23 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt @@ -1,16 +1,20 @@ package app.revanced.patches.youtube.ad.video -import app.revanced.patcher.extensions.ExternalLabel import app.revanced.patcher.extensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch 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.youtube.misc.contexthook.hookClientContextPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/VideoAdsPatch;" + @Suppress("ObjectPropertyName") val videoAdsPatch = bytecodePatch( name = "Video ads", @@ -20,6 +24,7 @@ val videoAdsPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, addResourcesPatch, + hookClientContextPatch ) compatibleWith( @@ -29,7 +34,9 @@ val videoAdsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -40,15 +47,33 @@ val videoAdsPatch = bytecodePatch( SwitchPreference("revanced_hide_video_ads"), ) - loadVideoAdsMethod.addInstructionsWithLabels( - 0, - """ - invoke-static { }, Lapp/revanced/extension/youtube/patches/VideoAdsPatch;->shouldShowAds()Z - move-result v0 - if-nez v0, :show_video_ads - return-void - """, - ExternalLabel("show_video_ads", loadVideoAdsMethod.getInstruction(0)), - ) + setOf( + loadVideoAdsMethod, + playerBytesAdLayoutMethod, + ).forEach { method -> + method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->hideVideoAds()Z + move-result v0 + if-eqz v0, :show_video_ads + return-void + :show_video_ads + nop + """ + ) + } + + + setOf( + Endpoint.GET_WATCH, + Endpoint.PLAYER, + Endpoint.REEL, + ).forEach { endpoint -> + addOSNameHook( + endpoint, + "$EXTENSION_CLASS_DESCRIPTOR->hideVideoAds(Ljava/lang/String;)Ljava/lang/String;", + ) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatch.kt index a93b0a4788..59ab4c5102 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoURLPatch.kt @@ -58,7 +58,9 @@ val copyVideoURLPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt index a1260cdd0c..fa0e381d75 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt @@ -45,7 +45,9 @@ val removeViewerDiscretionDialogPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt index b84afcb4d9..7543d21b87 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/AddMoreDoubleTapToSeekLengthOptionsPatch.kt @@ -22,18 +22,15 @@ val addMoreDoubleTapToSeekLengthOptionsPatch = resourcePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ) ) execute { // Values are hard coded to keep patching simple. - val doubleTapLengthOptionsString = "3, 5, 10, 15, 20, 30, 60, 120, 180, 240" - - val doubleTapLengths = doubleTapLengthOptionsString - .replace(" ", "") - .split(",") - if (doubleTapLengths.isEmpty()) throw PatchException("Invalid double-tap length elements") + val doubleTapLengths = listOf(3, 5, 10, 15, 20, 30, 60, 120, 180, 240) document("res/values/arrays.xml").use { document -> fun Element.removeAllChildren() { @@ -56,10 +53,9 @@ val addMoreDoubleTapToSeekLengthOptionsPatch = resourcePatch( entries.removeAllChildren() doubleTapLengths.forEach { length -> - val item = document.createElement("item") - item.textContent = length - entries.appendChild(item) - values.appendChild(item.cloneNode(true)) + document.createElement("item").apply { textContent = length.toString() } + .also(entries::appendChild) + .cloneNode(true).let(values::appendChild) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt index fce736f556..ee172a3a4c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatch.kt @@ -37,7 +37,9 @@ val disableDoubleTapActionsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt index 337dea4666..7f4d6e3b75 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -80,7 +80,9 @@ val downloadsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt index 9084e593d9..1a9bcbe4f5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt @@ -58,7 +58,9 @@ val disableHapticFeedbackPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt index 934442eeaf..6b7e48b2e9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarPatch.kt @@ -23,7 +23,9 @@ val seekbarPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt index 13c90762b4..94abd157e9 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt @@ -109,7 +109,9 @@ val swipeControlsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt index b5f90ac566..f3f2dde1e3 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt @@ -31,7 +31,9 @@ val disableAutoCaptionsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt index 36c6aa2653..8c8ce3bca5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt @@ -31,7 +31,9 @@ val customBrandingPatch = baseCustomBrandingPatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt index 4555a64b3c..ed60e1cbeb 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt @@ -3,27 +3,24 @@ package app.revanced.patches.youtube.layout.branding.header import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.wideLiteral -import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patcher.patch.resourcePatch -import app.revanced.patcher.patch.stringOption import app.revanced.patcher.util.Document -import app.revanced.patches.all.misc.resources.addResources -import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.layout.branding.addBrandLicensePatch +import app.revanced.patches.shared.layout.branding.header.CUSTOM_HEADER_RESOURCE_NAME +import app.revanced.patches.shared.layout.branding.header.changeHeaderPatch import app.revanced.patches.shared.misc.mapping.ResourceType import app.revanced.patches.shared.misc.mapping.resourceMappingPatch -import app.revanced.patches.shared.misc.settings.preference.ListPreference import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.util.ResourceGroup -import app.revanced.util.Utils.trimIndentMultiline -import app.revanced.util.copyResources import app.revanced.util.findElementByAttributeValueOrThrow import app.revanced.util.forEachInstructionAsSequence import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import java.io.File -private val variants = arrayOf("light", "dark") +internal val variants = arrayOf("light", "dark") + +private val logoResourceNames = arrayOf( + "revanced_header_minimal", + "revanced_header_rounded", +) private val targetResourceDirectoryNames = mapOf( "drawable-hdpi" to "194x72 px", @@ -32,25 +29,6 @@ private val targetResourceDirectoryNames = mapOf( "drawable-xxxhdpi" to "512x192 px", ) -/** - * Header logos built into this patch. - */ -private val logoResourceNames = arrayOf( - "revanced_header_minimal", - "revanced_header_rounded", -) - -/** - * Custom header resource/file name. - */ -private const val CUSTOM_HEADER_RESOURCE_NAME = "revanced_header_custom" - -/** - * Custom header resource/file names. - */ -private val customHeaderResourceFileNames = variants.map { variant -> - "${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png" -}.toTypedArray() private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeHeaderPatch;" @@ -97,182 +75,63 @@ private val changeHeaderBytecodePatch = bytecodePatch { } } -@Suppress("unused") -val changeHeaderPatch = resourcePatch( - name = "Change header", - description = "Adds an option to change the header logo in the top left corner of the app.", -) { - dependsOn(addResourcesPatch, changeHeaderBytecodePatch) - compatibleWith( - "com.google.android.youtube"( +val changeHeaderPatch = changeHeaderPatch( + targetResourceDirectoryNames = targetResourceDirectoryNames, + changeHeaderBytecodePatch = changeHeaderBytecodePatch, + compatiblePackages = arrayOf( + "com.google.android.youtube" to setOf( "20.14.43", "20.21.37", "20.26.46", "20.31.42", "20.37.48", - "20.40.45" - ), - ) - - val custom by stringOption( - name = "Custom header logo", - description = """ - Folder with images to use as a custom header logo. - - The folder must contain one or more of the following folders, depending on the DPI of the device: - ${targetResourceDirectoryNames.keys.joinToString("\n") { "- $it" }} - - Each of the folders must contain all of the following files: - ${customHeaderResourceFileNames.joinToString("\n")} - - The image dimensions must be as follows: - ${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")} - """.trimIndentMultiline(), - ) - - apply { - addResources("youtube", "layout.branding.changeHeaderPatch") - - PreferenceScreen.GENERAL.addPreferences( - if (custom == null) { - ListPreference("revanced_header_logo") - } else { - ListPreference( - key = "revanced_header_logo", - entriesKey = "revanced_header_logo_custom_entries", - entryValuesKey = "revanced_header_logo_custom_entry_values", - ) - }, + "20.40.45", + "20.44.38", + "20.45.36" ) + ), + variants = variants, + logoResourceNames = logoResourceNames, + preferenceScreen = PreferenceScreen.GENERAL, + resourcesAppId = "youtube", +) { + // Logo is replaced using an attribute reference. + document("res/values/attrs.xml").use { document -> + val resources = document.childNodes.item(0) - logoResourceNames.forEach { logo -> - variants.forEach { variant -> - copyResources( - "change-header", - ResourceGroup( - "drawable", - logo + "_" + variant + ".xml", - ), - ) - } + fun addAttributeReference(logoName: String) { + val item = document.createElement("attr") + item.setAttribute("format", "reference") + item.setAttribute("name", logoName) + resources.appendChild(item) } - // Copy custom template. Images are only used if settings - // are imported and a custom header is enabled. - targetResourceDirectoryNames.keys.forEach { dpi -> - variants.forEach { variant -> - copyResources( - "change-header", - ResourceGroup( - dpi, - *customHeaderResourceFileNames, - ), - ) - } - } + logoResourceNames.forEach { logoName -> addAttributeReference(logoName) } - // Logo is replaced using an attribute reference. - document("res/values/attrs.xml").use { document -> - val resources = document.childNodes.item(0) + addAttributeReference(CUSTOM_HEADER_RESOURCE_NAME) + } - fun addAttributeReference(logoName: String) { - val item = document.createElement("attr") - item.setAttribute("format", "reference") + // Add custom drawables to all styles that use the regular and premium logo. + document("res/values/styles.xml").use { document -> + arrayOf( + "Base.Theme.YouTube.Light" to "light", + "Base.Theme.YouTube.Dark" to "dark", + "CairoLightThemeRingo2Updates" to "light", + "CairoDarkThemeRingo2Updates" to "dark", + ).forEach { (style, mode) -> + val styleElement = document.childNodes.findElementByAttributeValueOrThrow("name", style) + + fun addDrawableElement(document: Document, logoName: String, mode: String) { + val item = document.createElement("item") item.setAttribute("name", logoName) - resources.appendChild(item) + item.textContent = "@drawable/${logoName}_$mode" + styleElement.appendChild(item) } - logoResourceNames.forEach { logoName -> - addAttributeReference(logoName) - } + logoResourceNames.forEach { logoName -> addDrawableElement(document, logoName, mode) } - addAttributeReference(CUSTOM_HEADER_RESOURCE_NAME) - } - - // Add custom drawables to all styles that use the regular and premium logo. - document("res/values/styles.xml").use { document -> - arrayOf( - "Base.Theme.YouTube.Light" to "light", - "Base.Theme.YouTube.Dark" to "dark", - "CairoLightThemeRingo2Updates" to "light", - "CairoDarkThemeRingo2Updates" to "dark", - ).forEach { (style, mode) -> - val styleElement = document.childNodes.findElementByAttributeValueOrThrow( - "name", - style, - ) - - fun addDrawableElement(document: Document, logoName: String, mode: String) { - val item = document.createElement("item") - item.setAttribute("name", logoName) - item.textContent = "@drawable/${logoName}_$mode" - styleElement.appendChild(item) - } - - logoResourceNames.forEach { logoName -> - addDrawableElement(document, logoName, mode) - } - - addDrawableElement(document, CUSTOM_HEADER_RESOURCE_NAME, mode) - } - } - - // Copy user provided images last, so if an exception is thrown due to bad input. - if (custom != null) { - val customFile = File(custom!!.trim()) - if (!customFile.exists()) { - throw PatchException( - "The custom header path cannot be found: " + - customFile.absolutePath, - ) - } - - if (!customFile.isDirectory) { - throw PatchException( - "The custom header path must be a folder: " + - customFile.absolutePath, - ) - } - - var copiedFiles = false - - // For each source folder, copy the files to the target resource directories. - customFile.listFiles { file -> - file.isDirectory && file.name in targetResourceDirectoryNames - }!!.forEach { dpiSourceFolder -> - val targetDpiFolder = get("res").resolve(dpiSourceFolder.name) - if (!targetDpiFolder.exists()) { - // Should never happen. - throw IllegalStateException("Resource not found: $dpiSourceFolder") - } - - val customFiles = dpiSourceFolder.listFiles { file -> - file.isFile && file.name in customHeaderResourceFileNames - }!! - - if (customFiles.isNotEmpty() && customFiles.size != variants.size) { - throw PatchException( - "Both light/dark mode images " + - "must be specified but only found: " + customFiles.map { it.name }, - ) - } - - customFiles.forEach { imgSourceFile -> - val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name) - imgSourceFile.copyTo(target = imgTargetFile, overwrite = true) - - copiedFiles = true - } - } - - if (!copiedFiles) { - throw PatchException( - "Expected to find directories and files: " + - customHeaderResourceFileNames.contentToString() + - "\nBut none were found in the provided option file path: " + customFile.absolutePath, - ) - } + addDrawableElement(document, CUSTOM_HEADER_RESOURCE_NAME, mode) } } -} +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatch.kt index a869906f37..44ed0292a8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideVideoActionsButtonsPatch.kt @@ -33,7 +33,9 @@ val hideVideoActionButtonsPatch = resourcePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/music/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/music/Fingerprints.kt new file mode 100644 index 0000000000..3e6241754a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/music/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.youtube.layout.buttons.music + +import app.revanced.patcher.accessFlags +import app.revanced.patcher.definingClass +import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.name +import app.revanced.patcher.parameterTypes +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.returnType +import com.android.tools.smali.dexlib2.AccessFlags + +internal val BytecodePatchContext.getOverridePackageNameMethod by gettingFirstMethodDeclaratively { + name("getOverridePackageName") + definingClass(EXTENSION_CLASS_DESCRIPTOR) + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returnType("Ljava/lang/String;") + parameterTypes() +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/music/OverrideOpenInYouTubeMusicButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/music/OverrideOpenInYouTubeMusicButtonPatch.kt new file mode 100644 index 0000000000..da3be27a51 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/music/OverrideOpenInYouTubeMusicButtonPatch.kt @@ -0,0 +1,126 @@ +package app.revanced.patches.youtube.layout.buttons.music + +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.extensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.forEachInstructionAsSequence +import app.revanced.util.returnEarly +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction +import org.w3c.dom.Element + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/OverrideOpenInYouTubeMusicButtonPatch;" + +val packageNameOption = stringOption( + name = "YouTube Music package name", + description = "The package name of the YouTube Music app to open when clicking the 'Open in YouTube Music' button.", + default = "app.revanced.android.apps.youtube.music", + values = mapOf( + "Original package name" to "com.google.android.apps.youtube.music", + "ReVanced default package name" to "app.revanced.android.apps.youtube.music" + ), + required = true, +) + +private val overrideOpenInYouTubeMusicManifestResourcePatch = resourcePatch { + apply { + val packageName by packageNameOption + + document("AndroidManifest.xml").use { document -> + val queriesList = document.getElementsByTagName("queries") + + val queries = if (queriesList.length > 0) queriesList.item(0) as Element + else document.createElement("queries").also(document::appendChild) + + document.createElement("package").apply { + setAttribute("android:name", packageName) + }.let(queries::appendChild) + } + } +} + +@Suppress("unused") +val overrideOpenInYouTubeMusicButtonPatch = bytecodePatch( + name = "Override 'Open in YouTube Music' button", + description = "Overrides the button to open YouTube Music under a different package name. " + + "By default, it overrides to the ReVanced default package name of YouTube Music.", +) { + dependsOn( + settingsPatch, + addResourcesPatch, + overrideOpenInYouTubeMusicManifestResourcePatch + ) + + compatibleWith( + "com.google.android.youtube"( + "20.14.43", + "20.21.37", + "20.26.46", + "20.31.42", + "20.37.48", + "20.40.45", + "20.44.38", + "20.45.36" + ), + ) + + val packageName by packageNameOption() + + apply { + addResources("youtube", "layout.buttons.music.overrideOpenInYouTubeMusicButtonPatch") + + PreferenceScreen.GENERAL.addPreferences( + PreferenceCategory( + titleKey = null, + tag = "app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory", + preferences = setOf(SwitchPreference(key = "revanced_override_open_in_youtube_music_button")) + ) + ) + + getOverridePackageNameMethod.returnEarly(packageName!!) + + forEachInstructionAsSequence({ _, _, instruction, index -> + if (instruction.opcode != Opcode.INVOKE_VIRTUAL) return@forEachInstructionAsSequence null + val reference = instruction.methodReference ?: return@forEachInstructionAsSequence null + if (reference.definingClass != "Landroid/content/Intent;") return@forEachInstructionAsSequence null + + when (reference.name) { + "setPackage" if reference.parameterTypes == listOf("Ljava/lang/String;") -> + index to "overrideSetPackage(Landroid/content/Intent;Ljava/lang/String;)Landroid/content/Intent;" + + "setData" if reference.parameterTypes == listOf("Landroid/net/Uri;") -> + index to "overrideSetData(Landroid/content/Intent;Landroid/net/Uri;)Landroid/content/Intent;" + + else -> null + } + }) { method, (index, methodDescriptor) -> + if (method.definingClass == EXTENSION_CLASS_DESCRIPTOR) return@forEachInstructionAsSequence + + val invokeString = when (val instruction = method.getInstruction(index)) { + is RegisterRangeInstruction -> + "invoke-static/range { v${instruction.startRegister} .. v${instruction.startRegister + instruction.registerCount - 1} }" + + is FiveRegisterInstruction -> + "invoke-static { v${instruction.registerC}, v${instruction.registerD} }" + + else -> return@forEachInstructionAsSequence + } + + method.replaceInstruction( + index, + "$invokeString, $EXTENSION_CLASS_DESCRIPTOR->$methodDescriptor" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index 64df88bb37..3875c9f45f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -60,7 +60,9 @@ val navigationBarPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt index 2af8a1177a..c9b2c3f62f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt @@ -43,7 +43,9 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -146,7 +148,8 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( titleAnchorMethodMatch.let { it.method.apply { val titleAnchorIndex = it[-1] - val titleAnchorRegister = getInstruction(titleAnchorIndex).registerA + val titleAnchorRegister = + getInstruction(titleAnchorIndex).registerA addInstruction( titleAnchorIndex + 1, @@ -154,7 +157,8 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( ) val playerCollapseButtonIndex = it[1] - val playerCollapseButtonRegister = getInstruction(playerCollapseButtonIndex).registerA + val playerCollapseButtonRegister = + getInstruction(playerCollapseButtonIndex).registerA addInstruction( playerCollapseButtonIndex + 1, @@ -173,7 +177,8 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( // so match on move-result-object after findViewById instead of check-cast. val moveResultIndex = it[2] val insertIndex = moveResultIndex + 1 - val insertRegister = getInstruction(moveResultIndex).registerA + val insertRegister = + getInstruction(moveResultIndex).registerA addInstructionsWithLabels( insertIndex, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt index c1ca314270..18d63174e8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt @@ -7,6 +7,9 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.youtube.misc.contexthook.Endpoint +import app.revanced.patches.youtube.misc.contexthook.addClientFormFactorHook +import app.revanced.patches.youtube.misc.contexthook.hookClientContextPatch import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch @@ -27,6 +30,7 @@ val changeFormFactorPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, addResourcesPatch, + hookClientContextPatch, navigationBarHookPatch ) @@ -37,7 +41,9 @@ val changeFormFactorPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -48,8 +54,6 @@ val changeFormFactorPatch = bytecodePatch( ListPreference("revanced_change_form_factor"), ) - hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR) - val formFactorEnumConstructorClass = formFactorEnumConstructorMethod.definingClass val createPlayerRequestBodyWithModelMatch = firstMethodComposite { @@ -70,11 +74,23 @@ val changeFormFactorPatch = bytecodePatch( addInstructions( index + 1, """ - invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getFormFactor(I)I + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getUniversalFormFactor(I)I move-result v$register """, ) } } + + setOf( + Endpoint.GET_WATCH, + Endpoint.NEXT, + Endpoint.GUIDE, + Endpoint.REEL, + ).forEach { endpoint -> + addClientFormFactorHook( + endpoint, + "$EXTENSION_CLASS_DESCRIPTOR->replaceBrokenFormFactor(I)I", + ) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt index 4417a906cc..be78bf46f8 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt @@ -39,7 +39,9 @@ val hideAutoplayPreviewPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ) ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt index e6f36cd84f..a82b5ca69d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndScreenCardsPatch.kt @@ -68,7 +68,9 @@ val hideEndScreenCardsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/Fingerprints.kt index 432cae1cf5..f57bc82e3d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/Fingerprints.kt @@ -1,14 +1,25 @@ package app.revanced.patches.youtube.layout.hide.endscreensuggestedvideo import app.revanced.patcher.* -import app.revanced.patcher.extensions.instructions +import app.revanced.patcher.accessFlags +import app.revanced.patcher.definingClass +import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.firstMethodComposite +import app.revanced.patcher.instructions +import app.revanced.patcher.name +import app.revanced.patcher.parameterTypes 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.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -internal val BytecodePatchContext.autoNavConstructorMethod by gettingFirstImmutableMethodDeclaratively("main_app_autonav") { +internal val BytecodePatchContext.autoNavConstructorMethod by gettingFirstImmutableMethodDeclaratively( + "main_app_autonav" +) { returnType("V") accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) } @@ -24,15 +35,50 @@ internal val BytecodePatchContext.removeOnLayoutChangeListenerMethodMatch by com accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returnType("V") parameterTypes() - opcodes( - Opcode.IPUT, - Opcode.INVOKE_VIRTUAL, + instructions( + allOf(Opcode.IPUT(), field { type == "I" }), + afterAtMost( + 3, + allOf( + Opcode.INVOKE_VIRTUAL(), + method { returnType == "V" && parameterTypes.isEmpty() } + ) + ), + allOf(Opcode.INVOKE_VIRTUAL(), method { + name == "removeOnLayoutChangeListener" && + returnType == "V" && + definingClass == "Lcom/google/android/apps/youtube/app/common/" + + "player/overlay/YouTubePlayerOverlaysLayout;" + }), ) - // This is the only reference present in the entire smali. - custom { - instructions.anyInstruction { - val reference = methodReference - reference?.name == "removeOnLayoutChangeListener" && reference.definingClass.endsWith("/YouTubePlayerOverlaysLayout;") - } +} + +internal fun BytecodePatchContext.getEndScreenSuggestedVideoMethodMatch(autoNavStatusMethod: Method): CompositeMatch { + val endScreenMethod = removeOnLayoutChangeListenerMethodMatch.let { + firstMethod(it.method.getInstruction(it[1]).methodReference!!) + } + + return firstMethodComposite { + name(endScreenMethod.name) + definingClass(endScreenMethod.definingClass) + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes() + instructions( + allOf( + Opcode.IGET_OBJECT(), + field { + definingClass == endScreenMethod.definingClass && + type == autoNavStatusMethod.definingClass + } + ), + afterAtMost( + 3, + allOf( + Opcode.INVOKE_VIRTUAL(), + method { this == autoNavStatusMethod } + ) + ), + ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt index ae14785935..f84395c95e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt @@ -2,8 +2,10 @@ package app.revanced.patches.youtube.layout.hide.endscreensuggestedvideo import app.revanced.patcher.extensions.ExternalLabel import app.revanced.patcher.extensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.fieldReference import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.firstMethod import app.revanced.patcher.immutableClassDef import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources @@ -11,9 +13,6 @@ import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen -import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.indexOfFirstInstructionReversedOrThrow -import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction private const val EXTENSION_CLASS_DESCRIPTOR = @@ -36,37 +35,34 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) apply { - addResources("youtube", "layout.hide.endscreensuggestedvideo.hideEndScreenSuggestedVideoPatch") + addResources( + "youtube", + "layout.hide.endscreensuggestedvideo.hideEndScreenSuggestedVideoPatch" + ) PreferenceScreen.PLAYER.addPreferences( SwitchPreference("revanced_end_screen_suggested_video"), ) - removeOnLayoutChangeListenerMethodMatch.let { - val endScreenMethod = navigate(it.immutableMethod).to(it[-1]).stop() - endScreenMethod.apply { - val autoNavStatusMethodName = - autoNavConstructorMethod.immutableClassDef.getAutoNavStatusMethod().name + val autoNavStatusMethod = + autoNavConstructorMethod.immutableClassDef.getAutoNavStatusMethod() - val invokeIndex = indexOfFirstInstructionOrThrow { - val reference = methodReference - reference?.name == autoNavStatusMethodName && - reference.returnType == "Z" && - reference.parameterTypes.isEmpty() - } + val endScreenMethod = removeOnLayoutChangeListenerMethodMatch.let { + firstMethod(it.method.getInstruction(it[1]).methodReference!!) + } - val iGetObjectIndex = - indexOfFirstInstructionReversedOrThrow(invokeIndex, Opcode.IGET_OBJECT) - val invokeReference = getInstruction(invokeIndex).reference - val iGetObjectReference = - getInstruction(iGetObjectIndex).reference - val opcodeName = getInstruction(invokeIndex).opcode.name + + getEndScreenSuggestedVideoMethodMatch(autoNavStatusMethod).let { match -> + match.method.apply { + val autoNavField = getInstruction(match[0]).fieldReference!! addInstructionsWithLabels( 0, @@ -75,10 +71,10 @@ val hideEndScreenSuggestedVideoPatch = bytecodePatch( move-result v0 if-eqz v0, :show_end_screen_recommendation - iget-object v0, p0, $iGetObjectReference + iget-object v0, p0, $autoNavField # This reference checks whether autoplay is turned on. - $opcodeName { v0 }, $invokeReference + invoke-virtual { v0 }, $autoNavStatusMethod move-result v0 # Hide suggested video end screen only when autoplay is turned off. diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt index 00cf27fe76..1d889d2846 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt @@ -35,7 +35,9 @@ val disableFullscreenAmbientModePatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) 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..a230ee663c 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,13 +12,18 @@ 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.litho.lazily.hookTreeNodeResult +import app.revanced.patches.youtube.misc.litho.lazily.lazilyConvertedElementHookPatch import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch import app.revanced.patches.youtube.misc.playservice.is_20_21_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_11_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 +88,8 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( versionCheckPatch, engagementPanelHookPatch, resourceMappingPatch, + hideHorizontalShelvesPatch, + lazilyConvertedElementHookPatch ), filterClasses = setOf( LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR, @@ -98,7 +105,9 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ), ) { @@ -140,7 +149,6 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( "revanced_comments_screen", preferences = setOf( SwitchPreference("revanced_hide_comments_ai_chat_summary"), - SwitchPreference("revanced_hide_comments_ai_summary"), SwitchPreference("revanced_hide_comments_channel_guidelines"), SwitchPreference("revanced_hide_comments_by_members_header"), SwitchPreference("revanced_hide_comments_section"), @@ -150,6 +158,7 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( SwitchPreference("revanced_hide_comments_emoji_and_timestamp_buttons"), SwitchPreference("revanced_hide_comments_preview_comment"), SwitchPreference("revanced_hide_comments_thanks_button"), + SwitchPreference("revanced_sanitize_comments_category_bar"), ), sorting = Sorting.UNSORTED, ), @@ -162,7 +171,7 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( SwitchPreference("revanced_hide_live_chat_replay_button"), SwitchPreference("revanced_hide_medical_panels"), SwitchPreference("revanced_hide_quick_actions"), - SwitchPreference("revanced_hide_related_videos"), + SwitchPreference("revanced_hide_quick_actions_related_videos"), SwitchPreference("revanced_hide_subscribers_community_guidelines"), SwitchPreference("revanced_hide_timed_reactions"), SwitchPreference("revanced_hide_video_title") @@ -215,7 +224,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"), @@ -241,7 +249,6 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( ), ) ), - SwitchPreference("revanced_hide_floating_microphone_button"), SwitchPreference( key = "revanced_hide_horizontal_shelves", tag = "app.revanced.extension.shared.settings.preference.BulletPointSwitchPreference", @@ -269,31 +276,32 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( ) } + if (!is_21_11_or_greater) { + PreferenceScreen.FEED.addPreferences( + SwitchPreference("revanced_hide_floating_microphone_button") + ) + } + + hookTreeNodeResult("$COMMENTS_FILTER_CLASS_NAME->sanitizeCommentsCategoryBar") + // region Hide mix playlists 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 @@ -448,18 +456,22 @@ val hideLayoutComponentsPatch = hideLayoutComponentsPatch( // region Hide Floating microphone - showFloatingMicrophoneButtonMethodMatch.let { - it.method.apply { - val index = it[-1] - val register = getInstruction(index).registerA + if (!is_21_11_or_greater) { + // Code has moved in 21.11+, but it's not clear when/ where this + // floating microphone can show or if this patch is still relevant. + showFloatingMicrophoneButtonMethodMatch.let { + it.method.apply { + val index = it[-1] + val register = getInstruction(index).registerA - addInstructions( - index + 1, - """ + addInstructions( + index + 1, + """ invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideFloatingMicrophoneButton(Z)Z move-result v$register """, - ) + ) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt index ad8a320178..2fe0360b29 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt @@ -52,7 +52,9 @@ val hideInfoCardsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatch.kt index abbe5f023d..a4aa3d97b5 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenu/HidePlayerFlyoutMenuPatch.kt @@ -30,7 +30,9 @@ val hidePlayerFlyoutMenuItemsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatch.kt index 71d0888516..4ad5cfe88a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/popup/PlayerPopupPanelsPatch.kt @@ -31,7 +31,9 @@ val disablePlayerPopupPanelsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt index 3ebbb974f4..b4c45e4440 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatch.kt @@ -35,7 +35,9 @@ val hideRelatedVideoOverlayPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -43,7 +45,7 @@ val hideRelatedVideoOverlayPatch = bytecodePatch( addResources("youtube", "layout.hide.relatedvideooverlay.hideRelatedVideoOverlayPatch") PreferenceScreen.PLAYER.addPreferences( - SwitchPreference("revanced_hide_related_videos_overlay"), + SwitchPreference("revanced_hide_player_related_videos_overlay"), ) val relatedEndScreenResultsMethod = diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt index 7700ff0a57..7cff92a08f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt @@ -35,7 +35,9 @@ val disableRollingNumberAnimationsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) 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..4cea676218 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, @@ -166,7 +166,9 @@ val hideShortsComponentsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -188,7 +190,8 @@ val hideShortsComponentsPatch = bytecodePatch( methodReference?.name == "getDimensionPixelSize" } + 1 - val sizeRegister = method.getInstruction(targetIndex).registerA + val sizeRegister = + method.getInstruction(targetIndex).registerA return@forEachInstructionAsSequence targetIndex to sizeRegister }) { method, (targetIndex, sizeRegister) -> @@ -204,42 +207,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/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt index 6195af9f4e..acdcd59efd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt @@ -32,7 +32,9 @@ val disableSignInToTVPopupPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt index b9ae184e0b..c4936e8838 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt @@ -30,7 +30,9 @@ val hideTimestampPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index 0ef561d714..efef43141a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -82,7 +82,9 @@ val miniplayerPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt index 236a6c05fe..c88afa172e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/ExitFullscreenPatch.kt @@ -27,7 +27,9 @@ val exitFullscreenPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -61,7 +63,7 @@ val exitFullscreenPatch = bytecodePatch( insertIndex, "invoke-static/range { p1 .. p1 }, " + "$EXTENSION_CLASS_DESCRIPTOR->endOfVideoReached(Ljava/lang/Enum;)V", - ) + ) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt index 79f044a6e7..d5b65c66df 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/fullscreen/OpenVideosFullscreenPatch.kt @@ -27,7 +27,9 @@ val openVideosFullscreenPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt index d4dd999ef5..06196619a4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt @@ -33,7 +33,9 @@ val customPlayerOverlayOpacityPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) 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..db342f440b 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, @@ -65,7 +67,9 @@ val returnYouTubeDislikePatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -126,17 +130,11 @@ 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 + it.type == conversionContextClassDef.type } ?: throw PatchException("Could not find conversion context field") - val conversionContextPathBuilderField = conversionContextToStringMethod.immutableClassDef - .fields.single { field -> field.type == "Ljava/lang/StringBuilder;" } - // Old pre 20.40 and lower hook. // 21.05 clobbers p0 (this) register. // Add additional registers so all parameters including p0 are free to use anywhere in the method. @@ -184,7 +182,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 +209,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 +258,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/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 63fa21a216..55cf4af7ed 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -15,6 +15,8 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_09_or_greater +import app.revanced.patcher.method +import app.revanced.patches.youtube.misc.playservice.is_21_10_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 @@ -35,7 +37,7 @@ import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ShortsAutoplayPatch;" -@Suppress("ObjectPropertyName") +@Suppress("unused") val shortsAutoplayPatch = bytecodePatch( name = "Shorts autoplay", description = "Adds options to automatically play the next Short.", @@ -54,7 +56,9 @@ val shortsAutoplayPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -98,12 +102,23 @@ val shortsAutoplayPatch = bytecodePatch( reelPlaybackRepeatMethod.apply { // The behavior enums are looked up from an ordinal value to an enum type. - findInstructionIndicesReversedOrThrow { - val reference = getReference() - reference?.definingClass == reelEnumClass && - reference.parameterTypes.firstOrNull() == "I" && - reference.returnType == reelEnumClass - }.forEach { index -> + + val match = if (is_21_10_or_greater) { + method { + returnType == reelEnumClass && + parameterTypes.size == 1 && + parameterTypes[0].startsWith("L") + } + } else { + method { + definingClass == reelEnumClass && + returnType == reelEnumClass && + parameterTypes.size == 1 && + parameterTypes[0] == "I" + } + } + + findInstructionIndicesReversedOrThrow { match(0, 0) {} }.forEach { index -> val register = getInstruction(index + 1).registerA addInstructions( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt index 518902cab2..d7b658eaf1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsplayer/OpenShortsInRegularPlayerPatch.kt @@ -57,7 +57,9 @@ val openShortsInRegularPlayerPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsresuming/DisableResumingShortsOnStartupPatch.kt similarity index 73% rename from patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsresuming/DisableResumingShortsOnStartupPatch.kt index c727aa8267..fd44fbe1f7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsresuming/DisableResumingShortsOnStartupPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.youtube.layout.startupshortsreset +package app.revanced.patches.youtube.layout.shortsresuming import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.getInstruction @@ -18,10 +18,11 @@ import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference private const val EXTENSION_CLASS_DESCRIPTOR = - "Lapp/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch;" + "Lapp/revanced/extension/youtube/patches/DisableResumingShortsOnStartupPatch;" @Suppress("unused") val disableResumingShortsOnStartupPatch = bytecodePatch( @@ -42,25 +43,36 @@ val disableResumingShortsOnStartupPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" - // This patch is obsolete with 21.03 because YT seems to have - // removed resuming Shorts functionality. - // TODO: Before adding 21.03+, merge this patch into `Hide Shorts component` + "20.40.45", + "20.44.38", + "20.45.36" ), ) apply { - // 21.03+ seems to no longer have resuming Shorts functionality. - if (is_21_03_or_greater) return@apply - - addResources("youtube", "layout.startupshortsreset.disableResumingShortsOnStartupPatch") + addResources("youtube", "layout.shortsresuming.disableResumingShortsOnStartupPatch") PreferenceScreen.SHORTS.addPreferences( - SwitchPreference("revanced_disable_resuming_shorts_player"), + SwitchPreference("revanced_disable_resuming_shorts_on_startup"), ) - if (is_20_03_or_greater) { - userWasInShortsAlternativeMethodMatch.let { + if (is_21_03_or_greater) { + userWasInShortsEvaluateMethodMatch.let { + it.method.apply { + val instruction = getInstruction(it[0]) + val zMRegister = instruction.startRegister + 2 + + addInstructions( + it[0], + """ + invoke-static { v$zMRegister }, ${EXTENSION_CLASS_DESCRIPTOR}->disableResumingShortsOnStartup(Z)Z + move-result v$zMRegister + """ + ) + } + } + } else if (is_20_03_or_greater) { + userWasInShortsListenerMethodMatch.let { it.method.apply { val insertIndex = it[2] + 1 val register = getInstruction(insertIndex).registerA @@ -68,7 +80,7 @@ val disableResumingShortsOnStartupPatch = bytecodePatch( addInstructions( insertIndex, """ - invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->disableResumingStartupShortsPlayer(Z)Z + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->disableResumingShortsOnStartup(Z)Z move-result v$register """, ) @@ -86,7 +98,7 @@ val disableResumingShortsOnStartupPatch = bytecodePatch( addInstructionsAtControlFlowLabel( listenableInstructionIndex, """ - invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableResumingStartupShortsPlayer()Z + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableResumingShortsOnStartup()Z move-result v$freeRegister if-eqz v$freeRegister, :show_startup_shorts_player return-void @@ -100,7 +112,7 @@ val disableResumingShortsOnStartupPatch = bytecodePatch( userWasInShortsConfigMethod.addInstructions( 0, """ - invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->disableResumingStartupShortsPlayer()Z + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->disableResumingShortsOnStartup()Z move-result v0 if-eqz v0, :show const/4 v0, 0x0 diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsresuming/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsresuming/Fingerprints.kt new file mode 100644 index 0000000000..4392f35283 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsresuming/Fingerprints.kt @@ -0,0 +1,79 @@ +package app.revanced.patches.youtube.layout.shortsresuming + +import app.revanced.patcher.* +import app.revanced.patcher.patch.BytecodePatchContext +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import kotlin.collections.all +import kotlin.collections.zip + + +/** + * 21.03+ + */ +internal val BytecodePatchContext.userWasInShortsEvaluateMethodMatch by composingFirstMethod { + val method1ParametersPrefix = listOf("L", "Z", "Z", "L", "Z") + val method2ParametersPrefix = listOf("L", "L", "L", "L", "L", "I") + + instructions( + allOf( + Opcode.INVOKE_DIRECT_RANGE(), + method { + name == "" && parameterTypes.zip(method1ParametersPrefix) + .all { (a, b) -> a.startsWith(b) } + } + ), + afterAtMost( + 50, allOf( + Opcode.INVOKE_DIRECT_RANGE(), + method { + name == "" && parameterTypes.zip(method2ParametersPrefix) + .all { (a, b) -> a.startsWith(b) } + } + ) + ) + ) +} + +/** + * 20.02+ + */ +internal +val BytecodePatchContext.userWasInShortsListenerMethodMatch by composingFirstMethod { + returnType("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameterTypes("Ljava/lang/Object;") + instructions( + allOf(Opcode.CHECK_CAST(), type("Ljava/lang/Boolean;")), + after(method { toString() == "Ljava/lang/Boolean;->booleanValue()Z" }), + after(Opcode.MOVE_RESULT()), + // 20.40+ string was merged into another string and is a partial match. + afterAtMost(30, "ShortsStartup SetUserWasInShortsListener"(String::contains)), + ) +} + +/** + * Pre 20.02 + */ +internal +val BytecodePatchContext.userWasInShortsLegacyMethod by gettingFirstMethodDeclaratively { + returnType("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameterTypes("Ljava/lang/Object;") + instructions( + "Failed to read user_was_in_shorts proto after successful warmup"(), + ) +} + +/** + * 18.15.40+ + */ +internal +val BytecodePatchContext.userWasInShortsConfigMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Z") + parameterTypes() + instructions( + 45358360L(), + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt index 41c049d821..1c76ff702a 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt @@ -134,7 +134,9 @@ val sponsorBlockPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt index 74e4d65c5d..bf94d54d3c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt @@ -42,7 +42,9 @@ val spoofAppVersionPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt index 599a6e36ea..f8cf9a5a29 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt @@ -36,7 +36,9 @@ val changeStartPagePatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/Fingerprints.kt deleted file mode 100644 index 931bff6052..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/Fingerprints.kt +++ /dev/null @@ -1,46 +0,0 @@ -package app.revanced.patches.youtube.layout.startupshortsreset - -import app.revanced.patcher.* -import app.revanced.patcher.patch.BytecodePatchContext -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -/** - * 20.02+ - */ -internal val BytecodePatchContext.userWasInShortsAlternativeMethodMatch by composingFirstMethod { - returnType("V") - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - parameterTypes("Ljava/lang/Object;") - instructions( - allOf(Opcode.CHECK_CAST(), type("Ljava/lang/Boolean;")), - after(method { toString() == "Ljava/lang/Boolean;->booleanValue()Z" }), - after(Opcode.MOVE_RESULT()), - // 20.40+ string was merged into another string and is a partial match. - afterAtMost(15, "userIsInShorts: "(String::contains)), - ) -} - -/** - * Pre 20.02 - */ -internal val BytecodePatchContext.userWasInShortsLegacyMethod by gettingFirstMethodDeclaratively { - returnType("V") - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - parameterTypes("Ljava/lang/Object;") - instructions( - "Failed to read user_was_in_shorts proto after successful warmup"(), - ) -} - -/** - * 18.15.40+ - */ -internal val BytecodePatchContext.userWasInShortsConfigMethod by gettingFirstMethodDeclaratively { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) - returnType("Z") - parameterTypes() - instructions( - 45358360L(), - ) -} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt index 87a40c54dc..deea5d78c0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt @@ -48,7 +48,10 @@ val themePatch = baseThemePatch( default = "@android:color/white", values = mapOf( "White" to "@android:color/white", - "Material You" to "@android:color/system_neutral1_50", + "Material You (Neutral)" to "@android:color/system_neutral1_100", + "Material You - Primary" to "@android:color/system_accent1_200", + "Material You - Secondary" to "@android:color/system_accent2_200", + "Material You - Tertiary" to "@android:color/system_accent3_200", "Catppuccin (Latte)" to "#E6E9EF", "Light pink" to "#FCCFF3", "Light blue" to "#D1E0FF", @@ -195,7 +198,9 @@ val themePatch = baseThemePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt index 6051aa2e79..8701cd3a92 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt @@ -39,7 +39,9 @@ val alternativeThumbnailsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt index e785fcf199..6049823c39 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt @@ -33,7 +33,9 @@ val bypassImageRegionRestrictionsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt index 11b2729a0e..239b43df05 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt @@ -29,7 +29,9 @@ val announcementsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt index 53cc5beca7..30bff80d6e 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/audiofocus/PauseOnAudioInterruptPatch.kt @@ -12,7 +12,8 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.settings.PreferenceScreen import app.revanced.patches.youtube.misc.settings.settingsPatch -private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch;" +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/PauseOnAudioInterruptPatch;" val pauseOnAudioInterruptPatch = bytecodePatch( name = "Pause on audio interrupt", @@ -31,7 +32,9 @@ val pauseOnAudioInterruptPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt index 6f4944eaa4..dfeffaa518 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -50,7 +50,9 @@ val removeBackgroundPlaybackRestrictionsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt index ec82e4bdbe..7725d7e6a7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/ClientContextHookPatch.kt @@ -1,15 +1,21 @@ package app.revanced.patches.youtube.misc.contexthook -import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod import app.revanced.com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable import app.revanced.patcher.accessFlags +import app.revanced.patcher.allOf import app.revanced.patcher.classDef +import app.revanced.patcher.composingFirstMethod import app.revanced.patcher.extensions.addInstructionsWithLabels import app.revanced.patcher.extensions.fieldReference import app.revanced.patcher.extensions.getInstruction import app.revanced.patcher.extensions.methodReference +import app.revanced.patcher.field +import app.revanced.patcher.firstMethodComposite import app.revanced.patcher.firstMethodDeclaratively import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.method import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.bytecodePatch @@ -26,8 +32,9 @@ import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import java.lang.ref.WeakReference -private lateinit var browseIdField: FieldReference +private lateinit var clientFormFactorField: FieldReference private lateinit var clientInfoField: FieldReference private lateinit var clientVersionField: FieldReference private lateinit var messageLiteBuilderField: FieldReference @@ -41,9 +48,15 @@ enum class Endpoint( BROWSE( BytecodePatchContext::browseEndpointParentMethod::get ), + GET_WATCH( + BytecodePatchContext::getWatchEndpointConstructorPrimaryMethod::get, + BytecodePatchContext::getWatchEndpointConstructorSecondaryMethod::get, + ), GUIDE( BytecodePatchContext::guideEndpointConstructorMethod::get ), + NEXT(BytecodePatchContext::nextEndpointParentMethod::get), + PLAYER(BytecodePatchContext::playerEndpointParentMethod::get), REEL( BytecodePatchContext::reelCreateItemsEndpointConstructorMethod::get, BytecodePatchContext::reelItemWatchEndpointConstructorMethod::get, @@ -90,12 +103,8 @@ val hookClientContextPatch = bytecodePatch( } } - browseEndpointParentMethod.immutableClassDef.browseEndpointConstructorMethodMatch.let { - it.method.apply { - val browseIdIndex = it[-1] - browseIdField = - getInstruction(browseIdIndex).fieldReference!! - } + clientFormFactorField = getSetClientFormFactorMethodMatch().let { + it.method.getInstruction(it[0]).fieldReference!! } } @@ -140,7 +149,7 @@ val hookClientContextPatch = bytecodePatch( } ) - it.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + it.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> it.addInstructionsAtControlFlowLabel( index, "invoke-direct/range { p0 .. p0 }, ${it.definingClass}->$helperMethodName()V" @@ -152,19 +161,22 @@ val hookClientContextPatch = bytecodePatch( } } +fun addClientFormFactorHook(endPoint: Endpoint, descriptor: String) { + endPoint.instructions += """ + iget v2, v1, $clientFormFactorField + invoke-static { v2 }, $descriptor + move-result v2 + iput v2, v1, $clientFormFactorField + """ +} + fun addClientVersionHook(endPoint: Endpoint, descriptor: String) { - endPoint.instructions += if (endPoint == Endpoint.BROWSE) """ - iget-object v3, p0, $browseIdField - iget-object v2, v1, $clientVersionField - invoke-static { v3, v2 }, $descriptor - move-result-object v2 - iput-object v2, v1, $clientVersionField - """ else """ + endPoint.instructions += """ iget-object v2, v1, $clientVersionField invoke-static { v2 }, $descriptor move-result-object v2 iput-object v2, v1, $clientVersionField - """ + """ } fun addOSNameHook(endPoint: Endpoint, descriptor: String) { @@ -173,5 +185,5 @@ fun addOSNameHook(endPoint: Endpoint, descriptor: String) { invoke-static { v2 }, $descriptor move-result-object v2 iput-object v2, v1, $osNameField - """ + """ } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt index 9c52762689..749b577656 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/contexthook/Fingerprints.kt @@ -1,6 +1,7 @@ package app.revanced.patches.youtube.misc.contexthook import app.revanced.patcher.ClassDefComposing +import app.revanced.patcher.CompositeMatch import app.revanced.patcher.accessFlags import app.revanced.patcher.after import app.revanced.patcher.afterAtMost @@ -9,8 +10,11 @@ import app.revanced.patcher.composingFirstMethod import app.revanced.patcher.custom import app.revanced.patcher.extensions.methodReference import app.revanced.patcher.field +import app.revanced.patcher.firstImmutableMethodDeclaratively +import app.revanced.patcher.firstMethodComposite import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively import app.revanced.patcher.gettingFirstMethodDeclaratively +import app.revanced.patcher.immutableClassDef import app.revanced.patcher.instructions import app.revanced.patcher.invoke import app.revanced.patcher.method @@ -22,7 +26,6 @@ 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.Method -import com.google.common.io.ByteArrayDataOutput internal const val CLIENT_INFO_CLASS_DESCRIPTOR = $$"Lcom/google/protos/youtube/api/innertube/InnertubeContext$ClientInfo;" @@ -86,25 +89,33 @@ internal val BytecodePatchContext.buildDummyClientContextBodyMethodMatch by comp ) } -internal val ClassDef.browseEndpointConstructorMethodMatch by ClassDefComposing.composingFirstMethod { - accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) - returnType("V") - var methodDefiningClass = "" - custom { - methodDefiningClass = this.definingClass - true + + +internal fun BytecodePatchContext.getSetClientFormFactorMethodMatch(): CompositeMatch { + val clientFormFactorEnumConstructorMethod = firstImmutableMethodDeclaratively( + "UNKNOWN_FORM_FACTOR", + "SMALL_FORM_FACTOR", + "LARGE_FORM_FACTOR", + "AUTOMOTIVE_FORM_FACTOR", + "WEARABLE_FORM_FACTOR", + ) { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + }.immutableClassDef.firstMethodComposite { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returnType("L") + parameterTypes("I") } - instructions( - ""(), - after( - allOf( - Opcode.IPUT_OBJECT(), - field { definingClass == methodDefiningClass && type == "Ljava/lang/String;" } - ) - ), - ) + return firstMethodComposite { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes("L") + instructions( + allOf(Opcode.IGET(), field { type == "I" && definingClass == CLIENT_INFO_CLASS_DESCRIPTOR }), + method { this == clientFormFactorEnumConstructorMethod } + ) + } } internal val BytecodePatchContext.browseEndpointParentMethod by gettingFirstImmutableMethodDeclaratively( @@ -113,6 +124,22 @@ internal val BytecodePatchContext.browseEndpointParentMethod by gettingFirstImmu returnType("Ljava/lang/String;") } +internal val BytecodePatchContext.getWatchEndpointConstructorPrimaryMethod by gettingFirstMethodDeclaratively( + "get_watch" +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") + custom { immutableClassDef.fields.any { it.type == "Ljava/util/function/Consumer;" } } +} + +internal val BytecodePatchContext.getWatchEndpointConstructorSecondaryMethod by gettingFirstMethodDeclaratively( + "get_watch" +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returnType("V") + custom { immutableClassDef.fields.none { it.type == "Ljava/util/function/Consumer;" } } +} + internal val BytecodePatchContext.guideEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively( "guide" ) { @@ -120,6 +147,18 @@ internal val BytecodePatchContext.guideEndpointConstructorMethod by gettingFirst returnType("V") } +internal val BytecodePatchContext.nextEndpointParentMethod by gettingFirstImmutableMethodDeclaratively( + "watchNextType" +) { + returnType("Ljava/lang/String;") +} + +internal val BytecodePatchContext.playerEndpointParentMethod by gettingFirstImmutableMethodDeclaratively( + "dataExpiredForSeconds" +) { + returnType("Ljava/lang/String;") +} + internal val BytecodePatchContext.reelCreateItemsEndpointConstructorMethod by gettingFirstImmutableMethodDeclaratively( "reel/create_reel_items" ) { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt index 3ac32bd420..f3e3829b27 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt @@ -24,7 +24,9 @@ val enableDebuggingPatch = enableDebuggingPatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt index 4b1381b280..6c11fab308 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt @@ -32,7 +32,9 @@ val spoofDeviceDimensionsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt index 28125a0809..e446d4bee7 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt @@ -18,7 +18,9 @@ val checkWatchHistoryDomainNameResolutionPatch = checkWatchHistoryDomainNameReso "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt index edeeb237f9..ff5f12ac4c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt @@ -41,7 +41,9 @@ val gmsCoreSupportPatch = gmsCoreSupportPatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt index 7978e8e8b4..46d046940d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt @@ -35,7 +35,9 @@ val bypassURLRedirectsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt index 0128184a68..e251a08f3c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt @@ -47,7 +47,9 @@ val openLinksExternallyPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) 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/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt index e30ae91997..5fa1047e0d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/loopvideo/LoopVideoPatch.kt @@ -36,7 +36,9 @@ val loopVideoPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt index f289bc9035..dcdec33876 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt @@ -126,3 +126,10 @@ internal val BytecodePatchContext.playerControlsButtonStrokeFeatureFlagMethod by parameterTypes() instructions(45713296L()) } + +internal val BytecodePatchContext.playerOverlayOpacityGradientFeatureFlagMethod by gettingFirstMethodDeclaratively { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Z") + parameterTypes() + instructions(45729621L()) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt index 3dde033313..e131eb5cf0 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt @@ -307,5 +307,11 @@ val playerControlsPatch = bytecodePatch( playerControlsButtonStrokeFeatureFlagMethod.returnLate(false) } } + + if (is_21_03_or_greater) { + // If enabled it can show a black gradient on lower part of screen in fullscreen mode. + // This override may not be needed if the new bold player overlay icons are in use. + playerOverlayOpacityGradientFeatureFlagMethod.returnLate(false) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt index f2e6b41ee3..0e75333421 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -127,6 +127,10 @@ var is_21_07_or_greater : Boolean by Delegates.notNull() private set var is_21_08_or_greater : Boolean by Delegates.notNull() private set +var is_21_10_or_greater : Boolean by Delegates.notNull() + private set +var is_21_11_or_greater : Boolean by Delegates.notNull() + private set val versionCheckPatch = resourcePatch( description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.", @@ -184,5 +188,7 @@ val versionCheckPatch = resourcePatch( is_21_06_or_greater = 260705000 <= playStoreServicesVersion is_21_07_or_greater = 260805000 <= playStoreServicesVersion is_21_08_or_greater = 260905000 <= playStoreServicesVersion + is_21_10_or_greater = 261080000 <= playStoreServicesVersion + is_21_11_or_greater = 261205000 <= playStoreServicesVersion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt index a8c9549578..92b898b6ef 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/SanitizeSharingLinksPatch.kt @@ -20,7 +20,9 @@ val sanitizeSharingLinksPatch = sanitizeSharingLinksPatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ) ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt index 05a8f57caf..2165a770f1 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt @@ -38,7 +38,9 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt index 05edd856cd..a228d6deb2 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt @@ -48,7 +48,6 @@ internal val BytecodePatchContext.backgroundPlaybackManagerShortsMethod by getti } internal fun BytecodePatchContext.getEngagementPanelControllerMethodMatch() = firstMethodComposite { - accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) returnType("L") parameterTypes("L", "L", "Z", "Z") instructions( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt index 2289a65dbd..131b702901 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/audio/ForceOriginalAudioPatch.kt @@ -25,7 +25,9 @@ val forceOriginalAudioPatch = forceOriginalAudioPatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) }, diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt index 7e3360dde0..89334fa27d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt @@ -61,7 +61,9 @@ val disableVideoCodecsPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt index 614fd9c0f3..7951352312 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt @@ -1,5 +1,6 @@ package app.revanced.patches.youtube.video.quality +import app.revanced.patcher.CompositeMatch import app.revanced.patcher.accessFlags import app.revanced.patcher.afterAtMost import app.revanced.patcher.allOf @@ -7,6 +8,7 @@ import app.revanced.patcher.composingFirstMethod import app.revanced.patcher.custom import app.revanced.patcher.definingClass import app.revanced.patcher.field +import app.revanced.patcher.firstMethodComposite import app.revanced.patcher.firstMethodDeclaratively import app.revanced.patcher.gettingFirstImmutableMethodDeclaratively import app.revanced.patcher.gettingFirstMethodDeclaratively @@ -18,11 +20,17 @@ import app.revanced.patcher.opcodes import app.revanced.patcher.parameterTypes import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.returnType +import app.revanced.util.findFieldFromToString import app.revanced.util.literal 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.newAdvancedQualityMenuStyleFlyoutMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + instructions(45712556L()) +} + internal val BytecodePatchContext.currentVideoFormatToStringMethod by gettingFirstImmutableMethodDeclaratively( "currentVideoFormat=" ) { @@ -60,6 +68,31 @@ internal val BytecodePatchContext.hidePremiumVideoQualityGetArrayMethod by getti custom { AccessFlags.SYNTHETIC.isSet(immutableClassDef.accessFlags) } } +internal const val FIXED_RESOLUTION_STRING = ", initialPlaybackVideoQualityFixedResolution=" + + +internal fun BytecodePatchContext.getPlaybackStartParametersConstructorMethod(): CompositeMatch { + val playbackStartParametersToStringMethod = firstMethodDeclaratively( + FIXED_RESOLUTION_STRING + ) { + name("toString") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("Ljava/lang/String;") + parameterTypes() + } + + val initialResolutionField = playbackStartParametersToStringMethod + .findFieldFromToString(FIXED_RESOLUTION_STRING) + + // Inject a call to override initial video quality. + return playbackStartParametersToStringMethod.immutableClassDef.firstMethodComposite { + name("") + instructions( + allOf(Opcode.IPUT_OBJECT(), field { this == initialResolutionField }) + ) + } +} + internal val BytecodePatchContext.videoQualityItemOnClickParentMethod by gettingFirstImmutableMethodDeclaratively( "VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT", ) { diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 15db089e5f..36bc2b148d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -1,8 +1,15 @@ package app.revanced.patches.youtube.video.quality +import app.revanced.patcher.allOf import app.revanced.patcher.extensions.addInstruction +import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.field +import app.revanced.patcher.firstMethodComposite import app.revanced.patcher.immutableClassDef +import app.revanced.patcher.instructions +import app.revanced.patcher.invoke +import app.revanced.patcher.name import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch @@ -15,6 +22,8 @@ import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.shared.videoQualityChangedMethodMatch import app.revanced.patches.youtube.video.information.onCreateHook import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.findFieldFromToString +import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction private const val EXTENSION_CLASS_DESCRIPTOR = @@ -64,11 +73,28 @@ val rememberVideoQualityPatch = bytecodePatch { onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted") + // Inject a call to override initial video quality. + getPlaybackStartParametersConstructorMethod().let { + it.method.apply { + val index = it[-1] + val register = getInstruction(index).registerA + + addInstructions( + index, + """ + invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->getInitialVideoQuality(Lj$/util/Optional;)Lj$/util/Optional; + move-result-object v$register + """ + ) + } + } + // Inject a call to remember the selected quality for Shorts. - videoQualityItemOnClickParentMethod.immutableClassDef.getVideoQualityItemOnClickMethod().addInstruction( - 0, - "invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V", - ) + videoQualityItemOnClickParentMethod.immutableClassDef.getVideoQualityItemOnClickMethod() + .addInstruction( + 0, + "invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V", + ) // Inject a call to remember the user selected quality for regular videos. videoQualityChangedMethodMatch.let { match -> diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt index eac5428f97..67f3c427cf 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt @@ -4,7 +4,9 @@ import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.shared.misc.settings.preference.BasePreference import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.youtube.misc.playservice.is_20_40_or_greater import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.util.insertLiteralOverride /** * Video quality settings. Used to organize all speed related settings together. @@ -30,7 +32,9 @@ val videoQualityPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ), ) @@ -45,5 +49,13 @@ val videoQualityPatch = bytecodePatch( preferences = settingsMenuVideoQualityGroup, ), ) + + if (is_20_40_or_greater) { + // Flag breaks opening advanced quality menu. + // Alternatively can be fixed by using a delay when simulating the UI click. + newAdvancedQualityMenuStyleFlyoutMethodMatch.let { + it.method.insertLiteralOverride(it[0], false) + } + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt index 16b54823e2..dcdf33c858 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt @@ -33,7 +33,9 @@ val playbackSpeedPatch = bytecodePatch( "20.26.46", "20.31.42", "20.37.48", - "20.40.45" + "20.40.45", + "20.44.38", + "20.45.36" ) ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt index 606c78c458..d7837d0794 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt @@ -21,12 +21,14 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch import app.revanced.patches.youtube.misc.playservice.is_19_47_or_greater import app.revanced.patches.youtube.misc.playservice.is_20_34_or_greater +import app.revanced.patches.youtube.misc.playservice.is_21_02_or_greater import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.patches.youtube.misc.recyclerviewtree.hook.addRecyclerViewTreeHook import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch import app.revanced.patches.youtube.misc.settings.settingsPatch import app.revanced.patches.youtube.video.speed.settingsMenuVideoSpeedGroup import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.insertLiteralOverride import app.revanced.util.returnEarly import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction @@ -88,7 +90,7 @@ internal val customPlaybackSpeedPatch = bytecodePatch( serverSideMaxSpeedFeatureFlagMethod.returnEarly(false) } - // region Force old video quality menu. + // region Force old playback speed menu. // Replace the speeds float array with custom speeds. speedArrayGeneratorMethodMatch.let { @@ -161,6 +163,15 @@ internal val customPlaybackSpeedPatch = bytecodePatch( // endregion + if (is_21_02_or_greater) { + flyoutMenuNonLegacyFeatureFlagMethodMatch.let { + it.method.insertLiteralOverride( + it[0], + "$EXTENSION_CLASS_DESCRIPTOR->useNewFlyoutMenu(Z)Z" + ) + } + } + // Close the unpatched playback dialog and show the custom speeds. addRecyclerViewTreeHook(EXTENSION_CLASS_DESCRIPTOR) @@ -186,6 +197,18 @@ internal val customPlaybackSpeedPatch = bytecodePatch( move-result v$speedRegister """ ) + + val enabledIndex = it[3].index + val enabledRegister = + getInstruction(enabledIndex).registerA + + addInstructions( + enabledIndex, + """ + invoke-static { v$enabledRegister }, $EXTENSION_CLASS_DESCRIPTOR->disableTapAndHoldSpeed(Z)Z + move-result v$enabledRegister + """ + ) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt index e431b77c37..ee8e067dd6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt @@ -32,6 +32,13 @@ internal val BytecodePatchContext.serverSideMaxSpeedFeatureFlagMethod by getting ) } +internal val BytecodePatchContext.flyoutMenuNonLegacyFeatureFlagMethodMatch by composingFirstMethod { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returnType("V") + parameterTypes() + instructions(45731126L()) +} + internal val BytecodePatchContext.speedArrayGeneratorMethodMatch by composingFirstMethod { accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) returnType("[L") diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 5694608dfe..4ee01fe84b 100644 --- a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -38,7 +38,7 @@ import kotlin.collections.remove * * @param fieldName The name of the field to find. Partial matches are allowed. */ -private fun Method.findInstructionIndexFromToString(fieldName: String): Int { +private fun Method.findInstructionIndexFromToString(fieldName: String, isField: Boolean) : Int { val stringIndex = indexOfFirstInstruction { val reference = getReference() reference?.string?.contains(fieldName) == true @@ -67,22 +67,55 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int { // Should never happen. throw IllegalArgumentException("Could not find StringBuilder append usage in: $this") } - val fieldUsageRegister = getInstruction(fieldUsageIndex).registerD + + var fieldUsageRegister = getInstruction(fieldUsageIndex).registerD // Look backwards up the method to find the instruction that sets the register. var fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldUsageIndex - 1) { fieldUsageRegister == writeRegister } - // If the field is a method call, then adjust from MOVE_RESULT to the method call. - val fieldSetOpcode = getInstruction(fieldSetIndex).opcode - if (fieldSetOpcode == MOVE_RESULT || - fieldSetOpcode == MOVE_RESULT_WIDE || - fieldSetOpcode == MOVE_RESULT_OBJECT - ) { - fieldSetIndex-- + // Some 'toString()' methods, despite using a StringBuilder, convert the value via + // 'Object.toString()' or 'String.valueOf(object)' before appending it to the StringBuilder. + // In this case, the correct index cannot be found. + // Additional validation is done to find the index of the correct field or method. + // + // Check up to 3 method calls. + var checksLeft = 3 + while (checksLeft > 0) { + // If the field is a method call, then adjust from MOVE_RESULT to the method call. + val fieldSetOpcode = getInstruction(fieldSetIndex).opcode + if (fieldSetOpcode == MOVE_RESULT || + fieldSetOpcode == MOVE_RESULT_WIDE || + fieldSetOpcode == MOVE_RESULT_OBJECT + ) { + fieldSetIndex-- + } + + val fieldSetReference = getInstruction(fieldSetIndex).reference + + if (isField && fieldSetReference is FieldReference || + !isField && fieldSetReference is MethodReference + ) { + // Valid index. + return fieldSetIndex + } else if (fieldSetReference is MethodReference && + // Object.toString(), String.valueOf(object) + fieldSetReference.returnType == "Ljava/lang/String;" + ) { + fieldUsageRegister = getInstruction(fieldSetIndex).registerC + + // Look backwards up the method to find the instruction that sets the register. + fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldSetIndex - 1) { + fieldUsageRegister == writeRegister + } + checksLeft-- + } else { + throw IllegalArgumentException("Unknown reference: $fieldSetReference") + } } + return fieldSetIndex } @@ -93,7 +126,7 @@ private fun Method.findInstructionIndexFromToString(fieldName: String): Int { */ context(context: BytecodePatchContext) internal fun Method.findMethodFromToString(fieldName: String): MutableMethod { - val methodUsageIndex = findInstructionIndexFromToString(fieldName) + val methodUsageIndex = findInstructionIndexFromToString(fieldName, false) return context.navigate(this).to(methodUsageIndex).stop() } @@ -103,7 +136,7 @@ internal fun Method.findMethodFromToString(fieldName: String): MutableMethod { * @param fieldName The name of the field to find. Partial matches are allowed. */ internal fun Method.findFieldFromToString(fieldName: String): FieldReference { - val methodUsageIndex = findInstructionIndexFromToString(fieldName) + val methodUsageIndex = findInstructionIndexFromToString(fieldName, true) return getInstruction(methodUsageIndex).getReference()!! } diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index e75cb6671a..c68dd0f599 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -188,6 +188,52 @@ + + + @string/revanced_change_start_page_entry_default + @string/revanced_change_start_page_entry_charts + @string/revanced_change_start_page_entry_episodes_for_later + @string/revanced_change_start_page_entry_explore + @string/revanced_change_start_page_entry_history + @string/revanced_change_start_page_entry_library + @string/revanced_change_start_page_entry_liked_music + @string/revanced_change_start_page_entry_playlists + @string/revanced_change_start_page_entry_search + @string/revanced_change_start_page_entry_subscriptions + + + DEFAULT + CHARTS + EPISODES_FOR_LATER + EXPLORE + HISTORY + LIBRARY + LIKED_MUSIC + PLAYLISTS + SEARCH + SUBSCRIPTIONS + + + + + @string/revanced_header_logo_entry_1 + @string/revanced_header_logo_entry_2 + + + DEFAULT + REGULAR + + + @string/revanced_header_logo_entry_1 + @string/revanced_header_logo_entry_2 + @string/revanced_header_logo_entry_3 + + + DEFAULT + REGULAR + CUSTOM + + @@ -425,7 +471,7 @@ MODERN_3 - + @string/revanced_header_logo_entry_1 @string/revanced_header_logo_entry_2 diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 28a4d71661..becd1a11ef 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -95,6 +95,13 @@ To translate new languages or improve the existing translations, visit translate App language Import / Export Import / Export ReVanced settings + Import from file + Settings imported successfully, save to apply + Failed to import settings + Export to file + Settings exported successfully + Failed to export settings + You are using ReVanced Patches version <i>%s</i> Note @@ -160,7 +167,7 @@ Playback may not work" Using original audio language Using default audio - To use this feature, change \'Spoof video streams\' to any client except Android Studio + To use this feature, change \'Spoof video streams\' to Android Reel Debugging @@ -281,6 +288,7 @@ However, enabling this will also log some user data such as your IP address." @@ -368,10 +376,10 @@ However, enabling this will also log some user data such as your IP address."Medical panels are shown Hide quick actions Quick actions in fullscreen are hidden - Quick actions in fullscreen are shown - Hide related videos - Related videos in quick actions are hidden - Related videos in quick actions are shown + Quick actions in fullscreen are shown + Hide quick actions related videos + Related videos in quick actions are hidden + Related videos in quick actions are shown Hide subscribers guidelines Subscribers community guidelines are hidden Subscribers community guidelines are shown @@ -478,10 +486,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 @@ -506,9 +510,6 @@ However, enabling this will also log some user data such as your IP address."Hide AI chat summary AI chat summary is hidden AI chat summary is shown - Hide AI comments summary - AI comments summary is hidden - AI comments summary is shown Hide channel guidelines Channel guidelines are hidden Channel guidelines are shown @@ -533,6 +534,9 @@ However, enabling this will also log some user data such as your IP address."Hide Thanks button Thanks button is hidden Thanks button is shown + Sanitize category bar + Category buttons except \'Top\' and \'Newest\' are hidden in the comment category bar + Category buttons are shown in the comment category bar Hide view count View count is hidden in feed and search results View count is shown in feed and search results @@ -1121,16 +1125,13 @@ To show the Audio track menu, change \'Spoof video streams\' to \'Android >Reel\ Hide end screen suggested video - "End screen suggested video is hidden when autoplay is turned off - -Autoplay can be changed in YouTube settings: -Settings → Playback → Autoplay next video" + End screen suggested video is hidden End screen suggested video is shown - Hide related videos overlay - Related videos overlay in fullscreen is hidden - Related videos overlay in fullscreen is shown + Hide related videos overlay + Related videos overlay in fullscreen is hidden + Related videos overlay in fullscreen is shown Hide video timestamp @@ -1434,10 +1435,16 @@ Ready to submit?" Tablet layout • Community posts are hidden +• Playback in feeds setting is not available +• Remix button and Sound button are not available in Shorts +• Video action bar is not collapsed Automotive layout +• Feed is organized by topics and channels +• Playback in feeds setting is not available +• Remix button and Sound button are not available in Shorts • Shorts open in the regular player -• Feed is organized by topics and channels" +• Video action bar is not collapsed" Spoof app version @@ -1452,6 +1459,11 @@ If later turned off, it is recommended to clear the app data to prevent UI bugs. 20.13.41 - Restore non collapsed video action bar 20.05.46 - Restore transcript functionality + + Override \'Open in YouTube Music\' button + \'Open in YouTube Music\' button opens your target music app + \'Open in YouTube Music\' button opens the original app + Change start page Default @@ -1485,10 +1497,10 @@ If later turned off, it is recommended to clear the app data to prevent UI bugs. Limitation: Using the back button on the toolbar may not work" Start page is changed only on app startup - - Disable resuming Shorts player - Shorts player will not resume on app startup - Shorts player will resume on app startup + + Disable resuming Shorts player + Shorts player will not resume on app startup + Shorts player will resume on app startup Open Shorts with @@ -1578,7 +1590,7 @@ Swipe to expand or close" YT ReVanced YT - + Header logo Default Regular @@ -1743,7 +1755,9 @@ Enabling this can unlock higher video qualities" Invalid custom playback speeds Auto Custom tap and hold speed - Playback speed between 0-8 + "Playback speed between 0-8 + +Set to 0, to disable tap and hold speed" Remember playback speed changes @@ -1802,6 +1816,12 @@ Video playback with AV1 may stutter or drop frames." + + Header logo + Default + ReVanced + Custom + YT Music ReVanced Music ReVanced @@ -1839,16 +1859,35 @@ Video playback with AV1 may stutter or drop frames." Search button is hidden Search button is shown + + Change start page + Default + Charts + Episodes for later + Explore + History + Library + Liked music + Playlists + Search + Subscriptions + + Hide category bar Category bar is hidden Category bar is shown - + Change miniplayer color Miniplayer color matches fullscreen player Miniplayer uses default color + + Forcibly enable miniplayer + Miniplayer stays minimized when playback changes + Miniplayer expands to fullscreen when playback changes + Navigation bar Hide or change navigation bar buttons