diff --git a/README.md b/README.md
index 6273da940..cb7f710c0 100644
--- a/README.md
+++ b/README.md
@@ -11,72 +11,73 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
-| `Alternative thumbnails` | Adds options to replace video thumbnails using the DeArrow API or image captures from the video. | 18.29.38 ~ 19.44.39 |
-| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.44.39 |
-| `Bypass URL redirects` | Adds an option to bypass URL redirects and open the original URL directly. | 18.29.38 ~ 19.44.39 |
-| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 18.29.38 ~ 19.44.39 |
-| `Change form factor` | Adds an option to change the UI appearance to a phone, tablet, or automotive device. | 18.29.38 ~ 19.44.39 |
-| `Change live ring click action` | Adds an option to open the channel instead of the live stream when clicking on the live ring. | 18.29.38 ~ 19.44.39 |
-| `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 18.29.38 ~ 19.44.39 |
-| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 18.29.38 ~ 19.44.39 |
-| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.44.39 |
-| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.44.39 |
-| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 18.29.38 ~ 19.44.39 |
-| `Custom branding name for YouTube` | Changes the YouTube app name to the name specified in patch options. | 18.29.38 ~ 19.44.39 |
-| `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 18.29.38 ~ 19.44.39 |
-| `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 18.29.38 ~ 19.44.39 |
-| `Description components` | Adds options to hide and disable description components. | 18.29.38 ~ 19.44.39 |
-| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 18.29.38 ~ 19.44.39 |
-| `Disable forced auto audio tracks` | Adds an option to disable audio tracks from being automatically enabled. | 18.29.38 ~ 19.44.39 |
-| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 18.29.38 ~ 19.44.39 |
-| `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 18.29.38 ~ 19.44.39 |
-| `Disable resuming Miniplayer on startup` | Adds an option to disable the Miniplayer 'Continue watching' from resuming on app startup. | 18.29.38 ~ 19.44.39 |
-| `Disable resuming Shorts on startup` | Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched. | 18.29.38 ~ 19.44.39 |
-| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 18.29.38 ~ 19.44.39 |
-| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 18.29.38 ~ 19.44.39 |
-| `Enable debug logging` | Adds an option to enable debug logging. | 18.29.38 ~ 19.44.39 |
-| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 18.29.38 ~ 19.44.39 |
-| `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 18.29.38 ~ 19.44.39 |
-| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 18.29.38 ~ 19.44.39 |
-| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 18.29.38 ~ 19.44.39 |
-| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts videos. | 18.29.38 ~ 19.44.39 |
-| `Hide accessibility controls dialog` | Removes, at compile time, accessibility controls dialog 'Turn on accessibility controls for the video player?'. | 18.29.38 ~ 19.44.39 |
-| `Hide action buttons` | Adds options to hide action buttons under videos. | 18.29.38 ~ 19.44.39 |
-| `Hide ads` | Adds options to hide ads. | 18.29.38 ~ 19.44.39 |
-| `Hide comments components` | Adds options to hide components related to comments. | 18.29.38 ~ 19.44.39 |
-| `Hide feed components` | Adds options to hide components related to feeds. | 18.29.38 ~ 19.44.39 |
-| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 18.29.38 ~ 19.44.39 |
-| `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.44.39 |
-| `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.44.39 |
-| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.44.39 |
-| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when the app icon is long pressed. | 18.29.38 ~ 19.44.39 |
-| `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 18.29.38 ~ 19.44.39 |
-| `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 18.29.38 ~ 19.44.39 |
-| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 18.29.38 ~ 19.44.39 |
-| `Miniplayer` | Adds options to change the in-app minimized player, and if patching target 19.16+ adds options to use modern miniplayers. | 18.29.38 ~ 19.44.39 |
-| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 18.29.38 ~ 19.44.39 |
-| `Open links externally` | Adds an option to always open links in your browser instead of the in-app browser. | 18.29.38 ~ 19.44.39 |
-| `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 18.29.38 ~ 19.44.39 |
-| `Player components` | Adds options to hide or change components related to the video player. | 18.29.38 ~ 19.44.39 |
-| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 18.29.38 ~ 19.44.39 |
-| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 18.29.38 ~ 19.44.39 |
-| `Return YouTube Dislike` | Adds an option to show the dislike count of videos using the Return YouTube Dislike API. | 18.29.38 ~ 19.44.39 |
-| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 18.29.38 ~ 19.44.39 |
-| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 18.29.38 ~ 19.44.39 |
-| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 18.29.38 ~ 19.44.39 |
-| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 18.29.38 ~ 19.44.39 |
-| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 18.29.38 ~ 19.44.39 |
-| `Snack bar components` | Adds options to hide or change components related to the snack bar. | 18.29.38 ~ 19.44.39 |
-| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 18.29.38 ~ 19.44.39 |
-| `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 18.29.38 ~ 19.44.39 |
-| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 18.29.38 ~ 19.44.39 |
-| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.44.39 |
-| `Theme` | Changes the app's themes to the values specified in patch options. | 18.29.38 ~ 19.44.39 |
-| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as the search bar, header, and toolbar buttons. | 18.29.38 ~ 19.44.39 |
-| `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.44.39 |
-| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.44.39 |
-| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 18.29.38 ~ 19.44.39 |
-| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 18.29.38 ~ 19.44.39 |
+| `Alternative thumbnails` | Adds options to replace video thumbnails using the DeArrow API or image captures from the video. | 19.05.36 ~ 19.47.53 |
+| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 19.05.36 ~ 19.47.53 |
+| `Bypass URL redirects` | Adds an option to bypass URL redirects and open the original URL directly. | 19.05.36 ~ 19.47.53 |
+| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 19.05.36 ~ 19.47.53 |
+| `Change form factor` | Adds an option to change the UI appearance to a phone, tablet, or automotive device. | 19.05.36 ~ 19.47.53 |
+| `Change live ring click action` | Adds an option to open the channel instead of the live stream when clicking on the live ring. | 19.05.36 ~ 19.47.53 |
+| `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 19.05.36 ~ 19.47.53 |
+| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 19.05.36 ~ 19.47.53 |
+| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 19.05.36 ~ 19.47.53 |
+| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 19.05.36 ~ 19.47.53 |
+| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 19.05.36 ~ 19.47.53 |
+| `Custom branding name for YouTube` | Changes the YouTube app name to the name specified in patch options. | 19.05.36 ~ 19.47.53 |
+| `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 19.05.36 ~ 19.47.53 |
+| `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 19.05.36 ~ 19.47.53 |
+| `Description components` | Adds options to hide and disable description components. | 19.05.36 ~ 19.47.53 |
+| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 19.05.36 ~ 19.47.53 |
+| `Disable forced auto audio tracks` | Adds an option to disable audio tracks from being automatically enabled. | 19.05.36 ~ 19.47.53 |
+| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 19.05.36 ~ 19.47.53 |
+| `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 19.05.36 ~ 19.47.53 |
+| `Disable layout updates` | Adds an option to disable layout updates by server. | 19.05.36 ~ 19.47.53 |
+| `Disable resuming Miniplayer on startup` | Adds an option to disable the Miniplayer 'Continue watching' from resuming on app startup. | 19.05.36 ~ 19.47.53 |
+| `Disable resuming Shorts on startup` | Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched. | 19.05.36 ~ 19.47.53 |
+| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 19.05.36 ~ 19.47.53 |
+| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 19.05.36 ~ 19.47.53 |
+| `Enable debug logging` | Adds an option to enable debug logging. | 19.05.36 ~ 19.47.53 |
+| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 19.05.36 ~ 19.47.53 |
+| `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 19.05.36 ~ 19.47.53 |
+| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 19.05.36 ~ 19.47.53 |
+| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 19.05.36 ~ 19.47.53 |
+| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts videos. | 19.05.36 ~ 19.47.53 |
+| `Hide accessibility controls dialog` | Removes, at compile time, accessibility controls dialog 'Turn on accessibility controls for the video player?'. | 19.05.36 ~ 19.47.53 |
+| `Hide action buttons` | Adds options to hide action buttons under videos. | 19.05.36 ~ 19.47.53 |
+| `Hide ads` | Adds options to hide ads. | 19.05.36 ~ 19.47.53 |
+| `Hide comments components` | Adds options to hide components related to comments. | 19.05.36 ~ 19.47.53 |
+| `Hide feed components` | Adds options to hide components related to feeds. | 19.05.36 ~ 19.47.53 |
+| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 19.05.36 ~ 19.47.53 |
+| `Hide layout components` | Adds options to hide general layout components. | 19.05.36 ~ 19.47.53 |
+| `Hide player buttons` | Adds options to hide buttons in the video player. | 19.05.36 ~ 19.47.53 |
+| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 19.05.36 ~ 19.47.53 |
+| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when the app icon is long pressed. | 19.05.36 ~ 19.47.53 |
+| `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 19.05.36 ~ 19.47.53 |
+| `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 19.05.36 ~ 19.47.53 |
+| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 19.05.36 ~ 19.47.53 |
+| `Miniplayer` | Adds options to change the in-app minimized player, and if patching target 19.16+ adds options to use modern miniplayers. | 19.05.36 ~ 19.47.53 |
+| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 19.05.36 ~ 19.47.53 |
+| `Open links externally` | Adds an option to always open links in your browser instead of the in-app browser. | 19.05.36 ~ 19.47.53 |
+| `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 19.05.36 ~ 19.47.53 |
+| `Player components` | Adds options to hide or change components related to the video player. | 19.05.36 ~ 19.47.53 |
+| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 19.05.36 ~ 19.47.53 |
+| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 19.05.36 ~ 19.47.53 |
+| `Return YouTube Dislike` | Adds an option to show the dislike count of videos using the Return YouTube Dislike API. | 19.05.36 ~ 19.47.53 |
+| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 19.05.36 ~ 19.47.53 |
+| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 19.05.36 ~ 19.47.53 |
+| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 19.05.36 ~ 19.47.53 |
+| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 19.05.36 ~ 19.47.53 |
+| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 19.05.36 ~ 19.47.53 |
+| `Snack bar components` | Adds options to hide or change components related to the snack bar. | 19.05.36 ~ 19.47.53 |
+| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 19.05.36 ~ 19.47.53 |
+| `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 19.05.36 ~ 19.47.53 |
+| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 19.05.36 ~ 19.47.53 |
+| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 19.05.36 ~ 19.47.53 |
+| `Theme` | Changes the app's themes to the values specified in patch options. | 19.05.36 ~ 19.47.53 |
+| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as the search bar, header, and toolbar buttons. | 19.05.36 ~ 19.47.53 |
+| `Translations for YouTube` | Add translations or remove string resources. | 19.05.36 ~ 19.47.53 |
+| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 19.05.36 ~ 19.47.53 |
+| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 19.05.36 ~ 19.47.53 |
+| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 19.05.36 ~ 19.47.53 |
### [📦 `com.google.android.apps.youtube.music`](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music)
@@ -84,49 +85,48 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
-| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.10.51 |
-| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.10.51 |
-| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.10.51 |
-| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.10.51 |
-| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.10.51 |
-| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.10.51 |
-| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.10.51 |
-| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.10.51 |
-| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.10.51 |
-| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.10.51 |
-| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.10.51 |
-| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.10.51 |
-| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.10.51 |
-| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.10.51 |
-| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.10.51 |
-| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.10.51 |
-| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.10.51 |
-| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.10.51 |
-| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.10.51 |
-| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.10.51 |
-| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.10.51 |
-| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.10.51 |
-| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.10.51 |
-| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.10.51 |
-| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.10.51 |
-| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.10.51 |
-| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.10.51 |
-| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.10.51 |
-| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.10.51 |
-| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.10.51 |
-| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.10.51 |
-| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.10.51 |
-| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.10.51 |
-| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.10.51 |
-| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.10.51 |
-| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.10.51 |
-| `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 7.16.53 |
-| `Spoof client` | Adds options to spoof the client to allow playback. | 6.20.51 ~ 8.10.51 |
-| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.10.51 |
-| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.10.51 |
-| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.10.51 |
-| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.10.51 |
-| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.10.51 |
+| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.12.53 |
+| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.12.53 |
+| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.12.53 |
+| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.12.53 |
+| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.12.53 |
+| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.12.53 |
+| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.12.53 |
+| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.12.53 |
+| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.12.53 |
+| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.12.53 |
+| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.12.53 |
+| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.12.53 |
+| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.12.53 |
+| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.12.53 |
+| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.12.53 |
+| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.12.53 |
+| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.12.53 |
+| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.12.53 |
+| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.12.53 |
+| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.12.53 |
+| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.12.53 |
+| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.12.53 |
+| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.12.53 |
+| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.12.53 |
+| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.12.53 |
+| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.12.53 |
+| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.12.53 |
+| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.12.53 |
+| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.12.53 |
+| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.12.53 |
+| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.12.53 |
+| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.12.53 |
+| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.12.53 |
+| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.12.53 |
+| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.12.53 |
+| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.12.53 |
+| `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 8.10.52 |
+| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.12.53 |
+| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.12.53 |
+| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.12.53 |
+| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.12.53 |
+| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.12.53 |
### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage)
@@ -134,19 +134,19 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
-| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 |
-| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 |
-| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.05.1 |
-| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.05.1 |
-| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.05.1 |
-| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.05.1 |
-| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.05.1 |
-| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.05.1 |
-| `Open links externally` | Adds an option to always open links in your browser instead of in the in-app-browser. | 2024.17.0 ~ 2025.05.1 |
-| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.05.1 |
-| `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | 2024.17.0 ~ 2025.05.1 |
-| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.05.1 |
-| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.05.1 |
+| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.12.0 |
+| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.12.0 |
+| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.12.0 |
+| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.12.0 |
+| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.12.0 |
+| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.12.0 |
+| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.12.0 |
+| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.12.0 |
+| `Open links externally` | Adds an option to always open links in your browser instead of in the in-app-browser. | 2024.17.0 ~ 2025.12.0 |
+| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.12.0 |
+| `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | 2024.17.0 ~ 2025.12.0 |
+| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.12.0 |
+| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.12.0 |
@@ -165,13 +165,11 @@ Example:
"use":true,
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -189,7 +187,7 @@ Example:
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -201,7 +199,8 @@ Example:
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
diff --git a/extensions/shared/build.gradle.kts b/extensions/shared/build.gradle.kts
index 69641ab9e..d9511b8b8 100644
--- a/extensions/shared/build.gradle.kts
+++ b/extensions/shared/build.gradle.kts
@@ -26,6 +26,7 @@ android {
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.preference)
+ implementation(libs.collections4)
implementation(libs.lang3)
compileOnly(project(":extensions:shared:stub"))
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java
index 71ae52a34..be5106d04 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java
@@ -108,13 +108,13 @@ public class FlyoutPatch {
if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() &&
textView.getParent() instanceof ViewGroup clickAbleArea) {
runOnMainThreadDelayed(() -> {
- textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label"));
- imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
- clickAbleArea.setOnClickListener(view -> {
- clickView(touchOutSideViewRef.get());
- VideoUtils.openInYouTube();
- });
- }, 0L
+ textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label"));
+ imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
+ clickAbleArea.setOnClickListener(view -> {
+ clickView(touchOutSideViewRef.get());
+ VideoUtils.openInYouTube();
+ });
+ }, 0L
);
}
}
@@ -126,14 +126,14 @@ public class FlyoutPatch {
textView.getParent() instanceof ViewGroup clickAbleArea
) {
runOnMainThreadDelayed(() -> {
- textView.setText(str("playback_rate_title"));
- imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
- imageView.setColorFilter(cf);
- clickAbleArea.setOnClickListener(view -> {
- clickView(touchOutSideViewRef.get());
- VideoUtils.showPlaybackSpeedFlyoutMenu();
- });
- }, 0L
+ textView.setText(str("playback_rate_title"));
+ imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
+ imageView.setColorFilter(cf);
+ clickAbleArea.setOnClickListener(view -> {
+ clickView(touchOutSideViewRef.get());
+ VideoUtils.showPlaybackSpeedFlyoutMenu();
+ });
+ }, 0L
);
}
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/GeneralPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/GeneralPatch.java
index f22e9aff4..1f3311ad1 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/GeneralPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/GeneralPatch.java
@@ -9,13 +9,11 @@ import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.ResourceUtils;
-/**
- * @noinspection ALL
- */
@SuppressWarnings("unused")
public class GeneralPatch {
@@ -79,6 +77,13 @@ public class GeneralPatch {
}
}
+ public static void hideSearchButton(View view) {
+ if (Settings.HIDE_SEARCH_BUTTON.get()) {
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
+ view.setLayoutParams(layoutParams);
+ }
+ }
+
public static boolean hideSoundSearchButton() {
return Settings.HIDE_SOUND_SEARCH_BUTTON.get();
}
@@ -123,7 +128,7 @@ public class GeneralPatch {
*
* The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called.
* Otherwise {@link AlertDialog#getButton(int)} method will always return null.
- * https://stackoverflow.com/a/4604145
+ * Reference
*
* That's why {@link AlertDialog#show()} is absolutely necessary.
* Instead, use two tricks to hide Alertdialog.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java
index e275463d1..6c228af1a 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java
@@ -7,15 +7,12 @@ import androidx.annotation.NonNull;
import java.util.LinkedHashMap;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.music.utils.VideoUtils;
-import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
-import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class AlbumMusicVideoPatch {
@@ -40,7 +37,7 @@ public class AlbumMusicVideoPatch {
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
- private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false);
+ private static volatile boolean isVideoLaunched = false;
@NonNull
private static volatile String playerResponseVideoId = "";
@@ -100,14 +97,6 @@ public class AlbumMusicVideoPatch {
if (request == null) {
return;
}
- // This hook is always called off the main thread,
- // but this can later be called for the same video id from the main thread.
- // This is not a concern, since the fetch will always be finished
- // and never block the main thread.
- // But if debugging, then still verify this is the situation.
- if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
- Logger.printException(() -> "Error: Blocking main thread");
- }
String songId = request.getStream();
if (songId.isEmpty()) {
Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
@@ -149,17 +138,16 @@ public class AlbumMusicVideoPatch {
private static void openMusic(@NonNull String songId) {
try {
- isVideoLaunched.compareAndSet(false, true);
-
// The newly opened video is not a music video.
// To prevent fetch requests from being sent, set the video id to the newly opened video
VideoUtils.runOnMainThreadDelayed(() -> {
+ isVideoLaunched = true;
playerResponseVideoId = songId;
currentVideoId = songId;
VideoUtils.openInYouTubeMusic(songId);
- }, 1000);
+ VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched = false, 3000);
+ }, 1500);
- VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 2500);
} catch (Exception ex) {
Logger.printException(() -> "openMusic failure", ex);
}
@@ -191,7 +179,7 @@ public class AlbumMusicVideoPatch {
* Injection point.
*/
public static boolean hideSnackBar() {
- return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched.get();
+ return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched;
}
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java
index d5e5b014b..23082f50e 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java
@@ -9,7 +9,6 @@ import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
-import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt
index 90fb6f963..62b98a5e3 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt
@@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.misc.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
-import app.revanced.extension.shared.patches.client.YouTubeAppClient
-import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.settings.AppLanguage
import app.revanced.extension.shared.utils.Logger
@@ -136,10 +138,11 @@ class PlaylistRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
- val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
- PlayerRoutes.GET_PLAYLIST_PAGE,
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ GET_PLAYLIST_PAGE,
clientType
)
+
/**
* For some reason, the tracks in Top Songs have the playlistId of the album:
* [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835)
@@ -152,7 +155,7 @@ class PlaylistRequest private constructor(
* So we can work around this by setting the language to English when sending the request.
*/
val requestBody =
- PlayerRoutes.createApplicationRequestBody(
+ createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId,
diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java
index 3f74973f2..05a214f8f 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java
@@ -3,6 +3,7 @@ package app.revanced.extension.music.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
+import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
@@ -109,6 +110,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_notification_button", FALSE, true);
public static final BooleanSetting HIDE_PLAYLIST_CARD_SHELF = new BooleanSetting("revanced_hide_playlist_card_shelf", FALSE, true);
public static final BooleanSetting HIDE_SAMPLE_SHELF = new BooleanSetting("revanced_hide_samples_shelf", FALSE, true);
+ public static final BooleanSetting HIDE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_search_button", FALSE, true);
public static final BooleanSetting HIDE_SOUND_SEARCH_BUTTON = new BooleanSetting("revanced_hide_sound_search_button", FALSE, true);
public static final BooleanSetting HIDE_TAP_TO_UPDATE_BUTTON = new BooleanSetting("revanced_hide_tap_to_update_button", FALSE, true);
public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true);
@@ -240,7 +242,10 @@ public class Settings extends BaseSettings {
// region Migration
// Old spoof versions that no longer work reliably.
- if (SPOOF_APP_VERSION_TARGET.get().compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0) {
+ String spoofAppVersionTarget = SPOOF_APP_VERSION_TARGET.get();
+ if (spoofAppVersionTarget.compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0) {
+ Utils.showToastShort(str("revanced_spoof_app_version_target_invalid_toast", spoofAppVersionTarget));
+ Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
Logger.printInfo(() -> "Resetting spoof app version target");
SPOOF_APP_VERSION_TARGET.resetToDefault();
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/Settings.java
index 2efc2eb37..eee9c0ae1 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/Settings.java
@@ -13,7 +13,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true);
// Layout
- public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE);
+ public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE, true);
public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true);
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true);
public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true);
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeAppClient.kt
similarity index 94%
rename from extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt
rename to extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeAppClient.kt
index 8723b8297..bbd1ba9e9 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeAppClient.kt
@@ -1,6 +1,7 @@
-package app.revanced.extension.shared.patches.client
+package app.revanced.extension.shared.innertube.client
import android.os.Build
+import app.revanced.extension.shared.patches.PatchStatus
import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.PackageUtils
import org.apache.commons.lang3.ArrayUtils
@@ -212,8 +213,15 @@ object YouTubeAppClient {
return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get()
}
+ private fun useIOS(): Boolean {
+ return PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get()
+ }
+
fun availableClientTypes(preferredClient: ClientType): Array {
- val availableClientTypes = ClientType.CLIENT_ORDER_TO_USE_YOUTUBE
+ val availableClientTypes = if (useIOS())
+ ClientType.CLIENT_ORDER_TO_USE_IOS
+ else
+ ClientType.CLIENT_ORDER_TO_USE
if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
val clientToUse: Array = arrayOfNulls(availableClientTypes.size)
@@ -230,7 +238,7 @@ object YouTubeAppClient {
}
}
- @Suppress("DEPRECATION")
+ @Suppress("DEPRECATION", "unused")
enum class ClientType(
/**
* [YouTube client type](https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients)
@@ -278,10 +286,6 @@ object YouTubeAppClient {
* If true, 'Authorization' must be included.
*/
val requireAuth: Boolean = false,
- /**
- * Whether a poToken is required to get playback for more than 1 minute.
- */
- val requirePoToken: Boolean = false,
/**
* Client name for innertube body.
*/
@@ -363,7 +367,7 @@ object YouTubeAppClient {
else
"iOS TV"
),
- IOS(
+ IOS_DEPRECATED(
id = 5,
deviceMake = DEVICE_MAKE_IOS,
deviceModel = DEVICE_MODEL_IOS,
@@ -372,7 +376,6 @@ object YouTubeAppClient {
userAgent = USER_AGENT_IOS,
clientVersion = CLIENT_VERSION_IOS,
supportsCookies = false,
- requirePoToken = true,
clientName = "IOS",
friendlyName = if (forceAVC())
"iOS Force AVC"
@@ -381,12 +384,20 @@ object YouTubeAppClient {
);
companion object {
- val CLIENT_ORDER_TO_USE_YOUTUBE: Array = arrayOf(
+ val CLIENT_ORDER_TO_USE: Array = arrayOf(
ANDROID_VR_NO_AUTH,
ANDROID_UNPLUGGED,
ANDROID_CREATOR,
IOS_UNPLUGGED,
- IOS,
+ ANDROID_VR,
+ )
+
+ val CLIENT_ORDER_TO_USE_IOS: Array = arrayOf(
+ ANDROID_VR_NO_AUTH,
+ ANDROID_UNPLUGGED,
+ ANDROID_CREATOR,
+ IOS_UNPLUGGED,
+ IOS_DEPRECATED,
ANDROID_VR,
)
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/MusicAppClient.java b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeMusicAppClient.java
similarity index 98%
rename from extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/MusicAppClient.java
rename to extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeMusicAppClient.java
index 21b580c8d..24081156a 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/MusicAppClient.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeMusicAppClient.java
@@ -1,10 +1,10 @@
-package app.revanced.extension.shared.patches.client;
+package app.revanced.extension.shared.innertube.client;
import android.os.Build;
import java.util.Locale;
-public class MusicAppClient {
+public class YouTubeMusicAppClient {
// Response to the '/next' request is 'Please update to continue using the app':
// https://github.com/inotia00/ReVanced_Extended/issues/2743
@@ -46,7 +46,7 @@ public class MusicAppClient {
private static final String DEVICE_MAKE_IOS_MUSIC = "Apple";
private static final String OS_NAME_IOS_MUSIC = "iOS";
- private MusicAppClient() {
+ private YouTubeMusicAppClient() {
}
private static String androidUserAgent(String clientVersion) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeWebClient.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeWebClient.kt
similarity index 72%
rename from extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeWebClient.kt
rename to extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeWebClient.kt
index 331d1d9a4..3f9c5e8c0 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeWebClient.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/client/YouTubeWebClient.kt
@@ -1,14 +1,10 @@
-package app.revanced.extension.shared.patches.client
+package app.revanced.extension.shared.innertube.client
/**
* Used to fetch video information.
*/
@Suppress("unused")
object YouTubeWebClient {
- /**
- * This user agent does not require a PoToken in [ClientType.MWEB]
- * https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259
- */
private const val USER_AGENT_SAFARI =
"Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)"
@@ -26,11 +22,11 @@ object YouTubeWebClient {
* Client version.
*/
@JvmField
- val clientVersion: String
+ val clientVersion: String,
) {
MWEB(
id = 2,
- clientVersion = "2.20241202.07.00"
+ clientVersion = "2.20241202.07.00",
),
WEB_REMIX(
id = 29,
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/requests/InnerTubeRequestBody.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/requests/InnerTubeRequestBody.kt
new file mode 100644
index 000000000..6e6a8c0d8
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/requests/InnerTubeRequestBody.kt
@@ -0,0 +1,364 @@
+package app.revanced.extension.shared.innertube.requests
+
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.client.YouTubeWebClient
+import app.revanced.extension.shared.requests.Requester
+import app.revanced.extension.shared.requests.Route.CompiledRoute
+import app.revanced.extension.shared.settings.BaseSettings
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.StringRef.str
+import app.revanced.extension.shared.utils.Utils
+import org.apache.commons.lang3.StringUtils
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.nio.charset.StandardCharsets
+import java.util.Date
+import java.util.Locale
+import java.util.TimeZone
+
+@Suppress("deprecation")
+object InnerTubeRequestBody {
+
+ private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"
+
+ private const val AUTHORIZATION_HEADER = "Authorization"
+ private val REQUEST_HEADER_KEYS = setOf(
+ AUTHORIZATION_HEADER, // Available only to logged-in users.
+ "X-GOOG-API-FORMAT-VERSION",
+ "X-Goog-Visitor-Id"
+ )
+
+ /**
+ * TCP connection and HTTP read timeout
+ */
+ private const val CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000 // 10 Seconds.
+
+ private val LOCALE: Locale = Utils.getContext().resources
+ .configuration.locale
+ private val LOCALE_COUNTRY: String = LOCALE.country
+ private val LOCALE_LANGUAGE: String = LOCALE.language
+ private val TIME_ZONE: TimeZone = TimeZone.getDefault()
+ private val TIME_ZONE_ID: String = TIME_ZONE.id
+ private val UTC_OFFSET_MINUTES: Int = TIME_ZONE.getOffset(Date().time) / 60000
+
+ @JvmStatic
+ fun createApplicationRequestBody(
+ clientType: YouTubeAppClient.ClientType,
+ videoId: String,
+ playlistId: String? = null,
+ botGuardPoToken: String = "",
+ visitorId: String = "",
+ setLocale: Boolean = false,
+ language: String = BaseSettings.SPOOF_STREAMING_DATA_LANGUAGE.get().language,
+ ): ByteArray {
+ val innerTubeBody = JSONObject()
+
+ try {
+ val client = JSONObject()
+ client.put("deviceMake", clientType.deviceMake)
+ client.put("deviceModel", clientType.deviceModel)
+ client.put("clientName", clientType.clientName)
+ client.put("clientVersion", clientType.clientVersion)
+ client.put("osName", clientType.osName)
+ client.put("osVersion", clientType.osVersion)
+ if (clientType.androidSdkVersion != null) {
+ client.put("androidSdkVersion", clientType.androidSdkVersion)
+ if (clientType.gmscoreVersionCode != null) {
+ client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
+ }
+ }
+ client.put(
+ "hl",
+ if (setLocale) {
+ language
+ } else {
+ LOCALE_LANGUAGE
+ }
+ )
+ client.put("gl", LOCALE_COUNTRY)
+ client.put("timeZone", TIME_ZONE_ID)
+ client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES")
+
+ val context = JSONObject()
+ context.put("client", client)
+
+ innerTubeBody.put("context", context)
+ innerTubeBody.put("contentCheckOk", true)
+ innerTubeBody.put("racyCheckOk", true)
+ innerTubeBody.put("videoId", videoId)
+
+ if (playlistId != null) {
+ innerTubeBody.put("playlistId", playlistId)
+ }
+
+ if (!StringUtils.isAnyEmpty(botGuardPoToken, visitorId)) {
+ val serviceIntegrityDimensions = JSONObject()
+ serviceIntegrityDimensions.put("poToken", botGuardPoToken)
+ innerTubeBody.put("serviceIntegrityDimensions", serviceIntegrityDimensions)
+ }
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create application innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun createWebInnertubeBody(
+ clientType: YouTubeWebClient.ClientType,
+ videoId: String
+ ): ByteArray {
+ val innerTubeBody = JSONObject()
+
+ try {
+ val client = JSONObject()
+ client.put("clientName", clientType.clientName)
+ client.put("clientVersion", clientType.clientVersion)
+ val context = JSONObject()
+ context.put("client", client)
+
+ val lockedSafetyMode = JSONObject()
+ lockedSafetyMode.put("lockedSafetyMode", false)
+ val user = JSONObject()
+ user.put("user", lockedSafetyMode)
+
+ innerTubeBody.put("context", context)
+ innerTubeBody.put("contentCheckOk", true)
+ innerTubeBody.put("racyCheckOk", true)
+ innerTubeBody.put("videoId", videoId)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create web innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ private fun androidInnerTubeBody(
+ clientType: YouTubeAppClient.ClientType = YouTubeAppClient.ClientType.ANDROID
+ ): JSONObject {
+ val innerTubeBody = JSONObject()
+
+ try {
+ val client = JSONObject()
+ client.put("deviceMake", clientType.deviceMake)
+ client.put("deviceModel", clientType.deviceModel)
+ client.put("clientName", clientType.clientName)
+ client.put("clientVersion", clientType.clientVersion)
+ client.put("osName", clientType.osName)
+ client.put("osVersion", clientType.osVersion)
+ client.put("androidSdkVersion", clientType.androidSdkVersion)
+ client.put("hl", LOCALE_LANGUAGE)
+ client.put("gl", LOCALE_COUNTRY)
+ client.put("timeZone", TIME_ZONE_ID)
+ client.put("utcOffsetMinutes", UTC_OFFSET_MINUTES.toString())
+
+ val context = JSONObject()
+ context.put("client", client)
+
+ innerTubeBody.put("context", context)
+ innerTubeBody.put("contentCheckOk", true)
+ innerTubeBody.put("racyCheckOk", true)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create android innerTubeBody" }, e)
+ }
+
+ return innerTubeBody
+ }
+
+ @JvmStatic
+ fun createPlaylistRequestBody(
+ videoId: String,
+ ): ByteArray {
+ val innerTubeBody = androidInnerTubeBody()
+
+ try {
+ innerTubeBody.put("params", "CAQ%3D")
+ // TODO: Implement an AlertDialog that allows changing the title of the playlist.
+ innerTubeBody.put("title", str("revanced_queue_manager_queue"))
+
+ val videoIds = JSONArray()
+ videoIds.put(0, videoId)
+ innerTubeBody.put("videoIds", videoIds)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create create/playlist innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun deletePlaylistRequestBody(
+ playlistId: String,
+ ): ByteArray {
+ val innerTubeBody = androidInnerTubeBody()
+
+ try {
+ innerTubeBody.put("playlistId", playlistId)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create delete/playlist innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun editPlaylistRequestBody(
+ videoId: String,
+ playlistId: String,
+ setVideoId: String?,
+ ): ByteArray {
+ val innerTubeBody = androidInnerTubeBody()
+
+ try {
+ innerTubeBody.put("playlistId", playlistId)
+
+ val actionsObject = JSONObject()
+ if (setVideoId != null && setVideoId.isNotEmpty()) {
+ actionsObject.put("action", "ACTION_REMOVE_VIDEO")
+ actionsObject.put("setVideoId", setVideoId)
+ } else {
+ actionsObject.put("action", "ACTION_ADD_VIDEO")
+ actionsObject.put("addedVideoId", videoId)
+ }
+
+ val actionsArray = JSONArray()
+ actionsArray.put(0, actionsObject)
+ innerTubeBody.put("actions", actionsArray)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create edit/playlist innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun getPlaylistsRequestBody(
+ playlistId: String,
+ ): ByteArray {
+ val innerTubeBody = androidInnerTubeBody()
+
+ try {
+ innerTubeBody.put("playlistId", playlistId)
+ innerTubeBody.put("excludeWatchLater", false)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create get/playlists innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun savePlaylistRequestBody(
+ playlistId: String,
+ libraryId: String,
+ ): ByteArray {
+ val innerTubeBody = androidInnerTubeBody()
+
+ try {
+ innerTubeBody.put("playlistId", playlistId)
+
+ val actionsObject = JSONObject()
+ actionsObject.put("action", "ACTION_ADD_PLAYLIST")
+ actionsObject.put("addedFullListId", libraryId)
+
+ val actionsArray = JSONArray()
+ actionsArray.put(0, actionsObject)
+ innerTubeBody.put("actions", actionsArray)
+ } catch (e: JSONException) {
+ Logger.printException({ "Failed to create save/playlist innerTubeBody" }, e)
+ }
+
+ return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
+ }
+
+ @JvmStatic
+ fun getInnerTubeResponseConnectionFromRoute(
+ route: CompiledRoute,
+ clientType: YouTubeAppClient.ClientType,
+ requestHeader: Map? = null,
+ dataSyncId: String? = null,
+ connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
+ readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
+ ) = getInnerTubeResponseConnectionFromRoute(
+ route = route,
+ userAgent = clientType.userAgent,
+ clientId = clientType.id.toString(),
+ clientVersion = clientType.clientVersion,
+ supportsCookies = clientType.supportsCookies,
+ requestHeader = requestHeader,
+ dataSyncId = dataSyncId,
+ connectTimeout = connectTimeout,
+ readTimeout = readTimeout,
+ )
+
+ @JvmStatic
+ fun getInnerTubeResponseConnectionFromRoute(
+ route: CompiledRoute,
+ clientType: YouTubeWebClient.ClientType,
+ requestHeader: Map? = null,
+ dataSyncId: String? = null,
+ connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
+ readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
+ ) = getInnerTubeResponseConnectionFromRoute(
+ route = route,
+ userAgent = clientType.userAgent,
+ clientId = clientType.id.toString(),
+ clientVersion = clientType.clientVersion,
+ requestHeader = requestHeader,
+ dataSyncId = dataSyncId,
+ connectTimeout = connectTimeout,
+ readTimeout = readTimeout,
+ )
+
+ @Throws(IOException::class)
+ fun getInnerTubeResponseConnectionFromRoute(
+ route: CompiledRoute,
+ userAgent: String,
+ clientId: String,
+ clientVersion: String,
+ supportsCookies: Boolean = true,
+ requestHeader: Map? = null,
+ dataSyncId: String? = null,
+ connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
+ readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
+ ): HttpURLConnection {
+ val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
+
+ connection.setRequestProperty("Content-Type", "application/json")
+ connection.setRequestProperty("User-Agent", userAgent)
+ connection.setRequestProperty("X-YouTube-Client-Name", clientId)
+ connection.setRequestProperty("X-YouTube-Client-Version", clientVersion)
+
+ connection.useCaches = false
+ connection.doOutput = true
+
+ connection.connectTimeout = connectTimeout
+ connection.readTimeout = readTimeout
+
+ if (requestHeader != null) {
+ for (key in REQUEST_HEADER_KEYS) {
+ var value = requestHeader[key]
+ if (value != null) {
+ if (key == AUTHORIZATION_HEADER) {
+ if (!supportsCookies) {
+ continue
+ }
+ }
+
+ connection.setRequestProperty(key, value)
+ }
+ }
+ }
+
+ // Used to identify brand accounts
+ if (dataSyncId != null && dataSyncId.isNotEmpty()) {
+ connection.setRequestProperty("X-Goog-PageId", dataSyncId)
+ }
+
+ return connection
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/requests/InnerTubeRoutes.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/requests/InnerTubeRoutes.kt
new file mode 100644
index 000000000..48ee66f8f
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/innertube/requests/InnerTubeRoutes.kt
@@ -0,0 +1,108 @@
+package app.revanced.extension.shared.innertube.requests
+
+import app.revanced.extension.shared.requests.Route
+import app.revanced.extension.shared.requests.Route.CompiledRoute
+
+object InnerTubeRoutes {
+
+ @JvmField
+ val CREATE_PLAYLIST = compileRoute(
+ endpoint = "playlist/create",
+ fields = "playlistId",
+ )
+
+ @JvmField
+ val DELETE_PLAYLIST = compileRoute(
+ endpoint = "playlist/delete",
+ )
+
+ @JvmField
+ val EDIT_PLAYLIST = compileRoute(
+ endpoint = "browse/edit_playlist",
+ fields = "status," + "playlistEditResults",
+ )
+
+ @JvmField
+ val GET_CATEGORY = compileRoute(
+ endpoint = "player",
+ fields = "microformat.playerMicroformatRenderer.category",
+ )
+
+ @JvmField
+ val GET_PLAYLISTS = compileRoute(
+ endpoint = "playlist/get_add_to_playlist",
+ fields = "contents.addToPlaylistRenderer.playlists.playlistAddToOptionRenderer",
+ )
+
+ @JvmField
+ val GET_SET_VIDEO_ID = compileRoute(
+ endpoint = "next",
+ fields = "contents.singleColumnWatchNextResults." +
+ "playlist.playlist.contents.playlistPanelVideoRenderer." +
+ "playlistSetVideoId",
+ )
+
+ @JvmField
+ val GET_PLAYLIST_PAGE = compileRoute(
+ endpoint = "next",
+ fields = "contents.singleColumnWatchNextResults.playlist.playlist",
+ )
+
+ @JvmField
+ val GET_STREAMING_DATA = compileRoute(
+ endpoint = "player",
+ fields = "streamingData",
+ alt = "proto",
+ prettier = true,
+ )
+
+ @JvmField
+ val GET_VIDEO_ACTION_BUTTON = compileRoute(
+ endpoint = "next",
+ fields = "contents.singleColumnWatchNextResults." +
+ "results.results.contents.slimVideoMetadataSectionRenderer." +
+ "contents.elementRenderer.newElement.type.componentType." +
+ "model.videoActionBarModel.buttons.buttonViewModel"
+ )
+
+ @JvmField
+ val GET_VIDEO_DETAILS = compileRoute(
+ endpoint = "player",
+ fields = "videoDetails.channelId," +
+ "videoDetails.isLiveContent," +
+ "videoDetails.isUpcoming"
+ )
+
+ private fun compileRoute(
+ endpoint: String,
+ fields: String? = null,
+ alt: String? = null,
+ prettier: Boolean = false,
+ ): CompiledRoute {
+ var query = Array(4) { "&" }
+ var i = 0
+ query[i] = "?"
+
+ val sb = StringBuilder(endpoint)
+ if (prettier == false) {
+ sb.append(query[i++])
+ sb.append("prettyPrint=false")
+ }
+ if (fields != null) {
+ sb.append(query[i++])
+ sb.append("fields=")
+ sb.append(fields)
+ }
+ if (alt != null) {
+ sb.append(query[i++])
+ sb.append("alt=")
+ sb.append(alt)
+ }
+
+ return Route(
+ Route.Method.POST,
+ sb.toString()
+ ).compile()
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/FullscreenAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/FullscreenAdsPatch.java
index e60a4ddf0..75711fe23 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/FullscreenAdsPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/FullscreenAdsPatch.java
@@ -37,7 +37,7 @@ public class FullscreenAdsPatch {
* Therefore, make sure that the dialog contains the ads at the beginning of the Method
*
* @param bytes proto buffer array
- * @param type dialog type (similar to {@link Enum#ordinal()})
+ * @param type dialog type (similar to {@link Enum#ordinal()})
*/
public static void checkDialog(byte[] bytes, int type) {
if (!HIDE_FULLSCREEN_ADS) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java
index 8282eadf5..74632f8c1 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java
@@ -11,4 +11,8 @@ public class PatchStatus {
// Replace this with true If the Spoof streaming data patch succeeds in YouTube.
return false;
}
+
+ public static boolean SpoofStreamingDataIOS() {
+ return false;
+ }
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java
index 20c987db3..a3625e834 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java
@@ -2,7 +2,6 @@ package app.revanced.extension.shared.patches;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
-
import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
import androidx.annotation.NonNull;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/WatchHistoryPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/WatchHistoryPatch.java
index 46e3d1cf9..4d37cde1e 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/WatchHistoryPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/WatchHistoryPatch.java
@@ -2,8 +2,8 @@ package app.revanced.extension.shared.patches;
import android.net.Uri;
-import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public final class WatchHistoryPatch {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofClientPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofClientPatch.java
index f26063a4f..a0faa7e06 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofClientPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofClientPatch.java
@@ -1,11 +1,11 @@
package app.revanced.extension.shared.patches.spoof;
-import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType;
-import app.revanced.extension.music.settings.Settings;
+import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
+import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("unused")
public class SpoofClientPatch extends BlockRequestPatch {
- private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get();
+ private static final ClientType CLIENT_TYPE = BaseSettings.SPOOF_CLIENT_TYPE.get();
/**
* Injection point.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java
index ef9d62ae7..40d4ceb8a 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java
@@ -10,21 +10,21 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
-import app.revanced.extension.shared.patches.client.YouTubeAppClient.ClientType;
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient.ClientType;
+import app.revanced.extension.shared.patches.PatchStatus;
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch {
- private static final String PO_TOKEN =
- BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get();
- private static final String VISITOR_DATA =
- BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get();
private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION =
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get();
+ private static final boolean SPOOF_STREAMING_DATA_TYPE_IOS =
+ PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get();
/**
* Any unreachable ip address. Used to intentionally fail requests.
@@ -69,17 +69,27 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* Skip response encryption in OnesiePlayerRequest.
*/
public static boolean skipResponseEncryption(boolean original) {
- if (SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
- return false;
+ if (!SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
+ return original;
}
+ return false;
+ }
- return original;
+ /**
+ * Injection point.
+ * Turns off a feature flag that interferes with video playback.
+ */
+ public static boolean usePlaybackStartFeatureFlag(boolean original) {
+ if (!SPOOF_STREAMING_DATA) {
+ return original;
+ }
+ return false;
}
/**
* Injection point.
*/
- public static void fetchStreams(String url, Map requestHeaders) {
+ public static void fetchStreams(String url, Map requestHeader) {
if (SPOOF_STREAMING_DATA) {
String id = Utils.getVideoIdFromRequest(url);
if (id == null) {
@@ -89,7 +99,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return;
}
- StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN);
+ StreamingDataRequest.fetchRequest(id, requestHeader);
}
}
@@ -210,6 +220,18 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return videoFormat;
}
+ public static String[] getEntries() {
+ return SPOOF_STREAMING_DATA_TYPE_IOS
+ ? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entries")
+ : ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entries");
+ }
+
+ public static String[] getEntryValues() {
+ return SPOOF_STREAMING_DATA_TYPE_IOS
+ ? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entry_values")
+ : ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entry_values");
+ }
+
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt
deleted file mode 100644
index bd99fc453..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt
+++ /dev/null
@@ -1,223 +0,0 @@
-package app.revanced.extension.shared.patches.spoof.requests
-
-import app.revanced.extension.shared.patches.client.YouTubeAppClient
-import app.revanced.extension.shared.patches.client.YouTubeWebClient
-import app.revanced.extension.shared.requests.Requester
-import app.revanced.extension.shared.requests.Route
-import app.revanced.extension.shared.requests.Route.CompiledRoute
-import app.revanced.extension.shared.settings.BaseSettings
-import app.revanced.extension.shared.utils.Logger
-import app.revanced.extension.shared.utils.Utils
-import org.apache.commons.lang3.StringUtils
-import org.json.JSONException
-import org.json.JSONObject
-import java.io.IOException
-import java.net.HttpURLConnection
-import java.nio.charset.StandardCharsets
-import java.util.Date
-import java.util.Locale
-import java.util.TimeZone
-
-@Suppress("deprecation")
-object PlayerRoutes {
- @JvmField
- val GET_CATEGORY: CompiledRoute = Route(
- Route.Method.POST,
- "player" +
- "?prettyPrint=false" +
- "&fields=microformat.playerMicroformatRenderer.category"
- ).compile()
-
- @JvmField
- val GET_PLAYLIST_PAGE: CompiledRoute = Route(
- Route.Method.POST,
- "next" +
- "?prettyPrint=false" +
- "&fields=contents.singleColumnWatchNextResults.playlist.playlist"
- ).compile()
-
- @JvmField
- val GET_STREAMING_DATA: CompiledRoute = Route(
- Route.Method.POST,
- "player" +
- "?fields=streamingData" +
- "&alt=proto"
- ).compile()
-
- @JvmField
- val GET_VIDEO_ACTION_BUTTON: CompiledRoute = Route(
- Route.Method.POST,
- "next" +
- "?prettyPrint=false" +
- "&fields=contents.singleColumnWatchNextResults." +
- "results.results.contents.slimVideoMetadataSectionRenderer." +
- "contents.elementRenderer.newElement.type.componentType." +
- "model.videoActionBarModel.buttons.buttonViewModel"
- ).compile()
-
- @JvmField
- val GET_VIDEO_DETAILS: CompiledRoute = Route(
- Route.Method.POST,
- "player" +
- "?prettyPrint=false" +
- "&fields=videoDetails.channelId," +
- "videoDetails.isLiveContent," +
- "videoDetails.isUpcoming"
- ).compile()
-
- private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"
-
- /**
- * TCP connection and HTTP read timeout
- */
- private const val CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000 // 10 Seconds.
-
- private val LOCALE: Locale = Utils.getContext().resources
- .configuration.locale
- private val LOCALE_COUNTRY: String = LOCALE.country
- private val LOCALE_LANGUAGE: String = LOCALE.language
- private val TIME_ZONE: TimeZone = TimeZone.getDefault()
- private val TIME_ZONE_ID: String = TIME_ZONE.id
- private val UTC_OFFSET_MINUTES: Int = TIME_ZONE.getOffset(Date().time) / 60000
-
- @JvmStatic
- fun createApplicationRequestBody(
- clientType: YouTubeAppClient.ClientType,
- videoId: String,
- playlistId: String? = null,
- botGuardPoToken: String = "",
- visitorId: String = "",
- setLocale: Boolean = false,
- language: String = BaseSettings.SPOOF_STREAMING_DATA_LANGUAGE.get().language,
- ): ByteArray {
- val innerTubeBody = JSONObject()
-
- try {
- val client = JSONObject()
- client.put("deviceMake", clientType.deviceMake)
- client.put("deviceModel", clientType.deviceModel)
- client.put("clientName", clientType.clientName)
- client.put("clientVersion", clientType.clientVersion)
- client.put("osName", clientType.osName)
- client.put("osVersion", clientType.osVersion)
- if (clientType.androidSdkVersion != null) {
- client.put("androidSdkVersion", clientType.androidSdkVersion)
- if (clientType.gmscoreVersionCode != null) {
- client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
- }
- }
- client.put(
- "hl",
- if (setLocale) {
- language
- } else {
- LOCALE_LANGUAGE
- }
- )
- client.put("gl", LOCALE_COUNTRY)
- client.put("timeZone", TIME_ZONE_ID)
- client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES")
-
- val context = JSONObject()
- context.put("client", client)
-
- innerTubeBody.put("context", context)
- innerTubeBody.put("contentCheckOk", true)
- innerTubeBody.put("racyCheckOk", true)
- innerTubeBody.put("videoId", videoId)
-
- if (playlistId != null) {
- innerTubeBody.put("playlistId", playlistId)
- }
-
- if (!StringUtils.isAnyEmpty(botGuardPoToken, visitorId)) {
- val serviceIntegrityDimensions = JSONObject()
- serviceIntegrityDimensions.put("poToken", botGuardPoToken)
- innerTubeBody.put("serviceIntegrityDimensions", serviceIntegrityDimensions)
- }
- } catch (e: JSONException) {
- Logger.printException({ "Failed to create application innerTubeBody" }, e)
- }
-
- return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
- }
-
- @JvmStatic
- fun createWebInnertubeBody(
- clientType: YouTubeWebClient.ClientType,
- videoId: String
- ): ByteArray {
- val innerTubeBody = JSONObject()
-
- try {
- val client = JSONObject()
- client.put("clientName", clientType.clientName)
- client.put("clientVersion", clientType.clientVersion)
- val context = JSONObject()
- context.put("client", client)
-
- val lockedSafetyMode = JSONObject()
- lockedSafetyMode.put("lockedSafetyMode", false)
- val user = JSONObject()
- user.put("user", lockedSafetyMode)
-
- innerTubeBody.put("context", context)
- innerTubeBody.put("contentCheckOk", true)
- innerTubeBody.put("racyCheckOk", true)
- innerTubeBody.put("videoId", videoId)
- } catch (e: JSONException) {
- Logger.printException({ "Failed to create web innerTubeBody" }, e)
- }
-
- return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
- }
-
- @JvmStatic
- fun getPlayerResponseConnectionFromRoute(
- route: CompiledRoute,
- clientType: YouTubeAppClient.ClientType
- ): HttpURLConnection {
- return getPlayerResponseConnectionFromRoute(
- route,
- clientType.userAgent,
- clientType.id.toString(),
- clientType.clientVersion
- )
- }
-
- @JvmStatic
- fun getPlayerResponseConnectionFromRoute(
- route: CompiledRoute,
- clientType: YouTubeWebClient.ClientType
- ): HttpURLConnection {
- return getPlayerResponseConnectionFromRoute(
- route,
- clientType.userAgent,
- clientType.id.toString(),
- clientType.clientVersion,
- )
- }
-
- @Throws(IOException::class)
- fun getPlayerResponseConnectionFromRoute(
- route: CompiledRoute,
- userAgent: String,
- clientId: String,
- clientVersion: String
- ): HttpURLConnection {
- val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
-
- connection.setRequestProperty("Content-Type", "application/json")
- connection.setRequestProperty("User-Agent", userAgent)
- connection.setRequestProperty("X-YouTube-Client-Name", clientId)
- connection.setRequestProperty("X-YouTube-Client-Version", clientVersion)
-
- connection.useCaches = false
- connection.doOutput = true
-
- connection.connectTimeout = CONNECTION_TIMEOUT_MILLISECONDS
- connection.readTimeout = CONNECTION_TIMEOUT_MILLISECONDS
- return connection
- }
-
-}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt
index 425ccfcb3..cd8f717cb 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.kt
@@ -1,14 +1,14 @@
package app.revanced.extension.shared.patches.spoof.requests
import androidx.annotation.GuardedBy
-import app.revanced.extension.shared.patches.client.YouTubeAppClient
-import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA
-import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody
-import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.StringRef.str
import app.revanced.extension.shared.utils.Utils
-import org.apache.commons.lang3.StringUtils
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
@@ -32,21 +32,19 @@ import java.util.concurrent.TimeoutException
* did use its own client streams.
*/
class StreamingDataRequest private constructor(
- videoId: String, playerHeaders: Map,
- visitorId: String, botGuardPoToken: String
+ videoId: String,
+ requestHeader: Map,
) {
private val videoId: String
private val future: Future
init {
- Objects.requireNonNull(playerHeaders)
+ Objects.requireNonNull(requestHeader)
this.videoId = videoId
this.future = Utils.submitOnBackgroundThread {
fetch(
videoId,
- playerHeaders,
- visitorId,
- botGuardPoToken
+ requestHeader,
)
}
}
@@ -86,33 +84,16 @@ class StreamingDataRequest private constructor(
companion object {
private const val AUTHORIZATION_HEADER = "Authorization"
- private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id"
- private val REQUEST_HEADER_KEYS = arrayOf(
- AUTHORIZATION_HEADER, // Available only to logged-in users.
- "X-GOOG-API-FORMAT-VERSION",
- VISITOR_ID_HEADER
- )
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
-
private val CLIENT_ORDER_TO_USE: Array =
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
-
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
- private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null
-
-
- /**
- * TCP connection and HTTP read timeout.
- */
- private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
-
- /**
- * Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
- */
- private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+ private var lastSpoofedClientFriendlyName: String? = null
@GuardedBy("itself")
val cache: MutableMap = Collections.synchronizedMap(
@@ -126,22 +107,24 @@ class StreamingDataRequest private constructor(
@JvmStatic
val lastSpoofedClientName: String
- get() = lastSpoofedClientType
- ?.friendlyName
- ?: "Unknown"
+ get() {
+ return if (lastSpoofedClientFriendlyName != null) {
+ lastSpoofedClientFriendlyName!!
+ } else {
+ "Unknown"
+ }
+ }
@JvmStatic
fun fetchRequest(
- videoId: String, fetchHeaders: Map,
- visitorId: String, botGuardPoToken: String
+ videoId: String,
+ fetchHeaders: Map,
) {
// Always fetch, even if there is an existing request for the same video.
cache[videoId] =
StreamingDataRequest(
videoId,
- fetchHeaders,
- visitorId,
- botGuardPoToken
+ fetchHeaders
)
}
@@ -150,71 +133,40 @@ class StreamingDataRequest private constructor(
return cache[videoId]
}
- private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ private fun handleConnectionError(
+ toastMessage: String,
+ ex: Exception?,
+ showToast: Boolean = false,
+ ) {
+ if (showToast) Utils.showToastShort(toastMessage)
Logger.printInfo({ toastMessage }, ex)
}
private fun send(
clientType: YouTubeAppClient.ClientType,
videoId: String,
- playerHeaders: Map,
- visitorId: String,
- botGuardPoToken: String
+ requestHeader: Map,
): HttpURLConnection? {
Objects.requireNonNull(clientType)
Objects.requireNonNull(videoId)
- Objects.requireNonNull(playerHeaders)
+ Objects.requireNonNull(requestHeader)
val startTime = System.currentTimeMillis()
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
try {
val connection =
- getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType)
- connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
- connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
-
- val usePoToken =
- clientType.requirePoToken && !StringUtils.isAnyEmpty(botGuardPoToken, visitorId)
-
- for (key in REQUEST_HEADER_KEYS) {
- var value = playerHeaders[key]
- if (value != null) {
- if (key == AUTHORIZATION_HEADER) {
- if (!clientType.supportsCookies) {
- Logger.printDebug { "Not including request header: $key" }
- continue
- }
- }
- if (key == VISITOR_ID_HEADER && usePoToken) {
- val originalVisitorId: String = value
- Logger.printDebug { "Original visitor id:\n$originalVisitorId" }
- Logger.printDebug { "Replaced visitor id:\n$visitorId" }
- value = visitorId
- }
-
- connection.setRequestProperty(key, value)
- }
- }
-
- val requestBody: ByteArray
- if (usePoToken) {
- requestBody = createApplicationRequestBody(
- clientType = clientType,
- videoId = videoId,
- botGuardPoToken = botGuardPoToken,
- visitorId = visitorId,
- setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
+ getInnerTubeResponseConnectionFromRoute(
+ GET_STREAMING_DATA,
+ clientType,
+ requestHeader
)
- Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
- } else {
- requestBody =
- createApplicationRequestBody(
- clientType = clientType,
- videoId = videoId,
- setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
- )
- }
+
+ val requestBody = createApplicationRequestBody(
+ clientType = clientType,
+ videoId = videoId,
+ setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
+ )
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@@ -243,15 +195,15 @@ class StreamingDataRequest private constructor(
}
private fun fetch(
- videoId: String, playerHeaders: Map,
- visitorId: String, botGuardPoToken: String
+ videoId: String,
+ requestHeader: Map,
): ByteBuffer? {
- lastSpoofedClientType = null
+ lastSpoofedClientFriendlyName = null
// Retry with different client if empty response body is received.
for (clientType in CLIENT_ORDER_TO_USE) {
if (clientType.requireAuth &&
- playerHeaders[AUTHORIZATION_HEADER] == null
+ requestHeader[AUTHORIZATION_HEADER] == null
) {
Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
continue
@@ -259,9 +211,7 @@ class StreamingDataRequest private constructor(
send(
clientType,
videoId,
- playerHeaders,
- visitorId,
- botGuardPoToken
+ requestHeader,
)?.let { connection ->
try {
// gzip encoding doesn't response with content length (-1),
@@ -271,14 +221,14 @@ class StreamingDataRequest private constructor(
} else {
BufferedInputStream(connection.inputStream).use { inputStream ->
ByteArrayOutputStream().use { stream ->
- val buffer = ByteArray(2048)
+ val buffer = ByteArray(4096)
var bytesRead: Int
while ((inputStream.read(buffer)
.also { bytesRead = it }) >= 0
) {
stream.write(buffer, 0, bytesRead)
}
- lastSpoofedClientType = clientType
+ lastSpoofedClientFriendlyName = clientType.friendlyName
return ByteBuffer.wrap(stream.toByteArray())
}
}
@@ -289,7 +239,12 @@ class StreamingDataRequest private constructor(
}
}
- handleConnectionError("Could not fetch any client streams", null)
+ handleConnectionError(str("revanced_spoof_streaming_data_failed_forbidden"), null, true)
+ handleConnectionError(
+ str("revanced_spoof_streaming_data_failed_forbidden_suggestion"),
+ null,
+ true
+ )
return null
}
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
index 004766e99..992c18370 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java
@@ -3,10 +3,10 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient;
+import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType;
-import app.revanced.extension.shared.patches.client.MusicAppClient;
-import app.revanced.extension.shared.patches.client.YouTubeAppClient;
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability;
/**
@@ -31,7 +31,7 @@ public class BaseSettings {
* Some patches are in a shared path, so they are declared here.
*/
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
- public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true);
+ public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", YouTubeMusicAppClient.ClientType.IOS_MUSIC_6_21, true);
/**
* These settings are used by YouTube.
@@ -43,11 +43,9 @@ public class BaseSettings {
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = new BooleanSetting("revanced_spoof_streaming_data_skip_response_encryption", TRUE, true);
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
+ public static final BooleanSetting SPOOF_STREAMING_DATA_TYPE_IOS = new BooleanSetting("revanced_spoof_streaming_data_type_ios", FALSE, true, "revanced_spoof_streaming_data_type_ios_user_dialog_message");
// Client type must be last spoof setting due to cyclic references.
- public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_UNPLUGGED, true);
-
- public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
- public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
+ public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true);
/**
* These settings are used by YouTube and YouTube Music.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
index 168ef7375..9834cc5f0 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java
@@ -22,16 +22,16 @@ import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import java.util.Objects;
-
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"})
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
+
/**
* Indicates that if a preference changes,
* to apply the change from the Setting to the UI component.
@@ -39,7 +39,11 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
public static boolean settingImportInProgress;
/**
- * Confirm and restart dialog button text and title.
+ * Prevents recursive calls during preference <-> UI syncing from showing extra dialogs.
+ */
+ private static boolean updatingPreference;
+
+ /**
* Set by subclasses if Strings cannot be added as a resource.
*/
@Nullable
@@ -52,7 +56,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
- Setting> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
+ if (updatingPreference) {
+ Logger.printDebug(() -> "Ignoring preference change as sync is in progress");
+ return;
+ }
+ if (str == null) {
+ return;
+ }
+ Setting> setting = Setting.getSettingFromPath(str);
if (setting == null) {
return;
}
@@ -73,10 +84,13 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
}
}
+ updatingPreference = true;
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
+ // Updating here can can cause a recursive call back into this same method.
updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different.
updateUIAvailability();
+ updatingPreference = false;
} catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
@@ -103,36 +117,39 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
Utils.verifyOnMainThread();
final var context = getActivity();
- showingUserDialogMessage = true;
- assert setting.userDialogMessage != null;
- new AlertDialog.Builder(context)
- .setTitle(android.R.string.dialog_alert_title)
- .setMessage(setting.userDialogMessage.toString())
- .setPositiveButton(android.R.string.ok, (dialog, id) -> {
- // User confirmed, save to the Setting.
- updatePreference(pref, setting, true, false);
+ final StringRef userDialogMessage = setting.userDialogMessage;
+ if (context != null && userDialogMessage != null) {
+ showingUserDialogMessage = true;
- // Update availability of other preferences that may be changed.
- updateUIAvailability();
+ new AlertDialog.Builder(context)
+ .setTitle(android.R.string.dialog_alert_title)
+ .setMessage(userDialogMessage.toString())
+ .setPositiveButton(android.R.string.ok, (dialog, id) -> {
+ // User confirmed, save to the Setting.
+ updatePreference(pref, setting, true, false);
- if (setting.rebootApp) {
- showRestartDialog(context);
- }
- })
- .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
- // Restore whatever the setting was before the change.
- updatePreference(pref, setting, true, true);
- })
- .setOnDismissListener(dialog -> showingUserDialogMessage = false)
- .setCancelable(false)
- .show();
+ // Update availability of other preferences that may be changed.
+ updateUIAvailability();
+
+ if (setting.rebootApp) {
+ showRestartDialog(context);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
+ // Restore whatever the setting was before the change.
+ updatePreference(pref, setting, true, true);
+ })
+ .setOnDismissListener(dialog -> showingUserDialogMessage = false)
+ .setCancelable(false)
+ .show();
+ }
}
/**
* Updates all Preferences values and their availability using the current values in {@link Setting}.
*/
protected void updateUIToSettingValues() {
- updatePreferenceScreen(getPreferenceScreen(), true,true);
+ updatePreferenceScreen(getPreferenceScreen(), true, true);
}
/**
@@ -146,14 +163,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* @return If the preference is currently set to the default value of the Setting.
*/
protected boolean prefIsSetToDefault(Preference pref, Setting> setting) {
+ Object defaultValue = setting.defaultValue;
if (pref instanceof SwitchPreference switchPref) {
- return switchPref.isChecked() == (Boolean) setting.defaultValue;
+ return switchPref.isChecked() == (Boolean) defaultValue;
}
+ String defaultValueString = defaultValue.toString();
if (pref instanceof EditTextPreference editPreference) {
- return editPreference.getText().equals(setting.defaultValue.toString());
+ return editPreference.getText().equals(defaultValueString);
}
if (pref instanceof ListPreference listPref) {
- return listPref.getValue().equals(setting.defaultValue.toString());
+ return listPref.getValue().equals(defaultValueString);
}
throw new IllegalStateException("Must override method to handle "
@@ -227,7 +246,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/**
* Updates a UI Preference with the {@link Setting} that backs it.
*
- * @param syncSetting If the UI should be synced {@link Setting} <-> Preference
+ * @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/
@@ -258,18 +277,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
listPreference.setSummary(objectStringValue);
}
- public static void showRestartDialog(@NonNull final Context context) {
+ public static void showRestartDialog(@NonNull Context context) {
if (restartDialogMessage == null) {
restartDialogMessage = str("revanced_extended_restart_message");
}
+
showRestartDialog(context, restartDialogMessage);
}
- public static void showRestartDialog(@NonNull final Context context, String message) {
+ public static void showRestartDialog(@NonNull Context context, String message) {
showRestartDialog(context, message, 0);
}
- public static void showRestartDialog(@NonNull final Context context, String message, long delay) {
+ public static void showRestartDialog(@NonNull Context context, String message, long delay) {
Utils.verifyOnMainThread();
new AlertDialog.Builder(context)
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
index 43305c23c..a67639cc0 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java
@@ -10,6 +10,8 @@ import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
+import androidx.annotation.Nullable;
+
import java.util.Objects;
import app.revanced.extension.shared.settings.Setting;
@@ -19,6 +21,12 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"})
public class ResettableEditTextPreference extends EditTextPreference {
+ /**
+ * Setting to reset.
+ */
+ @Nullable
+ private Setting> setting;
+
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -35,6 +43,10 @@ public class ResettableEditTextPreference extends EditTextPreference {
super(context);
}
+ public void setSetting(@Nullable Setting> setting) {
+ this.setting = setting;
+ }
+
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder);
@@ -44,7 +56,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
if (title != null) {
builder.setTitle(getTitle());
}
- final Setting> setting = Setting.getSettingFromPath(getKey());
+ if (setting == null) {
+ String key = getKey();
+ if (key != null) {
+ setting = Setting.getSettingFromPath(key);
+ }
+ }
if (setting != null) {
builder.setNeutralButton(str("revanced_extended_settings_reset"), null);
}
@@ -65,8 +82,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
}
button.setOnClickListener(v -> {
try {
- Setting> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey()));
- String defaultStringValue = setting.defaultValue.toString();
+ String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
EditText editText = getEditText();
editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java
index dcfd46864..c2f147dcf 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java
@@ -29,10 +29,20 @@ public class PackageUtils extends Utils {
}
}
+ @Nullable
+ public static Integer getTargetSDKVersion(@NonNull String packageName) {
+ ApplicationInfo applicationInfo = getApplicationInfo(packageName);
+ if (applicationInfo != null) {
+ return applicationInfo.targetSdkVersion;
+ }
+
+ return null;
+ }
+
public static boolean isPackageEnabled(@NonNull String packageName) {
- try {
- return getContext().getPackageManager().getApplicationInfo(packageName, 0).enabled;
- } catch (PackageManager.NameNotFoundException ignored) {
+ ApplicationInfo applicationInfo = getApplicationInfo(packageName);
+ if (applicationInfo != null) {
+ return applicationInfo.enabled;
}
return false;
@@ -47,6 +57,16 @@ public class PackageUtils extends Utils {
}
// utils
+ @Nullable
+ private static ApplicationInfo getApplicationInfo(@NonNull String packageName) {
+ try {
+ return getContext().getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Logger.printException(() -> "Failed to get application Info!" + e);
+ }
+ return null;
+ }
+
@Nullable
private static PackageInfo getPackageInfo() {
try {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java
index 268a9e0be..956f60039 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java
@@ -60,6 +60,7 @@ public class Utils {
private static WeakReference activityRef = new WeakReference<>(null);
@SuppressLint("StaticFieldLeak")
private static volatile Context context;
+ private static Locale contextLocale;
protected Utils() {
} // utility class
@@ -308,34 +309,51 @@ public class Utils {
* @return Context with locale applied.
*/
public static Context getLocalizedContext(Context mContext) {
- Activity mActivity = activityRef.get();
- if (mActivity == null) {
- return mContext;
- }
- if (mContext == null) {
- return null;
+ try {
+ Activity mActivity = activityRef.get();
+ if (mActivity != null && mContext != null) {
+ AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
+
+ // Locale of Application.
+ Locale applicationLocale = language == AppLanguage.DEFAULT
+ ? mActivity.getResources().getConfiguration().locale
+ : language.getLocale();
+
+ // Locale of Context.
+ Locale contextLocale = mContext.getResources().getConfiguration().locale;
+
+ // If they are different, overrides the Locale of the Context and resource.
+ if (applicationLocale != contextLocale) {
+ Utils.contextLocale = contextLocale;
+
+ // If they are different, overrides the Locale of the Context and resource.
+ Locale.setDefault(applicationLocale);
+ Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
+ configuration.setLocale(applicationLocale);
+ return mContext.createConfigurationContext(configuration);
+ }
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "getLocalizedContext failed", ex);
}
- AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
+ return mContext;
+ }
- // Locale of Application.
- Locale applicationLocale = language == AppLanguage.DEFAULT
- ? mActivity.getResources().getConfiguration().locale
- : language.getLocale();
-
- // Locale of Context.
- Locale contextLocale = mContext.getResources().getConfiguration().locale;
-
- // If they are identical, no need to override them.
- if (applicationLocale == contextLocale) {
- return mContext;
+ public static void resetLocalizedContext() {
+ try {
+ if (contextLocale != null) {
+ Locale.setDefault(contextLocale);
+ Context mContext = getContext();
+ if (mContext != null) {
+ Configuration config = mContext.getResources().getConfiguration();
+ config.setLocale(contextLocale);
+ setContext(mContext.createConfigurationContext(config));
+ }
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "resetLocalizedContext failed", ex);
}
-
- // If they are different, overrides the Locale of the Context and resource.
- Locale.setDefault(applicationLocale);
- Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
- configuration.setLocale(applicationLocale);
- return mContext.createConfigurationContext(configuration);
}
public static void setActivity(Activity mainActivity) {
@@ -353,14 +371,6 @@ public class Utils {
// Must initially set context to check the app language.
context = appContext;
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
-
- AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
- if (language != AppLanguage.DEFAULT) {
- // Create a new context with the desired language.
- Configuration config = appContext.getResources().getConfiguration();
- config.setLocale(language.getLocale());
- context = appContext.createConfigurationContext(config);
- }
}
public static void setClipboard(@NonNull String text) {
@@ -538,14 +548,6 @@ public class Utils {
return Build.VERSION.SDK_INT >= sdk;
}
- public static int dpToPx(float dp) {
- if (context == null) {
- return (int) dp;
- } else {
- return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
- }
- }
-
public static int dpToPx(int dp) {
if (context == null) {
return dp;
@@ -608,10 +610,10 @@ public class Utils {
*
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
- *
+ *
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
- *
+ *
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java
index 680846c13..eace3d445 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java
@@ -6,7 +6,6 @@ import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
-import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.kt
index 5625e07d3..9284d8a62 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.kt
@@ -6,7 +6,6 @@ import app.revanced.extension.shared.settings.Setting.Availability
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.youtube.settings.Settings
import org.apache.commons.lang3.StringUtils
-import kotlin.Boolean
@Suppress("unused")
object ChangeStartPagePatch {
@@ -44,7 +43,7 @@ object ChangeStartPagePatch {
}
appLaunched = true
- Logger.printDebug{ "Changing browseId to $browseId" }
+ Logger.printDebug { "Changing browseId to $browseId" }
return browseId
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/DownloadActionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/DownloadActionsPatch.java
index 0c1607561..d3fd2c399 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/DownloadActionsPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/DownloadActionsPatch.java
@@ -1,18 +1,34 @@
package app.revanced.extension.youtube.patches.general;
-import app.revanced.extension.shared.settings.BooleanSetting;
+import static app.revanced.extension.youtube.utils.VideoUtils.launchPlaylistExternalDownloader;
+import static app.revanced.extension.youtube.utils.VideoUtils.launchVideoExternalDownloader;
+
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Map;
+
import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.youtube.patches.utils.PlaylistPatch;
import app.revanced.extension.youtube.settings.Settings;
-import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
-public final class DownloadActionsPatch extends VideoUtils {
+public final class DownloadActionsPatch {
- private static final BooleanSetting overrideVideoDownloadButton =
- Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON;
+ private static final boolean OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON =
+ Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON.get();
- private static final BooleanSetting overridePlaylistDownloadButton =
- Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON;
+ private static final boolean OVERRIDE_VIDEO_DOWNLOAD_BUTTON =
+ Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON.get();
+
+ private static final boolean OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER =
+ OVERRIDE_VIDEO_DOWNLOAD_BUTTON && Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER.get();
+
+ private static final String ELEMENTS_SENDER_VIEW =
+ "com.google.android.libraries.youtube.rendering.elements.sender_view";
/**
* Injection point.
@@ -23,17 +39,21 @@ public final class DownloadActionsPatch extends VideoUtils {
*
* Appears to always be called from the main thread.
*/
- public static boolean inAppVideoDownloadButtonOnClick(String videoId) {
+ public static boolean inAppVideoDownloadButtonOnClick(@Nullable Map map, Object offlineVideoEndpointOuterClass,
+ @Nullable String videoId) {
try {
- if (!overrideVideoDownloadButton.get()) {
- return false;
- }
- if (videoId == null || videoId.isEmpty()) {
- return false;
- }
- launchVideoExternalDownloader(videoId);
+ if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(videoId)) {
+ if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER) {
+ if (map != null && map.get(ELEMENTS_SENDER_VIEW) instanceof View view) {
+ PlaylistPatch.setContext(view.getContext());
+ }
+ PlaylistPatch.prepareDialogBuilder(videoId);
+ } else {
+ launchVideoExternalDownloader(videoId);
+ }
- return true;
+ return true;
+ }
} catch (Exception ex) {
Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex);
}
@@ -49,15 +69,10 @@ public final class DownloadActionsPatch extends VideoUtils {
*/
public static String inAppPlaylistDownloadButtonOnClick(String playlistId) {
try {
- if (!overridePlaylistDownloadButton.get()) {
- return playlistId;
+ if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
+ launchPlaylistExternalDownloader(playlistId);
+ return "";
}
- if (playlistId == null || playlistId.isEmpty()) {
- return playlistId;
- }
- launchPlaylistExternalDownloader(playlistId);
-
- return "";
} catch (Exception ex) {
Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex);
}
@@ -73,15 +88,10 @@ public final class DownloadActionsPatch extends VideoUtils {
*/
public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) {
try {
- if (!overridePlaylistDownloadButton.get()) {
- return false;
+ if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
+ launchPlaylistExternalDownloader(playlistId);
+ return true;
}
- if (playlistId == null || playlistId.isEmpty()) {
- return false;
- }
- launchPlaylistExternalDownloader(playlistId);
-
- return true;
} catch (Exception ex) {
Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex);
}
@@ -92,7 +102,7 @@ public final class DownloadActionsPatch extends VideoUtils {
* Injection point.
*/
public static boolean overridePlaylistDownloadButtonVisibility() {
- return overridePlaylistDownloadButton.get();
+ return OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON;
}
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
index b5652471b..2c3c87989 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
@@ -40,6 +40,7 @@ import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
+import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@@ -105,6 +106,34 @@ public class GeneralPatch {
// endregion
+ // region [Disable layout updates] patch
+
+ private static final String[] REQUEST_HEADER_KEYS = {
+ "X-Youtube-Cold-Config-Data",
+ "X-Youtube-Cold-Hash-Data",
+ "X-Youtube-Hot-Config-Data",
+ "X-Youtube-Hot-Hash-Data"
+ };
+
+ private static final boolean DISABLE_LAYOUT_UPDATES =
+ Settings.DISABLE_LAYOUT_UPDATES.get();
+
+ /**
+ * @param key Keys to be added to the header of CronetBuilder.
+ * @param value Values to be added to the header of CronetBuilder.
+ * @return Empty value if setting is enabled.
+ */
+ public static String disableLayoutUpdates(String key, String value) {
+ if (DISABLE_LAYOUT_UPDATES && StringUtils.equalsAny(key, REQUEST_HEADER_KEYS)) {
+ Logger.printDebug(() -> "Blocking: " + key);
+ return "";
+ }
+
+ return value;
+ }
+
+ // endregion
+
// region [Disable splash animation] patch
public static boolean disableSplashAnimation(boolean original) {
@@ -234,6 +263,17 @@ public class GeneralPatch {
}
}
+ public static int getLibraryDrawableId(int original) {
+ if (ExtendedUtils.IS_19_26_OR_GREATER &&
+ !ExtendedUtils.isSpoofingToLessThan("19.27.00")) {
+ int libraryCairoId = ResourceUtils.getDrawableIdentifier("yt_outline_library_cairo_black_24");
+ if (libraryCairoId != 0) {
+ return libraryCairoId;
+ }
+ }
+ return original;
+ }
+
public static boolean switchCreateWithNotificationButton(boolean original) {
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original;
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java
index 489ac1cae..e3177dd60 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/OpenChannelOfLiveAvatarPatch.java
@@ -43,8 +43,8 @@ public final class OpenChannelOfLiveAvatarPatch {
/**
* Injection point.
*
- * @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
- * @param newlyLoadedVideoId id of the current video
+ * @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
+ * @param newlyLoadedVideoId id of the current video
*/
public static void fetchChannelId(@NonNull Map playbackStartDescriptorMap, String newlyLoadedVideoId) {
try {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt
index 1db71775a..64c3897aa 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/requests/VideoDetailsRequest.kt
@@ -2,8 +2,10 @@ package app.revanced.extension.youtube.patches.general.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
-import app.revanced.extension.shared.patches.client.YouTubeWebClient
-import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
+import app.revanced.extension.shared.innertube.client.YouTubeWebClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_DETAILS
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@@ -86,12 +88,11 @@ class VideoDetailsRequest private constructor(
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
try {
- val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
- PlayerRoutes.GET_VIDEO_DETAILS,
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ GET_VIDEO_DETAILS,
clientType
)
- val requestBody =
- PlayerRoutes.createWebInnertubeBody(clientType, videoId)
+ val requestBody = createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java
index e6172078d..0541d4f1b 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java
@@ -15,7 +15,16 @@ public class BackgroundPlaybackPatch {
*/
public static boolean isBackgroundPlaybackAllowed(boolean original) {
if (original) return true;
- return ShortsPlayerState.getCurrent().isClosed();
+ return ShortsPlayerState.getCurrent().isClosed() &&
+ // 1. Shorts background playback is enabled.
+ // 2. Autoplay in feed is turned on.
+ // 3. Play Shorts from feed.
+ // 4. Media controls appear in status bar.
+ // (For unpatched YouTube with Premium accounts, media controls do not appear in the status bar)
+ //
+ // This is just a visual bug and does not affect Shorts background play in any way.
+ // To fix this, just check PlayerType.
+ PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL;
}
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java
index e6a572af6..450ee50f9 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java
@@ -6,7 +6,9 @@ import android.view.ViewGroup;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.youtube.patches.utils.PlaylistPatch;
import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
@@ -19,7 +21,14 @@ public class ExternalDownload extends BottomControlButton {
bottomControlsViewGroup,
"external_download_button",
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER,
- view -> VideoUtils.launchVideoExternalDownloader(),
+ view -> {
+ if (Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER.get()) {
+ PlaylistPatch.setContext(view.getContext());
+ PlaylistPatch.prepareDialogBuilder(VideoInformation.getVideoId());
+ } else {
+ VideoUtils.launchVideoExternalDownloader();
+ }
+ },
null
);
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java
index f3207ae8f..279c62c67 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/ActionButtonsPatch.java
@@ -1,5 +1,7 @@
package app.revanced.extension.youtube.patches.player;
+import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.REMIX;
+
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
@@ -8,8 +10,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
-import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.*;
-
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@@ -106,8 +106,8 @@ public class ActionButtonsPatch {
/**
* Injection point.
*
- * @param list Type list of litho components
- * @param identifier Identifier of litho components
+ * @param list Type list of litho components
+ * @param identifier Identifier of litho components
*/
public static List hideActionButtonByIndex(@Nullable List list, @Nullable String identifier) {
try {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java
index 00d74d8c5..6ee95843d 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java
@@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.IntegerSetting;
-import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
@@ -34,7 +33,6 @@ import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.EngagementPanel;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.RootView;
-import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils;
@@ -441,7 +439,7 @@ public class PlayerPatch {
if (isLiveChatOrPlaylistPanel) {
return true;
}
- return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed();
+ return isAutoPopupPanel && !RootView.isShortsActive();
}
/**
@@ -471,8 +469,8 @@ public class PlayerPatch {
* Used in YouTube 20.05.46+.
*/
public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
- @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
- final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
+ @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
+ final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) {
Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L);
}
@@ -518,6 +516,12 @@ public class PlayerPatch {
return SPEED_OVERLAY_VALUE;
}
+ public static float speedOverlayRelativeValue(float original) {
+ return SPEED_OVERLAY_VALUE != 2.0f
+ ? 0f
+ : original;
+ }
+
public static boolean hideChannelWatermark(boolean original) {
return !Settings.HIDE_CHANNEL_WATERMARK.get() && original;
}
@@ -540,6 +544,10 @@ public class PlayerPatch {
return Settings.HIDE_FILMSTRIP_OVERLAY.get();
}
+ public static boolean hideFilmstripOverlay(boolean original) {
+ return !Settings.HIDE_FILMSTRIP_OVERLAY.get() && original;
+ }
+
public static boolean hideInfoCard(boolean original) {
return !Settings.HIDE_INFO_CARDS.get() && original;
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/SeekbarColorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/SeekbarColorPatch.java
index 8b3b99528..0975a9b6e 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/SeekbarColorPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/SeekbarColorPatch.java
@@ -47,7 +47,7 @@ public class SeekbarColorPatch {
/**
* Empty seekbar gradient, if hide seekbar in feed is enabled.
*/
- private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = { 0x0, 0x0 };
+ private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = {0x0, 0x0};
/**
* Default YouTube seekbar color brightness.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt
index af38ab459..b485dec6f 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt
@@ -1,8 +1,10 @@
package app.revanced.extension.youtube.patches.player.requests
import androidx.annotation.GuardedBy
-import app.revanced.extension.shared.patches.client.YouTubeAppClient
-import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_ACTION_BUTTON
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
class ActionButtonRequest private constructor(
private val videoId: String,
- private val playerHeaders: Map,
+ private val requestHeader: Map,
) {
private val future: Future> = Utils.submitOnBackgroundThread {
- fetch(videoId, playerHeaders)
+ fetch(videoId, requestHeader)
}
val array: Array
@@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
}
companion object {
- /**
- * TCP connection and HTTP read timeout.
- */
- private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
-
- /**
- * Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
- */
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
})
@JvmStatic
- fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map) {
+ fun fetchRequestIfNeeded(videoId: String, requestHeader: Map) {
Objects.requireNonNull(videoId)
synchronized(cache) {
if (!cache.containsKey(videoId)) {
- cache[videoId] = ActionButtonRequest(videoId, playerHeaders)
+ cache[videoId] = ActionButtonRequest(videoId, requestHeader)
}
}
}
@@ -93,43 +87,28 @@ class ActionButtonRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
- private val REQUEST_HEADER_KEYS = arrayOf(
- "Authorization", // Available only to logged-in users.
- "X-GOOG-API-FORMAT-VERSION",
- "X-Goog-Visitor-Id"
- )
-
- private fun sendRequest(videoId: String, playerHeaders: Map): JSONObject? {
+ private fun sendRequest(videoId: String, requestHeader: Map): JSONObject? {
Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis()
- // '/next' request does not require PoToken.
+ // '/next' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
- val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
- PlayerRoutes.GET_VIDEO_ACTION_BUTTON,
- clientType
- )
- connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
- connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
-
// Since [THANKS] button and [CLIP] button are shown only with the logged in,
// Set the [Authorization] field to property to get the correct action buttons.
- for (key in REQUEST_HEADER_KEYS) {
- var value = playerHeaders[key]
- if (value != null) {
- connection.setRequestProperty(key, value)
- }
- }
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ GET_VIDEO_ACTION_BUTTON,
+ clientType,
+ requestHeader,
+ )
- val requestBody =
- PlayerRoutes.createApplicationRequestBody(
- clientType = clientType,
- videoId = videoId
- )
+ val requestBody = createApplicationRequestBody(
+ clientType = clientType,
+ videoId = videoId
+ )
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
return emptyArray()
}
- private fun fetch(videoId: String, playerHeaders: Map): Array {
- val json = sendRequest(videoId, playerHeaders)
+ private fun fetch(
+ videoId: String,
+ requestHeader: Map
+ ): Array {
+ val json = sendRequest(videoId, requestHeader)
if (json != null) {
return parseResponse(json)
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java
index 4bf010129..d781ba937 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java
@@ -1,25 +1,15 @@
package app.revanced.extension.youtube.patches.shorts;
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
-import static app.revanced.extension.shared.utils.Utils.dpToPx;
import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible;
+import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
-import android.app.AlertDialog;
import android.content.Context;
-import android.graphics.ColorFilter;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.StateListDrawable;
import android.support.v7.widget.RecyclerView;
-import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -41,8 +31,7 @@ import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter;
import app.revanced.extension.youtube.settings.Settings;
-import app.revanced.extension.youtube.shared.ShortsPlayerState;
-import app.revanced.extension.youtube.utils.ThemeUtils;
+import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
@@ -66,7 +55,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) {
return;
}
- if (ShortsPlayerState.getCurrent().isClosed()) {
+ if (!isShortsActive()) {
return;
}
if (!isMoreButton(enumString)) {
@@ -90,105 +79,28 @@ public final class CustomActionsPatch {
}), 0);
}
- private static void showMoreButtonDialog(Context context) {
- ScrollView scrollView = new ScrollView(context);
- LinearLayout container = new LinearLayout(context);
+ private static void showMoreButtonDialog(Context mContext) {
+ ScrollView mScrollView = new ScrollView(mContext);
+ LinearLayout mLinearLayout = new LinearLayout(mContext);
+ mLinearLayout.setOrientation(LinearLayout.VERTICAL);
+ mLinearLayout.setPadding(0, 0, 0, 0);
- container.setOrientation(LinearLayout.VERTICAL);
- container.setPadding(0, 0, 0, 0);
-
- Map toolbarMap = new LinkedHashMap<>(arrSize);
+ Map actionsMap = new LinkedHashMap<>(arrSize);
for (CustomAction customAction : CustomAction.values()) {
if (customAction.settings.get()) {
String title = customAction.getLabel();
int iconId = customAction.getDrawableId();
Runnable action = customAction.getOnClickAction();
- LinearLayout itemLayout = createItemLayout(context, title, iconId);
- toolbarMap.putIfAbsent(itemLayout, action);
- container.addView(itemLayout);
+ LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, iconId);
+ actionsMap.putIfAbsent(itemLayout, action);
+ mLinearLayout.addView(itemLayout);
}
}
- scrollView.addView(container);
+ mScrollView.addView(mLinearLayout);
- AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setView(scrollView);
-
- AlertDialog dialog = builder.create();
- dialog.show();
-
- toolbarMap.forEach((view, action) ->
- view.setOnClickListener(v -> {
- action.run();
- dialog.dismiss();
- })
- );
- toolbarMap.clear();
-
- Window window = dialog.getWindow();
- if (window == null) {
- return;
- }
-
- // round corners
- GradientDrawable dialogBackground = new GradientDrawable();
- dialogBackground.setCornerRadius(32);
- window.setBackgroundDrawable(dialogBackground);
-
- // fit screen width
- int dialogWidth = (int) (context.getResources().getDisplayMetrics().widthPixels * 0.95);
- window.setLayout(dialogWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
-
- // move dialog to bottom
- WindowManager.LayoutParams layoutParams = window.getAttributes();
- layoutParams.gravity = Gravity.BOTTOM;
-
- // adjust the vertical offset
- layoutParams.y = dpToPx(5);
-
- window.setAttributes(layoutParams);
- }
-
- private static LinearLayout createItemLayout(Context context, String title, int iconId) {
- // Item Layout
- LinearLayout itemLayout = new LinearLayout(context);
- itemLayout.setOrientation(LinearLayout.HORIZONTAL);
- itemLayout.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(12));
- itemLayout.setGravity(Gravity.CENTER_VERTICAL);
- itemLayout.setClickable(true);
- itemLayout.setFocusable(true);
-
- // Create a StateListDrawable for the background
- StateListDrawable background = new StateListDrawable();
- ColorDrawable pressedDrawable = new ColorDrawable(ThemeUtils.getPressedElementColor());
- ColorDrawable defaultDrawable = new ColorDrawable(ThemeUtils.getBackgroundColor());
- background.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable);
- background.addState(new int[]{}, defaultDrawable);
- itemLayout.setBackground(background);
-
- // Icon
- ColorFilter cf = new PorterDuffColorFilter(ThemeUtils.getForegroundColor(), PorterDuff.Mode.SRC_ATOP);
- ImageView iconView = new ImageView(context);
- iconView.setImageResource(iconId);
- iconView.setColorFilter(cf);
- LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dpToPx(24), dpToPx(24));
- iconParams.setMarginEnd(dpToPx(16));
- iconView.setLayoutParams(iconParams);
- itemLayout.addView(iconView);
-
- // Text container
- LinearLayout textContainer = new LinearLayout(context);
- textContainer.setOrientation(LinearLayout.VERTICAL);
- TextView titleView = new TextView(context);
- titleView.setText(title);
- titleView.setTextSize(16);
- titleView.setTextColor(ThemeUtils.getForegroundColor());
- textContainer.addView(titleView);
-
- itemLayout.addView(textContainer);
-
- return itemLayout;
+ ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
}
private static boolean isMoreButton(String enumString) {
@@ -206,7 +118,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return;
}
- if (ShortsPlayerState.getCurrent().isClosed()) {
+ if (!isShortsActive()) {
return;
}
if (bottomSheetMenuObject == null) {
@@ -224,7 +136,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return;
}
- if (ShortsPlayerState.getCurrent().isClosed()) {
+ if (!isShortsActive()) {
return;
}
for (CustomAction customAction : CustomAction.values()) {
@@ -243,6 +155,34 @@ public final class CustomActionsPatch {
Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject);
}
+ /**
+ * Injection point.
+ */
+ public static boolean onBottomSheetMenuItemClick(View view) {
+ try {
+ if (view instanceof ViewGroup viewGroup) {
+ TextView textView = Utils.getChildView(viewGroup, v -> v instanceof TextView);
+ if (textView != null) {
+ String menuTitle = textView.getText().toString();
+ for (CustomAction customAction : CustomAction.values()) {
+ if (customAction.getLabel().equals(menuTitle)) {
+ View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
+ if (onLongClick != null) {
+ view.setOnLongClickListener(onLongClick);
+ }
+ customAction.getOnClickAction().run();
+ return true;
+ }
+ }
+ }
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "onBottomSheetMenuItemClick failed");
+ }
+
+ return false;
+ }
+
/**
* Injection point.
*/
@@ -252,7 +192,7 @@ public final class CustomActionsPatch {
}
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try {
- if (ShortsPlayerState.getCurrent().isClosed()) {
+ if (!isShortsActive()) {
return;
}
contextRef = new WeakReference<>(recyclerView.getContext());
@@ -267,8 +207,9 @@ public final class CustomActionsPatch {
if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) {
childCount = recyclerView.getChildCount();
if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) {
+ String menuTitle = textView.getText().toString();
for (CustomAction customAction : CustomAction.values()) {
- if (customAction.getLabel().equals(textView.getText().toString())) {
+ if (customAction.getLabel().equals(menuTitle)) {
View.OnClickListener onClick = customAction.getOnClickListener();
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
recyclerViewRef = new WeakReference<>(recyclerView);
@@ -384,6 +325,11 @@ public final class CustomActionsPatch {
true
)
),
+ SPEED_DIALOG(
+ Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG,
+ "yt_outline_play_arrow_half_circle_black_24",
+ () -> VideoUtils.showPlaybackSpeedDialog(contextRef.get())
+ ),
REPEAT_STATE(
Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE,
"yt_outline_arrow_repeat_1_black_24",
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsRepeatStatePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsRepeatStatePatch.java
index 50933c1f7..6e73485b7 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsRepeatStatePatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsRepeatStatePatch.java
@@ -2,6 +2,8 @@ package app.revanced.extension.youtube.patches.shorts;
import android.app.Activity;
+import androidx.annotation.Nullable;
+
import java.lang.ref.WeakReference;
import java.util.Objects;
@@ -29,10 +31,15 @@ public class ShortsRepeatStatePatch {
static void setYTEnumValue(Enum> ytBehavior) {
for (ShortsLoopBehavior rvBehavior : values()) {
- if (ytBehavior.name().endsWith(rvBehavior.name())) {
- rvBehavior.ytEnumValue = ytBehavior;
-
- Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytBehavior.name());
+ String ytName = ytBehavior.name();
+ if (ytName.endsWith(rvBehavior.name())) {
+ if (rvBehavior.ytEnumValue != null) {
+ Logger.printException(() -> "Conflicting behavior names: " + rvBehavior
+ + " ytBehavior: " + ytName);
+ } else {
+ rvBehavior.ytEnumValue = ytBehavior;
+ Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytName);
+ }
return;
}
}
@@ -77,25 +84,39 @@ public class ShortsRepeatStatePatch {
/**
* Injection point.
*/
- public static Enum> changeShortsRepeatBehavior(Enum> original) {
+ @Nullable
+ public static Enum> changeShortsRepeatBehavior(@Nullable Enum> original) {
try {
- final ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
+ ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
isAppInBackgroundPiPMode()
? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get()
: Settings.CHANGE_SHORTS_REPEAT_STATE.get();
+ Enum> overrideBehavior = behavior.ytEnumValue;
- if (behavior != ShortsLoopBehavior.UNKNOWN && behavior.ytEnumValue != null) {
- Logger.printDebug(() -> behavior.ytEnumValue == original
- ? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue
- : "Behavior setting is same as original. Using original: " + original.name()
- );
+ if (behavior != ShortsLoopBehavior.UNKNOWN && overrideBehavior != null) {
+ Logger.printDebug(() -> {
+ String name = original == null ? "unknown (null)" : original.name();
+ return overrideBehavior == original
+ ? "Behavior setting is same as original. Using original: " + name
+ : "Changing Shorts repeat behavior from: " + name + " to: " + overrideBehavior.name();
+ });
- return behavior.ytEnumValue;
+ // For some reason, in YouTube 20.09+, 'UNKNOWN' functions as 'Pause'.
+ return ExtendedUtils.IS_20_09_OR_GREATER && behavior == ShortsLoopBehavior.END_SCREEN
+ ? ShortsLoopBehavior.UNKNOWN.ytEnumValue
+ : overrideBehavior;
}
} catch (Exception ex) {
- Logger.printException(() -> "changeShortsRepeatState failure", ex);
+ Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
}
return original;
}
+
+ /**
+ * Injection point.
+ */
+ public static boolean isAutoPlay(@Nullable Enum> original) {
+ return ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original;
+ }
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java
index f4b0d16ad..794b9f0e1 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java
@@ -4,6 +4,7 @@ import android.view.View;
import java.lang.ref.WeakReference;
+import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "deprecation"})
@@ -59,4 +60,20 @@ public class SwipeControlsPatch {
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE;
}
+ public static final class SwipeOverlayTextSizeAvailability implements Setting.Availability {
+ @Override
+ public boolean isAvailable() {
+ return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
+ !Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
+ }
+ }
+
+ public static final class SwipeOverlayModernUIAvailability implements Setting.Availability {
+ @Override
+ public boolean isAvailable() {
+ return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
+ Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
+ }
+ }
+
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaybackSpeedWhilePlayingPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaybackSpeedWhilePlayingPatch.java
index 67e65bd5c..00990e95c 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaybackSpeedWhilePlayingPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaybackSpeedWhilePlayingPatch.java
@@ -1,20 +1,24 @@
package app.revanced.extension.youtube.patches.utils;
import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.youtube.shared.EngagementPanel;
import app.revanced.extension.youtube.shared.PlayerType;
+import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused")
public class PlaybackSpeedWhilePlayingPatch {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
public static boolean playbackSpeedChanged(float playbackSpeed) {
- PlayerType playerType = PlayerType.getCurrent();
- if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED &&
- playerType.isMaximizedOrFullscreenOrPiP()) {
+ if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED) {
+ if (PlayerType.getCurrent().isMaximizedOrFullscreenOrPiP()
+ // Since RVX has a default playback speed setting for Shorts,
+ // Playback speed reset should also be prevented in Shorts.
+ || ShortsPlayerState.getCurrent().isOpen() && EngagementPanel.isOpen()) {
+ Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request");
- Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request: " + playerType.name());
-
- return true;
+ return true;
+ }
}
return false;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaylistPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaylistPatch.java
new file mode 100644
index 000000000..484ad319a
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaylistPatch.java
@@ -0,0 +1,508 @@
+package app.revanced.extension.youtube.patches.utils;
+
+import static app.revanced.extension.shared.utils.StringRef.str;
+
+import android.content.Context;
+import android.view.KeyEvent;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
+
+import org.apache.commons.collections4.BidiMap;
+import org.apache.commons.collections4.bidimap.DualHashBidiMap;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.ResourceUtils;
+import app.revanced.extension.shared.utils.Utils;
+import app.revanced.extension.youtube.patches.utils.requests.CreatePlaylistRequest;
+import app.revanced.extension.youtube.patches.utils.requests.DeletePlaylistRequest;
+import app.revanced.extension.youtube.patches.utils.requests.EditPlaylistRequest;
+import app.revanced.extension.youtube.patches.utils.requests.GetPlaylistsRequest;
+import app.revanced.extension.youtube.patches.utils.requests.SavePlaylistRequest;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.utils.ExtendedUtils;
+import app.revanced.extension.youtube.utils.VideoUtils;
+import kotlin.Pair;
+
+// TODO: Implement sync queue and clean up code.
+@SuppressWarnings({"unused", "StaticFieldLeak"})
+public class PlaylistPatch extends VideoUtils {
+ private static final String AUTHORIZATION_HEADER = "Authorization";
+ private static final String[] REQUEST_HEADER_KEYS = {
+ AUTHORIZATION_HEADER,
+ "X-GOOG-API-FORMAT-VERSION",
+ "X-Goog-Visitor-Id"
+ };
+ private static final boolean QUEUE_MANAGER =
+ Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER.get()
+ || Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER.get();
+
+ private static Context mContext;
+ private static volatile String authorization = "";
+ public static volatile String dataSyncId = "";
+ public static volatile boolean isIncognito = false;
+ private static volatile Map requestHeader;
+ private static volatile String playlistId = "";
+ private static volatile String videoId = "";
+
+ private static String checkFailedAuth;
+ private static String checkFailedPlaylistId;
+ private static String checkFailedQueue;
+ private static String checkFailedVideoId;
+ private static String checkFailedGeneric;
+
+ private static String fetchFailedAdd;
+ private static String fetchFailedCreate;
+ private static String fetchFailedDelete;
+ private static String fetchFailedRemove;
+ private static String fetchFailedSave;
+
+ private static String fetchSucceededAdd;
+ private static String fetchSucceededCreate;
+ private static String fetchSucceededDelete;
+ private static String fetchSucceededRemove;
+ private static String fetchSucceededSave;
+
+ static {
+ Context mContext = Utils.getContext();
+ if (mContext != null && mContext.getResources() != null) {
+ checkFailedAuth = str("revanced_queue_manager_check_failed_auth");
+ checkFailedPlaylistId = str("revanced_queue_manager_check_failed_playlist_id");
+ checkFailedQueue = str("revanced_queue_manager_check_failed_queue");
+ checkFailedVideoId = str("revanced_queue_manager_check_failed_video_id");
+ checkFailedGeneric = str("revanced_queue_manager_check_failed_generic");
+
+ fetchFailedAdd = str("revanced_queue_manager_fetch_failed_add");
+ fetchFailedCreate = str("revanced_queue_manager_fetch_failed_create");
+ fetchFailedDelete = str("revanced_queue_manager_fetch_failed_delete");
+ fetchFailedRemove = str("revanced_queue_manager_fetch_failed_remove");
+ fetchFailedSave = str("revanced_queue_manager_fetch_failed_save");
+
+ fetchSucceededAdd = str("revanced_queue_manager_fetch_succeeded_add");
+ fetchSucceededCreate = str("revanced_queue_manager_fetch_succeeded_create");
+ fetchSucceededDelete = str("revanced_queue_manager_fetch_succeeded_delete");
+ fetchSucceededRemove = str("revanced_queue_manager_fetch_succeeded_remove");
+ fetchSucceededSave = str("revanced_queue_manager_fetch_succeeded_save");
+ }
+ }
+
+ @GuardedBy("itself")
+ private static final BidiMap lastVideoIds = new DualHashBidiMap<>();
+
+ /**
+ * Injection point.
+ */
+ public static boolean onKeyLongPress(int keyCode) {
+ if (!QUEUE_MANAGER || keyCode != KeyEvent.KEYCODE_BACK) {
+ return false;
+ }
+ if (mContext == null) {
+ handleCheckError(checkFailedQueue);
+ return false;
+ }
+ prepareDialogBuilder("");
+ return true;
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void removeFromQueue(@Nullable String setVideoId) {
+ if (StringUtils.isNotEmpty(setVideoId)) {
+ synchronized (lastVideoIds) {
+ String videoId = lastVideoIds.inverseBidiMap().get(setVideoId);
+ if (videoId != null) {
+ lastVideoIds.remove(videoId, setVideoId);
+ EditPlaylistRequest.clearVideoId(videoId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void setPivotBar(PivotBar view) {
+ if (QUEUE_MANAGER) {
+ mContext = view.getContext();
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void setRequestHeaders(String url, Map requestHeaders) {
+ if (QUEUE_MANAGER) {
+ try {
+ // Save requestHeaders whenever an account is switched.
+ String auth = requestHeaders.get(AUTHORIZATION_HEADER);
+ if (auth == null || authorization.equals(auth)) {
+ return;
+ }
+ for (String key : REQUEST_HEADER_KEYS) {
+ if (requestHeaders.get(key) == null) {
+ return;
+ }
+ }
+ authorization = auth;
+ requestHeader = requestHeaders;
+ } catch (Exception ex) {
+ Logger.printException(() -> "setRequestHeaders failure", ex);
+ }
+ }
+ }
+
+ /**
+ * Invoked by extension.
+ */
+ public static void setContext(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Invoked by extension.
+ */
+ public static void prepareDialogBuilder(@NonNull String currentVideoId) {
+ if (authorization.isEmpty() || (dataSyncId.isEmpty() && isIncognito)) {
+ handleCheckError(checkFailedAuth);
+ return;
+ }
+ if (currentVideoId.isEmpty()) {
+ buildBottomSheetDialog(QueueManager.noVideoIdQueueEntries);
+ } else {
+ videoId = currentVideoId;
+ synchronized (lastVideoIds) {
+ QueueManager[] customActionsEntries = playlistId.isEmpty() || lastVideoIds.get(currentVideoId) == null
+ ? QueueManager.addToQueueEntries
+ : QueueManager.removeFromQueueEntries;
+
+ buildBottomSheetDialog(customActionsEntries);
+ }
+ }
+ }
+
+ private static void buildBottomSheetDialog(QueueManager[] queueManagerEntries) {
+ ScrollView mScrollView = new ScrollView(mContext);
+ LinearLayout mLinearLayout = new LinearLayout(mContext);
+ mLinearLayout.setOrientation(LinearLayout.VERTICAL);
+ mLinearLayout.setPadding(0, 0, 0, 0);
+
+ Map actionsMap = new LinkedHashMap<>(queueManagerEntries.length);
+
+ for (QueueManager queueManager : queueManagerEntries) {
+ String title = queueManager.label;
+ int iconId = queueManager.drawableId;
+ Runnable action = queueManager.onClickAction;
+ LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, iconId);
+ actionsMap.putIfAbsent(itemLayout, action);
+ mLinearLayout.addView(itemLayout);
+ }
+
+ mScrollView.addView(mLinearLayout);
+
+ ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
+ }
+
+ private static void fetchQueue(boolean remove, boolean openPlaylist, boolean openVideo) {
+ try {
+ String currentPlaylistId = playlistId;
+ String currentVideoId = videoId;
+ synchronized (lastVideoIds) {
+ if (currentPlaylistId.isEmpty()) { // Queue is empty, create new playlist.
+ CreatePlaylistRequest.fetchRequestIfNeeded(currentVideoId, requestHeader, dataSyncId);
+ runOnMainThreadDelayed(() -> {
+ CreatePlaylistRequest request = CreatePlaylistRequest.getRequestForVideoId(currentVideoId);
+ if (request != null) {
+ Pair playlistIds = request.getPlaylistId();
+ if (playlistIds != null) {
+ String createdPlaylistId = playlistIds.getFirst();
+ String setVideoId = playlistIds.getSecond();
+ if (createdPlaylistId != null && setVideoId != null) {
+ playlistId = createdPlaylistId;
+ lastVideoIds.putIfAbsent(currentVideoId, setVideoId);
+ showToast(fetchSucceededCreate);
+ Logger.printDebug(() -> "Queue successfully created, playlistId: " + createdPlaylistId + ", setVideoId: " + setVideoId);
+ if (openPlaylist) {
+ openQueue(currentVideoId, openVideo);
+ }
+ return;
+ }
+ }
+ }
+ showToast(fetchFailedCreate);
+ }, 1000);
+ } else { // Queue is not empty, add or remove video.
+ String setVideoId = lastVideoIds.get(currentVideoId);
+ EditPlaylistRequest.fetchRequestIfNeeded(currentVideoId, currentPlaylistId, setVideoId, requestHeader, dataSyncId);
+
+ runOnMainThreadDelayed(() -> {
+ EditPlaylistRequest request = EditPlaylistRequest.getRequestForVideoId(currentVideoId);
+ if (request != null) {
+ String fetchedSetVideoId = request.getResult();
+ Logger.printDebug(() -> "fetchedSetVideoId: " + fetchedSetVideoId);
+ if (remove) { // Remove from queue.
+ if (StringUtils.isEmpty(fetchedSetVideoId)) {
+ lastVideoIds.remove(currentVideoId, setVideoId);
+ showToast(fetchSucceededRemove);
+ if (openPlaylist) {
+ openQueue(currentVideoId, openVideo);
+ }
+ return;
+ }
+ showToast(fetchFailedRemove);
+ } else { // Add to queue.
+ if (StringUtils.isNotEmpty(fetchedSetVideoId)) {
+ lastVideoIds.putIfAbsent(currentVideoId, fetchedSetVideoId);
+ showToast(fetchSucceededAdd);
+ Logger.printDebug(() -> "Video successfully added, setVideoId: " + fetchedSetVideoId);
+ if (openPlaylist) {
+ openQueue(currentVideoId, openVideo);
+ }
+ return;
+ }
+ showToast(fetchFailedAdd);
+ }
+ }
+ }, 1000);
+ }
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "fetchQueue failure", ex);
+ }
+ }
+
+ private static void saveToPlaylist() {
+ String currentPlaylistId = playlistId;
+ if (currentPlaylistId.isEmpty()) {
+ handleCheckError(checkFailedQueue);
+ return;
+ }
+ try {
+ GetPlaylistsRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
+ runOnMainThreadDelayed(() -> {
+ GetPlaylistsRequest request = GetPlaylistsRequest.getRequestForPlaylistId(currentPlaylistId);
+ if (request != null) {
+ Pair[] playlists = request.getPlaylists();
+ if (playlists != null) {
+ ScrollView mScrollView = new ScrollView(mContext);
+ LinearLayout mLinearLayout = new LinearLayout(mContext);
+ mLinearLayout.setOrientation(LinearLayout.VERTICAL);
+ mLinearLayout.setPadding(0, 0, 0, 0);
+
+ Map actionsMap = new LinkedHashMap<>(playlists.length);
+
+ int libraryIconId = QueueManager.SAVE_QUEUE.drawableId;
+
+ for (Pair playlist : playlists) {
+ String playlistId = playlist.getFirst();
+ String title = playlist.getSecond();
+ Runnable action = () -> saveToPlaylist(playlistId, title);
+ LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, libraryIconId);
+ actionsMap.putIfAbsent(itemLayout, action);
+ mLinearLayout.addView(itemLayout);
+ }
+
+ mScrollView.addView(mLinearLayout);
+
+ ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
+ GetPlaylistsRequest.clear();
+ }
+ }
+ }, 1000);
+ } catch (Exception ex) {
+ Logger.printException(() -> "saveToPlaylist failure", ex);
+ }
+ }
+
+ private static void saveToPlaylist(@Nullable String libraryId, @Nullable String libraryTitle) {
+ try {
+ if (StringUtils.isEmpty(libraryId)) {
+ handleCheckError(checkFailedPlaylistId);
+ return;
+ }
+ SavePlaylistRequest.fetchRequestIfNeeded(playlistId, libraryId, requestHeader, dataSyncId);
+
+ runOnMainThreadDelayed(() -> {
+ SavePlaylistRequest request = SavePlaylistRequest.getRequestForLibraryId(libraryId);
+ if (request != null) {
+ Boolean result = request.getResult();
+ if (BooleanUtils.isTrue(result)) {
+ showToast(String.format(fetchSucceededSave, libraryTitle));
+ SavePlaylistRequest.clear();
+ return;
+ }
+ showToast(fetchFailedSave);
+ }
+ }, 1000);
+ } catch (Exception ex) {
+ Logger.printException(() -> "saveToPlaylist failure", ex);
+ }
+ }
+
+ private static void removeQueue() {
+ String currentPlaylistId = playlistId;
+ if (currentPlaylistId.isEmpty()) {
+ handleCheckError(checkFailedQueue);
+ return;
+ }
+ try {
+ DeletePlaylistRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
+ runOnMainThreadDelayed(() -> {
+ DeletePlaylistRequest request = DeletePlaylistRequest.getRequestForPlaylistId(currentPlaylistId);
+ if (request != null) {
+ Boolean result = request.getResult();
+ if (BooleanUtils.isTrue(result)) {
+ playlistId = "";
+ synchronized (lastVideoIds) {
+ lastVideoIds.clear();
+ }
+ CreatePlaylistRequest.clear();
+ DeletePlaylistRequest.clear();
+ EditPlaylistRequest.clear();
+ GetPlaylistsRequest.clear();
+ SavePlaylistRequest.clear();
+ showToast(fetchSucceededDelete);
+ return;
+ }
+ }
+ showToast(fetchFailedDelete);
+ }, 1000);
+ } catch (Exception ex) {
+ Logger.printException(() -> "removeQueue failure", ex);
+ }
+ }
+
+ private static void downloadVideo() {
+ String currentVideoId = videoId;
+ launchVideoExternalDownloader(currentVideoId);
+ }
+
+ private static void openQueue() {
+ openQueue("", false);
+ }
+
+ private static void openQueue(String currentVideoId, boolean openVideo) {
+ String currentPlaylistId = playlistId;
+ if (currentPlaylistId.isEmpty()) {
+ handleCheckError(checkFailedQueue);
+ return;
+ }
+ if (openVideo) {
+ if (StringUtils.isEmpty(currentVideoId)) {
+ handleCheckError(checkFailedVideoId);
+ return;
+ }
+ // Open a video from a playlist
+ openPlaylist(currentPlaylistId, currentVideoId);
+ } else {
+ // Open a playlist
+ openPlaylist(currentPlaylistId);
+ }
+ }
+
+ private static void handleCheckError(String reason) {
+ showToast(String.format(checkFailedGeneric, reason));
+ }
+
+ private static void showToast(String reason) {
+ Utils.showToastShort(reason);
+ }
+
+ private enum QueueManager {
+ ADD_TO_QUEUE(
+ "revanced_queue_manager_add_to_queue",
+ "yt_outline_list_add_black_24",
+ () -> fetchQueue(false, false, false)
+ ),
+ ADD_TO_QUEUE_AND_OPEN_QUEUE(
+ "revanced_queue_manager_add_to_queue_and_open_queue",
+ "yt_outline_list_add_black_24",
+ () -> fetchQueue(false, true, false)
+ ),
+ ADD_TO_QUEUE_AND_PLAY_VIDEO(
+ "revanced_queue_manager_add_to_queue_and_play_video",
+ "yt_outline_list_play_arrow_black_24",
+ () -> fetchQueue(false, true, true)
+ ),
+ REMOVE_FROM_QUEUE(
+ "revanced_queue_manager_remove_from_queue",
+ "yt_outline_trash_can_black_24",
+ () -> fetchQueue(true, false, false)
+ ),
+ REMOVE_FROM_QUEUE_AND_OPEN_QUEUE(
+ "revanced_queue_manager_remove_from_queue_and_open_queue",
+ "yt_outline_trash_can_black_24",
+ () -> fetchQueue(true, true, false)
+ ),
+ OPEN_QUEUE(
+ "revanced_queue_manager_open_queue",
+ "yt_outline_list_view_black_24",
+ PlaylistPatch::openQueue
+ ),
+ // For some reason, the 'playlist/delete' endpoint is unavailable.
+ REMOVE_QUEUE(
+ "revanced_queue_manager_remove_queue",
+ "yt_outline_slash_circle_left_black_24",
+ PlaylistPatch::removeQueue
+ ),
+ SAVE_QUEUE(
+ "revanced_queue_manager_save_queue",
+ "yt_outline_bookmark_black_24",
+ PlaylistPatch::saveToPlaylist
+ ),
+ EXTERNAL_DOWNLOADER(
+ "revanced_queue_manager_external_downloader",
+ "yt_outline_download_black_24",
+ PlaylistPatch::downloadVideo
+ );
+
+ public final int drawableId;
+
+ @NonNull
+ public final String label;
+
+ @NonNull
+ public final Runnable onClickAction;
+
+ QueueManager(@NonNull String label, @NonNull String icon, @NonNull Runnable onClickAction) {
+ this.drawableId = ResourceUtils.getDrawableIdentifier(icon);
+ this.label = ResourceUtils.getString(label);
+ this.onClickAction = onClickAction;
+ }
+
+ public static final QueueManager[] addToQueueEntries = {
+ ADD_TO_QUEUE,
+ ADD_TO_QUEUE_AND_OPEN_QUEUE,
+ ADD_TO_QUEUE_AND_PLAY_VIDEO,
+ OPEN_QUEUE,
+ //REMOVE_QUEUE,
+ EXTERNAL_DOWNLOADER,
+ SAVE_QUEUE,
+ };
+
+ public static final QueueManager[] removeFromQueueEntries = {
+ REMOVE_FROM_QUEUE,
+ REMOVE_FROM_QUEUE_AND_OPEN_QUEUE,
+ OPEN_QUEUE,
+ //REMOVE_QUEUE,
+ EXTERNAL_DOWNLOADER,
+ SAVE_QUEUE,
+ };
+
+ public static final QueueManager[] noVideoIdQueueEntries = {
+ OPEN_QUEUE,
+ //REMOVE_QUEUE,
+ SAVE_QUEUE,
+ };
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/CreatePlaylistRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/CreatePlaylistRequest.kt
new file mode 100644
index 000000000..6496c77c2
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/CreatePlaylistRequest.kt
@@ -0,0 +1,281 @@
+package app.revanced.extension.youtube.patches.utils.requests
+
+import androidx.annotation.GuardedBy
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createPlaylistRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.CREATE_PLAYLIST
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_SET_VIDEO_ID
+import app.revanced.extension.shared.requests.Requester
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.Utils
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.SocketTimeoutException
+import java.util.Collections
+import java.util.Objects
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+class CreatePlaylistRequest private constructor(
+ private val videoId: String,
+ private val requestHeader: Map,
+ private val dataSyncId: String,
+) {
+ private val future: Future> = Utils.submitOnBackgroundThread {
+ fetch(
+ videoId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+
+ val playlistId: Pair?
+ get() {
+ try {
+ return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
+ } catch (ex: TimeoutException) {
+ Logger.printInfo(
+ { "getPlaylistId timed out" },
+ ex
+ )
+ } catch (ex: InterruptedException) {
+ Logger.printException(
+ { "getPlaylistId interrupted" },
+ ex
+ )
+ Thread.currentThread().interrupt() // Restore interrupt status flag.
+ } catch (ex: ExecutionException) {
+ Logger.printException(
+ { "getPlaylistId failure" },
+ ex
+ )
+ }
+
+ return null
+ }
+
+ companion object {
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+
+ @GuardedBy("itself")
+ val cache: MutableMap = Collections.synchronizedMap(
+ object : LinkedHashMap(100) {
+ private val CACHE_LIMIT = 50
+
+ override fun removeEldestEntry(eldest: Map.Entry): Boolean {
+ return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
+ }
+ })
+
+ @JvmStatic
+ fun clear() {
+ synchronized(cache) {
+ cache.clear()
+ }
+ }
+
+ @JvmStatic
+ fun fetchRequestIfNeeded(
+ videoId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ) {
+ Objects.requireNonNull(videoId)
+ synchronized(cache) {
+ if (!cache.containsKey(videoId)) {
+ cache[videoId] = CreatePlaylistRequest(
+ videoId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ fun getRequestForVideoId(videoId: String): CreatePlaylistRequest? {
+ synchronized(cache) {
+ return cache[videoId]
+ }
+ }
+
+ private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ Logger.printInfo({ toastMessage }, ex)
+ }
+
+ private fun sendCreatePlaylistRequest(
+ videoId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): JSONObject? {
+ Objects.requireNonNull(videoId)
+
+ val startTime = System.currentTimeMillis()
+ // 'playlist/create' endpoint does not require PoToken.
+ val clientType = YouTubeAppClient.ClientType.ANDROID
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching create playlist request for: $videoId, using client: $clientTypeName" }
+
+ try {
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ CREATE_PLAYLIST,
+ clientType,
+ requestHeader,
+ dataSyncId,
+ )
+
+ val requestBody = createPlaylistRequestBody(videoId = videoId)
+
+ connection.setFixedLengthStreamingMode(requestBody.size)
+ connection.outputStream.write(requestBody)
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) return Requester.parseJSONObject(connection)
+
+ handleConnectionError(
+ (clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.responseMessage),
+ null
+ )
+ } catch (ex: SocketTimeoutException) {
+ handleConnectionError("Connection timeout", ex)
+ } catch (ex: IOException) {
+ handleConnectionError("Network error", ex)
+ } catch (ex: Exception) {
+ Logger.printException({ "sendCreatePlaylistRequest failed" }, ex)
+ } finally {
+ Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
+ }
+
+ return null
+ }
+
+ private fun sendSetVideoIdRequest(
+ videoId: String,
+ playlistId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): JSONObject? {
+ Objects.requireNonNull(playlistId)
+
+ val startTime = System.currentTimeMillis()
+ // 'playlist/create' endpoint does not require PoToken.
+ val clientType = YouTubeAppClient.ClientType.ANDROID
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching set video id request for: $playlistId, using client: $clientTypeName" }
+
+ try {
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ GET_SET_VIDEO_ID,
+ clientType,
+ requestHeader,
+ dataSyncId,
+ )
+
+ val requestBody = createApplicationRequestBody(
+ clientType = clientType,
+ videoId = videoId,
+ playlistId = playlistId
+ )
+
+ connection.setFixedLengthStreamingMode(requestBody.size)
+ connection.outputStream.write(requestBody)
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) return Requester.parseJSONObject(connection)
+
+ handleConnectionError(
+ (clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.responseMessage),
+ null
+ )
+ } catch (ex: SocketTimeoutException) {
+ handleConnectionError("Connection timeout", ex)
+ } catch (ex: IOException) {
+ handleConnectionError("Network error", ex)
+ } catch (ex: Exception) {
+ Logger.printException({ "sendSetVideoIdRequest failed" }, ex)
+ } finally {
+ Logger.printDebug { "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
+ }
+
+ return null
+ }
+
+ private fun parseCreatePlaylistResponse(json: JSONObject): String? {
+ try {
+ return json.getString("playlistId")
+ } catch (e: JSONException) {
+ val jsonForMessage = json.toString()
+ Logger.printException(
+ { "Fetch failed while processing response data for response: $jsonForMessage" },
+ e
+ )
+ }
+
+ return null
+ }
+
+ private fun parseSetVideoIdResponse(json: JSONObject): String? {
+ try {
+ val secondaryContentsJsonObject =
+ json.getJSONObject("contents")
+ .getJSONObject("singleColumnWatchNextResults")
+ .getJSONObject("playlist")
+ .getJSONObject("playlist")
+ .getJSONArray("contents")
+ .get(0)
+
+ if (secondaryContentsJsonObject is JSONObject) {
+ return secondaryContentsJsonObject
+ .getJSONObject("playlistPanelVideoRenderer")
+ .getString("playlistSetVideoId")
+ }
+ } catch (e: JSONException) {
+ val jsonForMessage = json.toString()
+ Logger.printException(
+ { "Fetch failed while processing response data for response: $jsonForMessage" },
+ e
+ )
+ }
+
+ return null
+ }
+
+ private fun fetch(
+ videoId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): Pair? {
+ val createPlaylistJson = sendCreatePlaylistRequest(
+ videoId,
+ requestHeader,
+ dataSyncId
+ )
+ if (createPlaylistJson != null) {
+ val playlistId = parseCreatePlaylistResponse(createPlaylistJson)
+ if (playlistId != null) {
+ val setVideoIdJson = sendSetVideoIdRequest(
+ videoId,
+ playlistId,
+ requestHeader,
+ dataSyncId
+ )
+ if (setVideoIdJson != null) {
+ val setVideoId = parseSetVideoIdResponse(setVideoIdJson)
+ if (setVideoId != null) {
+ return Pair(playlistId, setVideoId)
+ }
+ }
+ }
+ }
+
+ return null
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/DeletePlaylistRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/DeletePlaylistRequest.kt
new file mode 100644
index 000000000..ea7ca60b2
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/DeletePlaylistRequest.kt
@@ -0,0 +1,187 @@
+package app.revanced.extension.youtube.patches.utils.requests
+
+import androidx.annotation.GuardedBy
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.deletePlaylistRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.DELETE_PLAYLIST
+import app.revanced.extension.shared.requests.Requester
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.Utils
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.SocketTimeoutException
+import java.util.Collections
+import java.util.Objects
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+class DeletePlaylistRequest private constructor(
+ private val playlistId: String,
+ private val requestHeader: Map,
+ private val dataSyncId: String,
+) {
+ private val future: Future = Utils.submitOnBackgroundThread {
+ fetch(
+ playlistId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+
+ val result: Boolean?
+ get() {
+ try {
+ return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
+ } catch (ex: TimeoutException) {
+ Logger.printInfo(
+ { "getResult timed out" },
+ ex
+ )
+ } catch (ex: InterruptedException) {
+ Logger.printException(
+ { "getResult interrupted" },
+ ex
+ )
+ Thread.currentThread().interrupt() // Restore interrupt status flag.
+ } catch (ex: ExecutionException) {
+ Logger.printException(
+ { "getResult failure" },
+ ex
+ )
+ }
+
+ return null
+ }
+
+ companion object {
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+
+ @GuardedBy("itself")
+ val cache: MutableMap = Collections.synchronizedMap(
+ object : LinkedHashMap(100) {
+ private val CACHE_LIMIT = 50
+
+ override fun removeEldestEntry(eldest: Map.Entry): Boolean {
+ return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
+ }
+ })
+
+ @JvmStatic
+ fun clear() {
+ synchronized(cache) {
+ cache.clear()
+ }
+ }
+
+ @JvmStatic
+ fun fetchRequestIfNeeded(
+ playlistId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ) {
+ Objects.requireNonNull(playlistId)
+ synchronized(cache) {
+ if (!cache.containsKey(playlistId)) {
+ cache[playlistId] = DeletePlaylistRequest(
+ playlistId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ fun getRequestForPlaylistId(playlistId: String): DeletePlaylistRequest? {
+ synchronized(cache) {
+ return cache[playlistId]
+ }
+ }
+
+ private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ Logger.printInfo({ toastMessage }, ex)
+ }
+
+ private fun sendRequest(
+ playlistId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): JSONObject? {
+ Objects.requireNonNull(playlistId)
+
+ val startTime = System.currentTimeMillis()
+ // 'playlist/delete' endpoint does not require PoToken.
+ val clientType = YouTubeAppClient.ClientType.ANDROID
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching delete playlist request, playlistId: $playlistId, using client: $clientTypeName" }
+
+ try {
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ DELETE_PLAYLIST,
+ clientType,
+ requestHeader,
+ dataSyncId
+ )
+
+ val requestBody = deletePlaylistRequestBody(playlistId)
+
+ connection.setFixedLengthStreamingMode(requestBody.size)
+ connection.outputStream.write(requestBody)
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) return Requester.parseJSONObject(connection)
+
+ handleConnectionError(
+ (clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.responseMessage),
+ null
+ )
+ } catch (ex: SocketTimeoutException) {
+ handleConnectionError("Connection timeout", ex)
+ } catch (ex: IOException) {
+ handleConnectionError("Network error", ex)
+ } catch (ex: Exception) {
+ Logger.printException({ "sendRequest failed" }, ex)
+ } finally {
+ Logger.printDebug { "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
+ }
+
+ return null
+ }
+
+ private fun parseResponse(json: JSONObject): Boolean? {
+ try {
+ return json.has("command")
+ } catch (e: JSONException) {
+ val jsonForMessage = json.toString()
+ Logger.printException(
+ { "Fetch failed while processing response data for response: $jsonForMessage" },
+ e
+ )
+ }
+
+ return null
+ }
+
+ private fun fetch(
+ playlistId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): Boolean? {
+ val json = sendRequest(
+ playlistId,
+ requestHeader,
+ dataSyncId,
+ )
+ if (json != null) {
+ return parseResponse(json)
+ }
+
+ return null
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/EditPlaylistRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/EditPlaylistRequest.kt
new file mode 100644
index 000000000..a76277543
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/EditPlaylistRequest.kt
@@ -0,0 +1,225 @@
+package app.revanced.extension.youtube.patches.utils.requests
+
+import androidx.annotation.GuardedBy
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.editPlaylistRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.EDIT_PLAYLIST
+import app.revanced.extension.shared.requests.Requester
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.Utils
+import org.apache.commons.lang3.StringUtils
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.SocketTimeoutException
+import java.util.Collections
+import java.util.Objects
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+class EditPlaylistRequest private constructor(
+ private val videoId: String,
+ private val playlistId: String,
+ private val setVideoId: String?,
+ private val requestHeader: Map,
+ private val dataSyncId: String,
+) {
+ private val future: Future = Utils.submitOnBackgroundThread {
+ fetch(
+ videoId,
+ playlistId,
+ setVideoId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+
+ val result: String?
+ get() {
+ try {
+ return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
+ } catch (ex: TimeoutException) {
+ Logger.printInfo(
+ { "getResult timed out" },
+ ex
+ )
+ } catch (ex: InterruptedException) {
+ Logger.printException(
+ { "getResult interrupted" },
+ ex
+ )
+ Thread.currentThread().interrupt() // Restore interrupt status flag.
+ } catch (ex: ExecutionException) {
+ Logger.printException(
+ { "getResult failure" },
+ ex
+ )
+ }
+
+ return null
+ }
+
+ companion object {
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+
+ @GuardedBy("itself")
+ val cache: MutableMap = Collections.synchronizedMap(
+ object : LinkedHashMap(100) {
+ private val CACHE_LIMIT = 50
+
+ override fun removeEldestEntry(eldest: Map.Entry): Boolean {
+ return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
+ }
+ })
+
+ @JvmStatic
+ fun clear() {
+ synchronized(cache) {
+ cache.clear()
+ }
+ }
+
+ @JvmStatic
+ fun clearVideoId(videoId: String) {
+ synchronized(cache) {
+ cache.remove(videoId)
+ }
+ }
+
+ @JvmStatic
+ fun fetchRequestIfNeeded(
+ videoId: String,
+ playlistId: String,
+ setVideoId: String?,
+ requestHeader: Map,
+ dataSyncId: String,
+ ) {
+ Objects.requireNonNull(videoId)
+ synchronized(cache) {
+ if (!cache.containsKey(videoId)) {
+ cache[videoId] = EditPlaylistRequest(
+ videoId,
+ playlistId,
+ setVideoId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ fun getRequestForVideoId(videoId: String): EditPlaylistRequest? {
+ synchronized(cache) {
+ return cache[videoId]
+ }
+ }
+
+ private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ Logger.printInfo({ toastMessage }, ex)
+ }
+
+ private fun sendRequest(
+ videoId: String,
+ playlistId: String,
+ setVideoId: String?,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): JSONObject? {
+ Objects.requireNonNull(videoId)
+
+ val startTime = System.currentTimeMillis()
+ // 'browse/edit_playlist' endpoint does not require PoToken.
+ val clientType = YouTubeAppClient.ClientType.ANDROID
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching edit playlist request, videoId: $videoId, playlistId: $playlistId, setVideoId: $setVideoId, using client: $clientTypeName" }
+
+ try {
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ EDIT_PLAYLIST,
+ clientType,
+ requestHeader,
+ dataSyncId
+ )
+
+ val requestBody = editPlaylistRequestBody(
+ videoId = videoId,
+ playlistId = playlistId,
+ setVideoId = setVideoId
+ )
+
+ connection.setFixedLengthStreamingMode(requestBody.size)
+ connection.outputStream.write(requestBody)
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) return Requester.parseJSONObject(connection)
+
+ handleConnectionError(
+ (clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.responseMessage),
+ null
+ )
+ } catch (ex: SocketTimeoutException) {
+ handleConnectionError("Connection timeout", ex)
+ } catch (ex: IOException) {
+ handleConnectionError("Network error", ex)
+ } catch (ex: Exception) {
+ Logger.printException({ "sendRequest failed" }, ex)
+ } finally {
+ Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
+ }
+
+ return null
+ }
+
+ private fun parseResponse(json: JSONObject, remove: Boolean): String? {
+ try {
+ if (json.getString("status") == "STATUS_SUCCEEDED") {
+ if (remove) {
+ return ""
+ }
+ val playlistEditResultsJSONObject =
+ json.getJSONArray("playlistEditResults").get(0)
+
+ if (playlistEditResultsJSONObject is JSONObject) {
+ return playlistEditResultsJSONObject
+ .getJSONObject("playlistEditVideoAddedResultData")
+ .getString("setVideoId")
+ }
+ }
+ } catch (e: JSONException) {
+ val jsonForMessage = json.toString()
+ Logger.printException(
+ { "Fetch failed while processing response data for response: $jsonForMessage" },
+ e
+ )
+ }
+
+ return null
+ }
+
+ private fun fetch(
+ videoId: String,
+ playlistId: String,
+ setVideoId: String?,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): String? {
+ val json = sendRequest(
+ videoId,
+ playlistId,
+ setVideoId,
+ requestHeader,
+ dataSyncId,
+ )
+ if (json != null) {
+ return parseResponse(json, StringUtils.isNotEmpty(setVideoId))
+ }
+
+ return null
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/GetPlaylistsRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/GetPlaylistsRequest.kt
new file mode 100644
index 000000000..94361f9ef
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/GetPlaylistsRequest.kt
@@ -0,0 +1,222 @@
+package app.revanced.extension.youtube.patches.utils.requests
+
+import androidx.annotation.GuardedBy
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getPlaylistsRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLISTS
+import app.revanced.extension.shared.requests.Requester
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.Utils
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.SocketTimeoutException
+import java.util.Collections
+import java.util.Objects
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+class GetPlaylistsRequest private constructor(
+ private val playlistId: String,
+ private val requestHeader: Map,
+ private val dataSyncId: String,
+) {
+ private val future: Future>> = Utils.submitOnBackgroundThread {
+ fetch(
+ playlistId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+
+ val playlists: Array>?
+ get() {
+ try {
+ return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
+ } catch (ex: TimeoutException) {
+ Logger.printInfo(
+ { "getPlaylists timed out" },
+ ex
+ )
+ } catch (ex: InterruptedException) {
+ Logger.printException(
+ { "getPlaylists interrupted" },
+ ex
+ )
+ Thread.currentThread().interrupt() // Restore interrupt status flag.
+ } catch (ex: ExecutionException) {
+ Logger.printException(
+ { "getPlaylists failure" },
+ ex
+ )
+ }
+
+ return null
+ }
+
+ companion object {
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+
+ @GuardedBy("itself")
+ val cache: MutableMap = Collections.synchronizedMap(
+ object : LinkedHashMap(100) {
+ private val CACHE_LIMIT = 50
+
+ override fun removeEldestEntry(eldest: Map.Entry): Boolean {
+ return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
+ }
+ })
+
+ @JvmStatic
+ fun clear() {
+ synchronized(cache) {
+ cache.clear()
+ }
+ }
+
+ @JvmStatic
+ fun fetchRequestIfNeeded(
+ playlistId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ) {
+ Objects.requireNonNull(playlistId)
+ synchronized(cache) {
+ if (!cache.containsKey(playlistId)) {
+ cache[playlistId] = GetPlaylistsRequest(
+ playlistId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+ }
+ }
+
+ @JvmStatic
+ fun getRequestForPlaylistId(playlistId: String): GetPlaylistsRequest? {
+ synchronized(cache) {
+ return cache[playlistId]
+ }
+ }
+
+ private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ Logger.printInfo({ toastMessage }, ex)
+ }
+
+ private fun sendRequest(
+ playlistId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): JSONObject? {
+ Objects.requireNonNull(playlistId)
+
+ val startTime = System.currentTimeMillis()
+ // 'playlist/get_add_to_playlist' endpoint does not require PoToken.
+ val clientType = YouTubeAppClient.ClientType.ANDROID
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching get playlists request, playlistId: $playlistId, using client: $clientTypeName" }
+
+ try {
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ GET_PLAYLISTS,
+ clientType,
+ requestHeader,
+ dataSyncId
+ )
+
+ val requestBody = getPlaylistsRequestBody(playlistId)
+
+ connection.setFixedLengthStreamingMode(requestBody.size)
+ connection.outputStream.write(requestBody)
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) return Requester.parseJSONObject(connection)
+
+ handleConnectionError(
+ (clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.responseMessage),
+ null
+ )
+ } catch (ex: SocketTimeoutException) {
+ handleConnectionError("Connection timeout", ex)
+ } catch (ex: IOException) {
+ handleConnectionError("Network error", ex)
+ } catch (ex: Exception) {
+ Logger.printException({ "sendRequest failed" }, ex)
+ } finally {
+ Logger.printDebug { "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
+ }
+
+ return null
+ }
+
+ private fun parseResponse(json: JSONObject): Array>? {
+ try {
+ val addToPlaylistRendererJsonObject =
+ json.getJSONArray("contents").get(0)
+
+ if (addToPlaylistRendererJsonObject is JSONObject) {
+ val playlistsJsonArray =
+ addToPlaylistRendererJsonObject
+ .getJSONObject("addToPlaylistRenderer")
+ .getJSONArray("playlists")
+
+ val playlistsLength = playlistsJsonArray.length()
+ val playlists: Array?> =
+ arrayOfNulls(playlistsLength)
+
+ for (i in 0..playlistsLength - 1) {
+ val elementsJsonObject =
+ playlistsJsonArray.get(i)
+
+ if (elementsJsonObject is JSONObject) {
+ val playlistAddToOptionRendererJSONObject =
+ elementsJsonObject
+ .getJSONObject("playlistAddToOptionRenderer")
+
+ val playlistId = playlistAddToOptionRendererJSONObject
+ .getString("playlistId")
+ val playlistTitle =
+ (playlistAddToOptionRendererJSONObject
+ .getJSONObject("title")
+ .getJSONArray("runs")
+ .get(0) as JSONObject)
+ .getString("text")
+
+ playlists[i] = Pair(playlistId, playlistTitle)
+ }
+ }
+
+ val finalPlaylists = playlists.filterNotNull().toTypedArray()
+ if (finalPlaylists.isNotEmpty()) {
+ return finalPlaylists
+ }
+ }
+ } catch (e: JSONException) {
+ val jsonForMessage = json.toString()
+ Logger.printException(
+ { "Fetch failed while processing response data for response: $jsonForMessage" },
+ e
+ )
+ }
+
+ return null
+ }
+
+ private fun fetch(
+ playlistId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): Array>? {
+ val json = sendRequest(playlistId, requestHeader, dataSyncId)
+ if (json != null) {
+ return parseResponse(json)
+ }
+
+ return null
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/SavePlaylistRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/SavePlaylistRequest.kt
new file mode 100644
index 000000000..221dfc737
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/requests/SavePlaylistRequest.kt
@@ -0,0 +1,193 @@
+package app.revanced.extension.youtube.patches.utils.requests
+
+import androidx.annotation.GuardedBy
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.savePlaylistRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.EDIT_PLAYLIST
+import app.revanced.extension.shared.requests.Requester
+import app.revanced.extension.shared.utils.Logger
+import app.revanced.extension.shared.utils.Utils
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.IOException
+import java.net.SocketTimeoutException
+import java.util.Collections
+import java.util.Objects
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+class SavePlaylistRequest private constructor(
+ private val playlistId: String,
+ private val libraryId: String,
+ private val requestHeader: Map,
+ private val dataSyncId: String,
+) {
+ private val future: Future = Utils.submitOnBackgroundThread {
+ fetch(
+ playlistId,
+ libraryId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+
+ val result: Boolean?
+ get() {
+ try {
+ return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
+ } catch (ex: TimeoutException) {
+ Logger.printInfo(
+ { "getResult timed out" },
+ ex
+ )
+ } catch (ex: InterruptedException) {
+ Logger.printException(
+ { "getResult interrupted" },
+ ex
+ )
+ Thread.currentThread().interrupt() // Restore interrupt status flag.
+ } catch (ex: ExecutionException) {
+ Logger.printException(
+ { "getResult failure" },
+ ex
+ )
+ }
+
+ return null
+ }
+
+ companion object {
+ private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
+
+ @GuardedBy("itself")
+ val cache: MutableMap = Collections.synchronizedMap(
+ object : LinkedHashMap(100) {
+ private val CACHE_LIMIT = 50
+
+ override fun removeEldestEntry(eldest: Map.Entry): Boolean {
+ return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
+ }
+ })
+
+ @JvmStatic
+ fun clear() {
+ synchronized(cache) {
+ cache.clear()
+ }
+ }
+
+ @JvmStatic
+ fun fetchRequestIfNeeded(
+ playlistId: String,
+ libraryId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ) {
+ Objects.requireNonNull(playlistId)
+ synchronized(cache) {
+ cache[libraryId] = SavePlaylistRequest(
+ playlistId,
+ libraryId,
+ requestHeader,
+ dataSyncId,
+ )
+ }
+ }
+
+ @JvmStatic
+ fun getRequestForLibraryId(libraryId: String): SavePlaylistRequest? {
+ synchronized(cache) {
+ return cache[libraryId]
+ }
+ }
+
+ private fun handleConnectionError(toastMessage: String, ex: Exception?) {
+ Logger.printInfo({ toastMessage }, ex)
+ }
+
+ private fun sendRequest(
+ playlistId: String,
+ libraryId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): JSONObject? {
+ Objects.requireNonNull(playlistId)
+ Objects.requireNonNull(libraryId)
+
+ val startTime = System.currentTimeMillis()
+ // 'browse/edit_playlist' endpoint does not require PoToken.
+ val clientType = YouTubeAppClient.ClientType.ANDROID
+ val clientTypeName = clientType.name
+ Logger.printDebug { "Fetching edit playlist request, playlistId: $playlistId, libraryId: $libraryId, using client: $clientTypeName" }
+
+ try {
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ EDIT_PLAYLIST,
+ clientType,
+ requestHeader,
+ dataSyncId
+ )
+
+ val requestBody = savePlaylistRequestBody(libraryId, playlistId)
+
+ connection.setFixedLengthStreamingMode(requestBody.size)
+ connection.outputStream.write(requestBody)
+
+ val responseCode = connection.responseCode
+ if (responseCode == 200) return Requester.parseJSONObject(connection)
+
+ handleConnectionError(
+ (clientTypeName + " not available with response code: "
+ + responseCode + " message: " + connection.responseMessage),
+ null
+ )
+ } catch (ex: SocketTimeoutException) {
+ handleConnectionError("Connection timeout", ex)
+ } catch (ex: IOException) {
+ handleConnectionError("Network error", ex)
+ } catch (ex: Exception) {
+ Logger.printException({ "sendRequest failed" }, ex)
+ } finally {
+ Logger.printDebug { "playlistId: $playlistId libraryId: $libraryId took: ${(System.currentTimeMillis() - startTime)}ms" }
+ }
+
+ return null
+ }
+
+ private fun parseResponse(json: JSONObject): Boolean? {
+ try {
+ return json.getString("status") == "STATUS_SUCCEEDED"
+ } catch (e: JSONException) {
+ val jsonForMessage = json.toString()
+ Logger.printException(
+ { "Fetch failed while processing response data for response: $jsonForMessage" },
+ e
+ )
+ }
+
+ return null
+ }
+
+ private fun fetch(
+ playlistId: String,
+ libraryId: String,
+ requestHeader: Map,
+ dataSyncId: String,
+ ): Boolean? {
+ val json = sendRequest(
+ playlistId,
+ libraryId,
+ requestHeader,
+ dataSyncId,
+ )
+ if (json != null) {
+ return parseResponse(json)
+ }
+
+ return null
+ }
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/AV1CodecPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/AV1CodecPatch.java
index 71f5dd9d6..8cf4980d1 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/AV1CodecPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/AV1CodecPatch.java
@@ -1,17 +1,10 @@
package app.revanced.extension.youtube.patches.video;
-import static app.revanced.extension.shared.utils.StringRef.str;
-
-import app.revanced.extension.shared.utils.Logger;
-import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class AV1CodecPatch {
- private static final int LITERAL_VALUE_AV01 = 1635135811;
- private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123;
private static final String VP9_CODEC = "video/x-vnd.on2.vp9";
- private static long lastTimeResponse = 0;
/**
* Replace the SW AV01 codec to VP9 codec.
@@ -22,32 +15,4 @@ public class AV1CodecPatch {
public static String replaceCodec(String original) {
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original;
}
-
- /**
- * Replace the SW AV01 codec request with a Dolby Vision codec request.
- * This request is invalid, so it falls back to codecs other than AV01.
- *
- * Limitation: Fallback process causes about 15-20 seconds of buffering.
- *
- * @param literalValue literal value of the codec
- */
- public static int rejectResponse(int literalValue) {
- if (!Settings.REJECT_AV1_CODEC.get())
- return literalValue;
-
- Logger.printDebug(() -> "Response: " + literalValue);
-
- if (literalValue != LITERAL_VALUE_AV01)
- return literalValue;
-
- final long currentTime = System.currentTimeMillis();
-
- // Ignore the invoke within 20 seconds.
- if (currentTime - lastTimeResponse > 20000) {
- lastTimeResponse = currentTime;
- Utils.showToastShort(str("revanced_reject_av1_codec_toast"));
- }
-
- return LITERAL_VALUE_DOLBY_VISION;
- }
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/CustomPlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/CustomPlaybackSpeedPatch.java
index cca3c4ff4..83e343397 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/CustomPlaybackSpeedPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/CustomPlaybackSpeedPatch.java
@@ -75,30 +75,30 @@ public class CustomPlaybackSpeedPatch {
return isCustomPlaybackSpeedEnabled() ? 0 : original;
}
- public static String[] getListEntries() {
+ public static String[] getEntries() {
return isCustomPlaybackSpeedEnabled()
? customSpeedEntries
: defaultSpeedEntries;
}
- public static String[] getListEntryValues() {
+ public static String[] getEntryValues() {
return isCustomPlaybackSpeedEnabled()
? customSpeedEntryValues
: defaultSpeedEntryValues;
}
- public static String[] getTrimmedListEntries() {
+ public static String[] getTrimmedEntries() {
if (playbackSpeedEntries == null) {
- final String[] playbackSpeedWithAutoEntries = getListEntries();
+ final String[] playbackSpeedWithAutoEntries = getEntries();
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
}
return playbackSpeedEntries;
}
- public static String[] getTrimmedListEntryValues() {
+ public static String[] getTrimmedEntryValues() {
if (playbackSpeedEntryValues == null) {
- final String[] playbackSpeedWithAutoEntryValues = getListEntryValues();
+ final String[] playbackSpeedWithAutoEntryValues = getEntryValues();
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
index 5211c6a47..58897b35d 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java
@@ -1,12 +1,14 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
+import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
+import app.revanced.extension.shared.settings.BooleanSetting;
+import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
@@ -17,25 +19,57 @@ import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused")
public class PlaybackSpeedPatch {
+ private static final FloatSetting DEFAULT_PLAYBACK_SPEED =
+ Settings.DEFAULT_PLAYBACK_SPEED;
+ private static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS =
+ Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
+
private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC =
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
private static final long TOAST_DELAY_MILLISECONDS = 750;
private static long lastTimeSpeedChanged;
+ private static float lastSelectedPlaybackSpeed = 1.0f;
+
+ private static volatile String channelId = "";
+ private static volatile String videoId = "";
private static boolean isLiveStream;
+ private static volatile String channelIdShorts = "";
+ private static volatile String videoIdShorts = "";
+ private static boolean isLiveStreamShorts;
+
/**
* Injection point.
*/
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
- isLiveStream = newlyLoadedLiveStreamValue;
- Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
+ if (isShortsActive()) {
+ channelIdShorts = newlyLoadedChannelId;
+ videoIdShorts = newlyLoadedVideoId;
+ isLiveStreamShorts = newlyLoadedLiveStreamValue;
- final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId);
- Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed);
+ Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
+ } else {
+ channelId = newlyLoadedChannelId;
+ videoId = newlyLoadedVideoId;
+ isLiveStream = newlyLoadedLiveStreamValue;
- VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed);
+ Logger.printDebug(() -> "newShortsVideoStarted: " + newlyLoadedVideoId);
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
+ @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
+ final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
+ channelIdShorts = newlyLoadedChannelId;
+ videoIdShorts = newlyLoadedVideoId;
+ isLiveStreamShorts = newlyLoadedLiveStreamValue;
+
+ Logger.printDebug(() -> "newShortsVideoStarted: " + newlyLoadedVideoId);
}
/**
@@ -65,17 +99,34 @@ public class PlaybackSpeedPatch {
/**
* Injection point.
*/
- public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
- if (VideoInformation.lastPlayerResponseIsShort() &&
- Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()
- ) {
- float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
- Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
+ public static float getPlaybackSpeed(float playbackSpeed) {
+ boolean isShorts = isShortsActive();
+ String currentChannelId = isShorts ? channelIdShorts : channelId;
+ String currentVideoId = isShorts ? videoIdShorts : videoId;
+ boolean currentVideoIsLiveStream = isShorts ? isLiveStreamShorts : isLiveStream;
+ boolean currentVideoIsWhitelisted = Whitelist.isChannelWhitelistedPlaybackSpeed(currentChannelId);
+ boolean currentVideoIsMusic = !isShorts && isMusic();
- return defaultPlaybackSpeed;
+ if (currentVideoIsLiveStream || currentVideoIsWhitelisted || currentVideoIsMusic) {
+ Logger.printDebug(() -> "changing playback speed to: 1.0");
+ VideoInformation.setPlaybackSpeed(1.0f);
+ return 1.0f;
}
- return playbackSpeed;
+ float defaultPlaybackSpeed = isShorts ? DEFAULT_PLAYBACK_SPEED_SHORTS.get() : DEFAULT_PLAYBACK_SPEED.get();
+
+ if (defaultPlaybackSpeed < 0) {
+ float finalPlaybackSpeed = isShorts ? playbackSpeed : lastSelectedPlaybackSpeed;
+ VideoInformation.overridePlaybackSpeed(finalPlaybackSpeed);
+ Logger.printDebug(() -> "changing playback speed to: " + finalPlaybackSpeed);
+ return finalPlaybackSpeed;
+ } else {
+ if (isShorts) {
+ VideoInformation.setPlaybackSpeed(defaultPlaybackSpeed);
+ }
+ Logger.printDebug(() -> "changing playback speed to: " + defaultPlaybackSpeed);
+ return defaultPlaybackSpeed;
+ }
}
/**
@@ -86,51 +137,57 @@ public class PlaybackSpeedPatch {
*/
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
try {
- if (PatchStatus.RememberPlaybackSpeed() &&
- Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
- // With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
- // then the menu will allow increasing without bounds but the max speed is
- // still capped to under 8.0x.
- playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
+ boolean isShorts = isShortsActive();
+ if (PatchStatus.RememberPlaybackSpeed()) {
+ BooleanSetting rememberPlaybackSpeedLastSelectedSetting = isShorts
+ ? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED
+ : Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED;
+ FloatSetting playbackSpeedSetting = isShorts
+ ? DEFAULT_PLAYBACK_SPEED_SHORTS
+ : DEFAULT_PLAYBACK_SPEED;
+ BooleanSetting showToastSetting = isShorts
+ ? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED_TOAST
+ : Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST;
- // Prevent toast spamming if using the 0.05x adjustments.
- // Show exactly one toast after the user stops interacting with the speed menu.
- final long now = System.currentTimeMillis();
- lastTimeSpeedChanged = now;
+ if (rememberPlaybackSpeedLastSelectedSetting.get()) {
+ // With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
+ // then the menu will allow increasing without bounds but the max speed is
+ // still capped to under 8.0x.
+ playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
- final float finalPlaybackSpeed = playbackSpeed;
- Utils.runOnMainThreadDelayed(() -> {
- if (lastTimeSpeedChanged != now) {
- // The user made additional speed adjustments and this call is outdated.
- return;
- }
+ // Prevent toast spamming if using the 0.05x adjustments.
+ // Show exactly one toast after the user stops interacting with the speed menu.
+ final long now = System.currentTimeMillis();
+ lastTimeSpeedChanged = now;
- if (Settings.DEFAULT_PLAYBACK_SPEED.get() == finalPlaybackSpeed) {
- // User changed to a different speed and immediately changed back.
- // Or the user is going past 8.0x in the glitched out 0.05x menu.
- return;
- }
- Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed);
+ final float finalPlaybackSpeed = playbackSpeed;
+ Utils.runOnMainThreadDelayed(() -> {
+ if (lastTimeSpeedChanged != now) {
+ // The user made additional speed adjustments and this call is outdated.
+ return;
+ }
+ if (playbackSpeedSetting.get() == finalPlaybackSpeed) {
+ // User changed to a different speed and immediately changed back.
+ // Or the user is going past 8.0x in the glitched out 0.05x menu.
+ return;
+ }
+ playbackSpeedSetting.save(finalPlaybackSpeed);
- if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) {
- return;
- }
- Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
- }, TOAST_DELAY_MILLISECONDS);
+ if (showToastSetting.get()) {
+ Utils.showToastShort(str(isShorts ? "revanced_remember_playback_speed_toast_shorts" : "revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
+ }
+ }, TOAST_DELAY_MILLISECONDS);
+ }
+ } else if (!isShorts) {
+ lastSelectedPlaybackSpeed = playbackSpeed;
}
} catch (Exception ex) {
Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex);
}
}
- private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
- return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId))
- ? 1.0f
- : Settings.DEFAULT_PLAYBACK_SPEED.get();
- }
-
- private static boolean isMusic(@Nullable String videoId) {
- if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) {
+ private static boolean isMusic() {
+ if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && !videoId.isEmpty()) {
try {
MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java
index 45d84038c..0ce60f14e 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java
@@ -1,6 +1,7 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
+import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import androidx.annotation.NonNull;
@@ -14,8 +15,10 @@ import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused")
public class VideoQualityPatch {
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
- private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
- private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
+ private static final IntegerSetting shortsQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS;
+ private static final IntegerSetting shortsQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI_SHORTS;
+ private static final IntegerSetting videoQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
+ private static final IntegerSetting videoQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
@NonNull
public static String videoId = "";
@@ -35,12 +38,11 @@ public class VideoQualityPatch {
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
- if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
- return;
- if (videoId.equals(newlyLoadedVideoId))
- return;
- videoId = newlyLoadedVideoId;
- setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
+ if (PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL &&
+ !videoId.equals(newlyLoadedVideoId)) {
+ videoId = newlyLoadedVideoId;
+ setVideoQuality(750);
+ }
}
/**
@@ -53,42 +55,62 @@ public class VideoQualityPatch {
);
}
- private static void setVideoQuality(final long delayMillis) {
- final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE
- ? mobileQualitySetting.get()
- : wifiQualitySetting.get();
+ private static void setVideoQuality(long delayMillis) {
+ boolean isShorts = isShortsActive();
+ IntegerSetting defaultQualitySetting = Utils.getNetworkType() == Utils.NetworkType.MOBILE
+ ? isShorts ? shortsQualityMobile : videoQualityMobile
+ : isShorts ? shortsQualityWifi : videoQualityWifi;
- if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
- return;
+ int defaultQuality = defaultQualitySetting.get();
- Utils.runOnMainThreadDelayed(() -> {
- final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
- Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
- VideoInformation.overrideVideoQuality(qualityToUseFinal);
- }, delayMillis
- );
+ if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
+ Utils.runOnMainThreadDelayed(() -> {
+ final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
+ Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
+ VideoInformation.overrideVideoQuality(qualityToUseFinal);
+ }, delayMillis
+ );
+ }
}
private static void userSelectedVideoQuality(final int defaultQuality) {
- if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get())
- return;
- if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
- return;
+ if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
+ final Utils.NetworkType networkType = Utils.getNetworkType();
+ String networkTypeMessage = networkType == Utils.NetworkType.MOBILE
+ ? str("revanced_remember_video_quality_mobile")
+ : str("revanced_remember_video_quality_wifi");
- final Utils.NetworkType networkType = Utils.getNetworkType();
+ if (isShortsActive()) {
+ if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED.get()) {
+ IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
+ ? shortsQualityMobile
+ : shortsQualityWifi;
- switch (networkType) {
- case NONE -> {
- Utils.showToastShort(str("revanced_remember_video_quality_none"));
- return;
+ defaultQualitySetting.save(defaultQuality);
+
+ if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST.get()) {
+ Utils.showToastShort(str(
+ "revanced_remember_video_quality_toast_shorts",
+ networkTypeMessage, (defaultQuality + "p")
+ ));
+ }
+ }
+ } else {
+ if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) {
+ IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
+ ? videoQualityMobile
+ : videoQualityWifi;
+
+ defaultQualitySetting.save(defaultQuality);
+
+ if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
+ Utils.showToastShort(str(
+ "revanced_remember_video_quality_toast",
+ networkTypeMessage, (defaultQuality + "p")
+ ));
+ }
+ }
}
- case MOBILE -> mobileQualitySetting.save(defaultQuality);
- default -> wifiQualitySetting.save(defaultQuality);
}
-
- if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
- return;
-
- Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p"));
}
}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt
index d48ca8b51..a5283ad3d 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/MusicRequest.kt
@@ -2,9 +2,13 @@ package app.revanced.extension.youtube.patches.video.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
-import app.revanced.extension.shared.patches.client.YouTubeAppClient
-import app.revanced.extension.shared.patches.client.YouTubeWebClient
-import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
+import app.revanced.extension.shared.innertube.client.YouTubeAppClient
+import app.revanced.extension.shared.innertube.client.YouTubeWebClient
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
+import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_CATEGORY
+import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@@ -124,12 +128,12 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
- val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
- PlayerRoutes.GET_PLAYLIST_PAGE,
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ GET_PLAYLIST_PAGE,
clientType
)
val requestBody =
- PlayerRoutes.createApplicationRequestBody(
+ createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = "RD$videoId"
@@ -168,12 +172,11 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
try {
- val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
- PlayerRoutes.GET_CATEGORY,
+ val connection = getInnerTubeResponseConnectionFromRoute(
+ GET_CATEGORY,
clientType
)
- val requestBody =
- PlayerRoutes.createWebInnertubeBody(clientType, videoId)
+ val requestBody = createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
index ca5ff0aaa..b3b23057c 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -115,7 +115,7 @@ public class ReturnYouTubeDislike {
private static final Rect middleSeparatorBounds;
/**
- * Left separator horizontal padding for Rolling Number layout.
+ * Horizontal padding between the left and middle separator.
*/
public static final int leftSeparatorShapePaddingPixels;
private static final ShapeDrawable leftSeparatorShape;
@@ -131,7 +131,7 @@ public class ReturnYouTubeDislike {
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
- leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, dp);
+ leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.4f, dp);
leftSeparatorShape = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds);
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 6ea0ffe24..1b0bed356 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -28,6 +28,8 @@ import app.revanced.extension.shared.settings.LongSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
+import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.DeArrowAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
@@ -40,6 +42,7 @@ import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.Fullscr
import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior;
+import app.revanced.extension.youtube.patches.swipe.SwipeControlsPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
@@ -148,7 +151,6 @@ public class Settings extends BaseSettings {
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", PatchStatus.SplashAnimation(), true);
- public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", TRUE, true);
public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
@@ -156,6 +158,8 @@ public class Settings extends BaseSettings {
public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true);
+ public static final BooleanSetting DISABLE_LAYOUT_UPDATES = new BooleanSetting("revanced_disable_layout_updates", false, true, "revanced_disable_layout_updates_user_dialog_message");
+ public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", FALSE, true, "revanced_disable_translucent_status_bar_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 StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));
@@ -179,12 +183,14 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_subscriptions_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true);
public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message");
- public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true, "revanced_enable_translucent_navigation_bar_user_dialog_message");
+ public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
// PreferenceScreen: General - Override buttons
- public static final BooleanSetting OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_playlist_download_button", FALSE);
- public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_download_button", FALSE);
+ public static final BooleanSetting OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_playlist_download_button", FALSE, true);
+ public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_download_button", FALSE, true);
+ public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER = new BooleanSetting("revanced_override_video_download_button_queue_manager", FALSE, true,
+ "revanced_queue_manager_user_dialog_message", parent(OVERRIDE_VIDEO_DOWNLOAD_BUTTON));
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST = new StringSetting("revanced_external_downloader_package_name_playlist", "com.deniscerri.ytdl");
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO = new StringSetting("revanced_external_downloader_package_name_video", "com.deniscerri.ytdl");
public static final BooleanSetting OVERRIDE_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_youtube_music_button", FALSE, true
@@ -332,7 +338,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC = new BooleanSetting("revanced_hide_player_flyout_menu_listen_with_youtube_music", TRUE);
// PreferenceScreen: Player - Fullscreen
- public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE);
+ public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true);
public static final BooleanSetting ENTER_FULLSCREEN = new BooleanSetting("revanced_enter_fullscreen", FALSE);
public static final EnumSetting EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED);
public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL));
@@ -394,6 +400,8 @@ public class Settings extends BaseSettings {
public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_overlay_button_copy_video_url_timestamp", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_MUTE_VOLUME = new BooleanSetting("revanced_overlay_button_mute_volume", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_overlay_button_external_downloader", FALSE);
+ public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER = new BooleanSetting("revanced_overlay_button_external_downloader_queue_manager", FALSE, true,
+ "revanced_queue_manager_user_dialog_message", parent(OVERLAY_BUTTON_EXTERNAL_DOWNLOADER));
public static final BooleanSetting OVERLAY_BUTTON_SPEED_DIALOG = new BooleanSetting("revanced_overlay_button_speed_dialog", FALSE);
public static final BooleanSetting OVERLAY_BUTTON_PLAY_ALL = new BooleanSetting("revanced_overlay_button_play_all", FALSE);
public static final EnumSetting OVERLAY_BUTTON_PLAY_ALL_TYPE = new EnumSetting<>("revanced_overlay_button_play_all_type", PlaylistIdPrefix.ALL_CONTENTS_WITH_TIME_DESCENDING);
@@ -490,12 +498,13 @@ public class Settings extends BaseSettings {
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true);
+ public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG = new BooleanSetting("revanced_shorts_custom_actions_speed_dialog", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true);
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true,
- parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
+ parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true,
- parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
+ parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
// Experimental Flags
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true);
@@ -515,9 +524,15 @@ public class Settings extends BaseSettings {
public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
+ public static final BooleanSetting SWIPE_OVERLAY_ALTERNATIVE_UI = new BooleanSetting("revanced_swipe_overlay_alternative_ui", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
+ public static final BooleanSetting SWIPE_SHOW_CIRCULAR_OVERLAY = new BooleanSetting("revanced_swipe_show_circular_overlay", FALSE, true,
+ new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
+ public static final BooleanSetting SWIPE_OVERLAY_MINIMAL_STYLE = new BooleanSetting("revanced_swipe_overlay_minimal_style", FALSE, true,
+ new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
+ public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
+ public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true,
+ new SwipeControlsPatch.SwipeOverlayTextSizeAvailability());
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
- public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
- public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
@@ -536,30 +551,38 @@ public class Settings extends BaseSettings {
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
- // PreferenceScreen: Video
- public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
- public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
- public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
+ // PreferenceScreen: Video - Codec
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
+ public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
+ public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
+
+ // PreferenceScreen: Video - Playback speed
+ public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
+ public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
+ public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
+ public static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS = new FloatSetting("revanced_default_playback_speed_shorts", -2.0f);
+ public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected", TRUE);
+ public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED));
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
- public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
- public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
+
+ // PreferenceScreen: Video - Video quality
+ public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
+ public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
+ public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS = new IntegerSetting("revanced_default_video_quality_mobile_shorts", -2, true);
+ public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI_SHORTS = new IntegerSetting("revanced_default_video_quality_wifi_shorts", -2, true);
+ public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected", TRUE);
+ public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED));
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
- // Experimental Flags
- public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
- public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
- public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE);
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
- public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
- public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
- public static final BooleanSetting REJECT_AV1_CODEC = new BooleanSetting("revanced_reject_av1_codec", FALSE, true);
+ public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
+ public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
// PreferenceScreen: Miscellaneous
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
@@ -611,24 +634,34 @@ public class Settings extends BaseSettings {
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
+ public static final FloatSetting SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
+ public static final FloatSetting SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
+ public static final FloatSetting SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
+ public static final FloatSetting SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
+ public static final FloatSetting SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
+ public static final FloatSetting SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
+ public static final FloatSetting SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
+ public static final FloatSetting SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
+ public static final FloatSetting SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
+ public static final FloatSetting SB_CATEGORY_UNSUBMITTED_OPACITY = new FloatSetting("sb_unsubmitted_opacity", 1.0f);
// SB Setting not exported
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
@@ -637,6 +670,16 @@ public class Settings extends BaseSettings {
static {
// region Migration initialized
+
+ // Old spoof versions that no longer work reliably.
+ String spoofAppVersionTarget = SPOOF_APP_VERSION_TARGET.get();
+ if (spoofAppVersionTarget.compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0) {
+ Utils.showToastShort(str("revanced_spoof_app_version_target_invalid_toast", spoofAppVersionTarget));
+ Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
+ Logger.printInfo(() -> "Resetting spoof app version target");
+ SPOOF_APP_VERSION_TARGET.resetToDefault();
+ }
+
// Categories were previously saved without a 'sb_' key prefix, so they need an additional adjustment.
Set> sbCategories = new HashSet<>(Arrays.asList(
SB_CATEGORY_SPONSOR,
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
index 5eab38ba7..6a2fd721f 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference;
import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams;
+import static app.revanced.extension.shared.settings.BaseSettings.SPOOF_STREAMING_DATA_TYPE;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
@@ -9,6 +10,7 @@ import static app.revanced.extension.shared.utils.Utils.getChildView;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.shared.utils.Utils.showToastShort;
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED;
+import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE;
@@ -31,6 +33,7 @@ import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
+import android.util.Pair;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -55,12 +58,17 @@ import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
+import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch;
+import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
+import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
+import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
+import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.ThemeUtils;
@@ -74,14 +82,19 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
@SuppressLint("SuspiciousIndentation")
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
- if (str == null) return;
- Setting> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
+ if (str == null) {
+ return;
+ }
- if (setting == null) return;
+ Setting> setting = Setting.getSettingFromPath(str);
+ if (setting == null) {
+ return;
+ }
Preference mPreference = findPreference(str);
-
- if (mPreference == null) return;
+ if (mPreference == null) {
+ return;
+ }
if (mPreference instanceof SwitchPreference switchPreference) {
BooleanSetting boolSetting = (BooleanSetting) setting;
@@ -108,9 +121,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else {
Setting.privateSetValueFromString(setting, listPreference.getValue());
}
- if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
- listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
- listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
+ if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
+ listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
+ listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
+ }
+ if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
+ listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
+ listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
}
if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting);
@@ -122,18 +139,11 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
ReVancedSettingsPreference.initializeReVancedSettings();
- if (settingImportInProgress) {
- return;
- }
-
- if (!showingUserDialogMessage) {
+ if (!settingImportInProgress && !showingUserDialogMessage) {
final Context context = getActivity();
- if (setting.userDialogMessage != null
- && mPreference instanceof SwitchPreference switchPreference
- && setting.defaultValue instanceof Boolean defaultValue
- && switchPreference.isChecked() != defaultValue) {
- showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
+ if (setting.userDialogMessage != null && !prefIsSetToDefault(mPreference, setting)) {
+ showSettingUserDialogConfirmation(context, mPreference, setting);
} else if (setting.rebootApp) {
showRestartDialog(context);
}
@@ -143,25 +153,56 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
}
};
- private void showSettingUserDialogConfirmation(Context context, SwitchPreference switchPreference, BooleanSetting setting) {
+ /**
+ * @return If the preference is currently set to the default value of the Setting.
+ */
+ private boolean prefIsSetToDefault(Preference pref, Setting> setting) {
+ Object defaultValue = setting.defaultValue;
+ if (pref instanceof SwitchPreference switchPref) {
+ return switchPref.isChecked() == (Boolean) defaultValue;
+ }
+ String defaultValueString = defaultValue.toString();
+ if (pref instanceof EditTextPreference editPreference) {
+ return editPreference.getText().equals(defaultValueString);
+ }
+ if (pref instanceof ListPreference listPref) {
+ return listPref.getValue().equals(defaultValueString);
+ }
+
+ throw new IllegalStateException("Must override method to handle "
+ + "preference type: " + pref.getClass());
+ }
+
+ private void showSettingUserDialogConfirmation(Context context, Preference pref, Setting> setting) {
Utils.verifyOnMainThread();
- showingUserDialogMessage = true;
- assert setting.userDialogMessage != null;
- new AlertDialog.Builder(context)
- .setTitle(str("revanced_extended_confirm_user_dialog_title"))
- .setMessage(setting.userDialogMessage.toString())
- .setPositiveButton(android.R.string.ok, (dialog, id) -> {
- if (setting.rebootApp) {
- showRestartDialog(context);
- }
- })
- .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
- switchPreference.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
- })
- .setOnDismissListener(dialog -> showingUserDialogMessage = false)
- .setCancelable(false)
- .show();
+ final StringRef userDialogMessage = setting.userDialogMessage;
+ if (context != null && userDialogMessage != null) {
+ showingUserDialogMessage = true;
+
+ new AlertDialog.Builder(context)
+ .setTitle(str("revanced_extended_confirm_user_dialog_title"))
+ .setMessage(userDialogMessage.toString())
+ .setPositiveButton(android.R.string.ok, (dialog, id) -> {
+ if (setting.rebootApp) {
+ showRestartDialog(context);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
+ // Restore whatever the setting was before the change.
+ if (setting instanceof BooleanSetting booleanSetting &&
+ pref instanceof SwitchPreference switchPreference) {
+ switchPreference.setChecked(booleanSetting.defaultValue);
+ } else if (setting instanceof EnumSetting> enumSetting &&
+ pref instanceof ListPreference listPreference) {
+ listPreference.setValue(enumSetting.defaultValue.toString());
+ updateListPreferenceSummary(listPreference, setting);
+ }
+ })
+ .setOnDismissListener(dialog -> showingUserDialogMessage = false)
+ .setCancelable(false)
+ .show();
+ }
}
static PreferenceManager mPreferenceManager;
@@ -197,6 +238,9 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
}
}
+ Integer targetSDKVersion = ExtendedUtils.getTargetSDKVersion(getContext().getPackageName());
+ boolean isEdgeToEdgeSupported = isSDKAbove(35) && targetSDKVersion != null && targetSDKVersion >= 35;
+
for (PreferenceScreen mPreferenceScreen : preferenceScreenMap.values()) {
mPreferenceScreen.setOnPreferenceClickListener(
preferenceScreen -> {
@@ -205,11 +249,24 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
.findViewById(android.R.id.content)
.getParent();
- // Fix required for Android 15
- if (isSDKAbove(35)) {
+ // Edge-to-edge is enforced if the following conditions are met:
+ // 1. targetSDK is 35 or greater (YouTube 19.44.39 or greater).
+ // 2. user is using Android 15 or greater.
+ //
+ // Related Issues:
+ // https://github.com/ReVanced/revanced-patches/issues/3976
+ // https://github.com/ReVanced/revanced-patches/issues/4606
+ //
+ // Docs:
+ // https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
+ //
+ // Since ReVanced Settings Activity do not use AndroidX libraries,
+ // You will need to manually fix the layout breakage caused by edge-to-edge.
+ if (isEdgeToEdgeSupported) {
rootView.setOnApplyWindowInsetsListener((v, insets) -> {
Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
- v.setPadding(0, statusInsets.top, 0, 0);
+ Insets navInsets = insets.getInsets(WindowInsets.Type.navigationBars());
+ v.setPadding(0, statusInsets.top, 0, navInsets.bottom);
return insets;
});
}
@@ -283,9 +340,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else if (preference instanceof EditTextPreference editTextPreference) {
editTextPreference.setText(setting.get().toString());
} else if (preference instanceof ListPreference listPreference) {
- if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
- listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
- listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
+ if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
+ listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
+ listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
+ }
+ if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
+ listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
+ listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
}
if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting);
@@ -298,6 +359,10 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
copyPreferences(getPreferenceScreen(), originalPreferenceScreen);
+
+ sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
+ sortPreferenceListMenu(Settings.SPOOF_STREAMING_DATA_LANGUAGE);
+ sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
} catch (Exception th) {
Logger.printException(() -> "Error during onCreate()", th);
}
@@ -312,9 +377,69 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
@Override
public void onDestroy() {
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
+ Utils.resetLocalizedContext();
super.onDestroy();
}
+ /**
+ * Sorts a preference list by menu entries, but preserves the first value as the first entry.
+ *
+ * @noinspection SameParameterValue
+ */
+ private static void sortListPreferenceByValues(ListPreference listPreference, int firstEntriesToPreserve) {
+ CharSequence[] entries = listPreference.getEntries();
+ CharSequence[] entryValues = listPreference.getEntryValues();
+ final int entrySize = entries.length;
+
+ if (entrySize != entryValues.length) {
+ // Xml array declaration has a missing/extra entry.
+ throw new IllegalStateException();
+ }
+
+ // Since the text of Preference is Spanned, CharSequence#toString() should not be used.
+ // If CharSequence#toString() is used, Spanned styling, such as HTML syntax, will be broken.
+ List> firstPairs = new ArrayList<>(firstEntriesToPreserve);
+ List> pairsToSort = new ArrayList<>(entrySize);
+
+ for (int i = 0; i < entrySize; i++) {
+ Pair pair = new Pair<>(entries[i], entryValues[i]);
+ if (i < firstEntriesToPreserve) {
+ firstPairs.add(pair);
+ } else {
+ pairsToSort.add(pair);
+ }
+ }
+
+ pairsToSort.sort((pair1, pair2)
+ -> pair1.first.toString().compareToIgnoreCase(pair2.first.toString()));
+
+ CharSequence[] sortedEntries = new CharSequence[entrySize];
+ CharSequence[] sortedEntryValues = new CharSequence[entrySize];
+
+ int i = 0;
+ for (Pair pair : firstPairs) {
+ sortedEntries[i] = pair.first;
+ sortedEntryValues[i] = pair.second;
+ i++;
+ }
+
+ for (Pair pair : pairsToSort) {
+ sortedEntries[i] = pair.first;
+ sortedEntryValues[i] = pair.second;
+ i++;
+ }
+
+ listPreference.setEntries(sortedEntries);
+ listPreference.setEntryValues(sortedEntryValues);
+ }
+
+ private void sortPreferenceListMenu(EnumSetting> setting) {
+ Preference preference = findPreference(setting.key);
+ if (preference instanceof ListPreference languagePreference) {
+ sortListPreferenceByValues(languagePreference, 1);
+ }
+ }
+
/**
* Recursively stores all preferences and their dependencies grouped by their parent PreferenceGroup.
*
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
index c3c22520b..e4cc256bf 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
@@ -2,7 +2,6 @@ package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
-import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
import android.preference.Preference;
import android.preference.SwitchPreference;
@@ -43,11 +42,11 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
enableDisablePreferences();
AmbientModePreferenceLinks();
- ExternalDownloaderPreferenceLinks();
FullScreenPanelPreferenceLinks();
NavigationPreferenceLinks();
RYDPreferenceLinks();
SeekBarPreferenceLinks();
+ ShortsPreferenceLinks();
SpeedOverlayPreferenceLinks();
QuickActionsPreferenceLinks();
TabletLayoutLinks();
@@ -65,18 +64,6 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
);
}
- /**
- * Enable/Disable Preference for External downloader settings
- */
- private static void ExternalDownloaderPreferenceLinks() {
- // Override download button will not work if spoofed with YouTube 18.24.xx or earlier.
- enableDisablePreferences(
- isSpoofingToLessThan("18.24.00"),
- Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON,
- Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON
- );
- }
-
/**
* Enable/Disable Preferences not working in tablet layout
*/
@@ -200,6 +187,19 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
);
}
+ /**
+ * Enable/Disable Preference related to Shorts settings
+ */
+ private static void ShortsPreferenceLinks() {
+ if (!PatchStatus.RememberPlaybackSpeed()) {
+ enableDisablePreferences(
+ true,
+ Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG
+ );
+ Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG.save(false);
+ }
+ }
+
/**
* Enable/Disable Preference related to Speed overlay settings
*/
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SegmentCategoryListPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SegmentCategoryListPreference.java
index b94ee3135..68b8727a5 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SegmentCategoryListPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SegmentCategoryListPreference.java
@@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
+import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
import android.app.AlertDialog;
import android.content.Context;
@@ -12,41 +13,51 @@ import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.TableLayout;
-import android.widget.TableRow;
+import android.widget.GridLayout;
import android.widget.TextView;
+import java.util.Locale;
import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
-import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
@SuppressWarnings({"unused", "deprecation"})
public class SegmentCategoryListPreference extends ListPreference {
- private SegmentCategory mCategory;
- private EditText mEditText;
- private int mClickedDialogEntryIndex;
+ private SegmentCategory category;
+ private TextView colorDotView;
+ private EditText colorEditText;
+ private EditText opacityEditText;
+ /**
+ * #RRGGBB
+ */
+ private int categoryColor;
+ /**
+ * [0, 1]
+ */
+ private float categoryOpacity;
+ private int selectedDialogEntryIndex;
private void init() {
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
- final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT;
- mCategory = Objects.requireNonNull(segmentCategory);
+ category = Objects.requireNonNull(segmentCategory);
+
// Edit: Using preferences to sync together multiple pieces
- // of code together is messy and should be rethought.
+ // of code is messy and should be rethought.
setKey(segmentCategory.behaviorSetting.key);
setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
+ final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
setEntries(isHighlightCategory
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
: CategoryBehaviour.getBehaviorDescriptions());
setEntryValues(isHighlightCategory
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
: CategoryBehaviour.getBehaviorKeyValues());
- updateTitle();
+
+ updateTitleFromCategory();
}
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@@ -73,28 +84,41 @@ public class SegmentCategoryListPreference extends ListPreference {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
- super.onPrepareDialogBuilder(builder);
+
+ categoryColor = category.getColorNoOpacity();
+ categoryOpacity = category.getOpacity();
Context context = builder.getContext();
- TableLayout table = new TableLayout(context);
- table.setOrientation(LinearLayout.HORIZONTAL);
- table.setPadding(70, 0, 150, 0);
-
- TableRow row = new TableRow(context);
+ GridLayout gridLayout = new GridLayout(context);
+ gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout.
+ gridLayout.setColumnCount(3);
+ gridLayout.setRowCount(2);
+ GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
+ gridParams.rowSpec = GridLayout.spec(0); // First row.
+ gridParams.columnSpec = GridLayout.spec(0); // First column.
TextView colorTextLabel = new TextView(context);
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
- row.addView(colorTextLabel);
+ colorTextLabel.setLayoutParams(gridParams);
+ gridLayout.addView(colorTextLabel);
- TextView colorDotView = new TextView(context);
- colorDotView.setText(mCategory.getCategoryColorDot());
- colorDotView.setPadding(30, 0, 30, 0);
- row.addView(colorDotView);
+ gridParams = new GridLayout.LayoutParams();
+ gridParams.rowSpec = GridLayout.spec(0); // First row.
+ gridParams.columnSpec = GridLayout.spec(1); // Second column.
+ gridParams.setMargins(0, 0, 10, 0);
+ colorDotView = new TextView(context);
+ colorDotView.setLayoutParams(gridParams);
+ gridLayout.addView(colorDotView);
+ updateCategoryColorDot();
- mEditText = new EditText(context);
- mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
- mEditText.setText(mCategory.colorString());
- mEditText.addTextChangedListener(new TextWatcher() {
+ gridParams = new GridLayout.LayoutParams();
+ gridParams.rowSpec = GridLayout.spec(0); // First row.
+ gridParams.columnSpec = GridLayout.spec(2); // Third column.
+ colorEditText = new EditText(context);
+ colorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
+ colorEditText.setTextLocale(Locale.US);
+ colorEditText.setText(category.getColorString());
+ colorEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@@ -104,44 +128,111 @@ public class SegmentCategoryListPreference extends ListPreference {
}
@Override
- public void afterTextChanged(Editable s) {
+ public void afterTextChanged(Editable edit) {
try {
- String colorString = s.toString();
+ String colorString = edit.toString();
+ final int colorStringLength = colorString.length();
+
if (!colorString.startsWith("#")) {
- s.insert(0, "#"); // recursively calls back into this method
+ edit.insert(0, "#"); // Recursively calls back into this method.
return;
}
- if (colorString.length() > 7) {
- s.delete(7, colorString.length());
+
+ final int maxColorStringLength = 7; // #RRGGBB
+ if (colorStringLength > maxColorStringLength) {
+ edit.delete(maxColorStringLength, colorStringLength);
return;
}
- final int color = Color.parseColor(colorString);
- colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
+
+ categoryColor = Color.parseColor(colorString);
+ updateCategoryColorDot();
} catch (IllegalArgumentException ex) {
- // ignore
+ // Ignore.
}
}
});
- mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
- row.addView(mEditText);
+ colorEditText.setLayoutParams(gridParams);
+ gridLayout.addView(colorEditText);
- table.addView(row);
- builder.setView(table);
- builder.setTitle(mCategory.title.toString());
+ gridParams = new GridLayout.LayoutParams();
+ gridParams.rowSpec = GridLayout.spec(1); // Second row.
+ gridParams.columnSpec = GridLayout.spec(0, 1); // First and second column.
+ TextView opacityLabel = new TextView(context);
+ opacityLabel.setText(str("revanced_sb_color_opacity_label"));
+ opacityLabel.setLayoutParams(gridParams);
+ gridLayout.addView(opacityLabel);
+
+ gridParams = new GridLayout.LayoutParams();
+ gridParams.rowSpec = GridLayout.spec(1); // Second row.
+ gridParams.columnSpec = GridLayout.spec(2); // Third column.
+ opacityEditText = new EditText(context);
+ opacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
+ opacityEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable edit) {
+ try {
+ String editString = edit.toString();
+ final int opacityStringLength = editString.length();
+
+ final int maxOpacityStringLength = 4; // [0.00, 1.00]
+ if (opacityStringLength > maxOpacityStringLength) {
+ edit.delete(maxOpacityStringLength, opacityStringLength);
+ return;
+ }
+
+ final float opacity = opacityStringLength == 0
+ ? 0
+ : Float.parseFloat(editString);
+ if (opacity < 0) {
+ categoryOpacity = 0;
+ edit.replace(0, opacityStringLength, "0");
+ return;
+ } else if (opacity > 1.0f) {
+ categoryOpacity = 1;
+ edit.replace(0, opacityStringLength, "1.0");
+ return;
+ } else if (!editString.endsWith(".")) {
+ // Ignore "0." and "1." until the user finishes entering a valid number.
+ categoryOpacity = opacity;
+ }
+
+ updateCategoryColorDot();
+ } catch (NumberFormatException ex) {
+ // Should never happen.
+ Logger.printException(() -> "Could not parse opacity string", ex);
+ }
+ }
+ });
+ opacityEditText.setLayoutParams(gridParams);
+ gridLayout.addView(opacityEditText);
+ updateOpacityText();
+
+ builder.setView(gridLayout);
+ builder.setTitle(category.title.toString());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
try {
- mCategory.resetColor();
- updateTitle();
+ category.resetColorAndOpacity();
+ updateTitleFromCategory();
Utils.showToastShort(str("revanced_sb_color_reset"));
} catch (Exception ex) {
Logger.printException(() -> "setNeutralButton failure", ex);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
- mClickedDialogEntryIndex = findIndexOfValue(getValue());
- builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
+
+ selectedDialogEntryIndex = findIndexOfValue(getValue());
+ builder.setSingleChoiceItems(getEntries(), selectedDialogEntryIndex,
+ (dialog, which) -> selectedDialogEntryIndex = which);
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
@@ -150,31 +241,50 @@ public class SegmentCategoryListPreference extends ListPreference {
@Override
protected void onDialogClosed(boolean positiveResult) {
try {
- if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
- String value = getEntryValues()[mClickedDialogEntryIndex].toString();
+ if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
+ String value = getEntryValues()[selectedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
- mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
+ category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
SegmentCategory.updateEnabledCategories();
}
- String colorString = mEditText.getText().toString();
try {
- if (!colorString.equals(mCategory.colorString())) {
- mCategory.setColor(colorString);
+ String colorString = colorEditText.getText().toString();
+ if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) {
+ category.setColor(colorString);
+ category.setOpacity(categoryOpacity);
Utils.showToastShort(str("revanced_sb_color_changed"));
}
} catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_sb_color_invalid"));
}
- updateTitle();
+
+ updateTitleFromCategory();
}
} catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex);
}
}
- private void updateTitle() {
- setTitle(mCategory.getTitleWithColorDot());
- setEnabled(Settings.SB_ENABLED.get());
+ private void applyOpacityToCategoryColor() {
+ categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
+ }
+
+ private void updateTitleFromCategory() {
+ categoryColor = category.getColorNoOpacity();
+ categoryOpacity = category.getOpacity();
+ applyOpacityToCategoryColor();
+
+ setTitle(category.getTitleWithColorDot(categoryColor));
+ }
+
+ private void updateCategoryColorDot() {
+ applyOpacityToCategoryColor();
+
+ colorDotView.setText(SegmentCategory.getCategoryColorDot(categoryColor));
+ }
+
+ private void updateOpacityText() {
+ opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
}
}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockSettingsPreference.java
index d3107381f..e98b55d65 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockSettingsPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockSettingsPreference.java
@@ -233,6 +233,7 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
statsCategory = new PreferenceCategory(mActivity);
statsCategory.setLayoutResource(preferencesCategoryLayout);
statsCategory.setTitle(str("revanced_sb_stats"));
+ statsCategory.setEnabled(Settings.SB_ENABLED.get());
mPreferenceScreen.addPreference(statsCategory);
fetchAndDisplayStats();
@@ -261,7 +262,6 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
final String key = category.keyValue;
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
- segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
}
}
} catch (Exception ex) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
index 3d18b32b2..cad23ab5f 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
@@ -58,6 +58,10 @@ public final class RootView {
return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get();
}
+ public static boolean isShortsActive() {
+ return ShortsPlayerState.getCurrent().isOpen();
+ }
+
/**
* Get current BrowseId.
* Rest of the implementation added by patch.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt
index e3e56c58e..76f201265 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt
@@ -48,4 +48,12 @@ enum class ShortsPlayerState {
fun isClosed(): Boolean {
return this == CLOSED
}
+
+ /**
+ * Check if the shorts player is [OPEN].
+ * Useful for checking if a shorts player is open.
+ */
+ fun isOpen(): Boolean {
+ return this == OPEN
+ }
}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
index d4228f3ec..11a588fba 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java
@@ -139,7 +139,7 @@ public class SponsorBlockSettings {
for (SegmentCategory category : categories) {
JSONObject categoryObject = new JSONObject();
String categoryKey = category.keyValue;
- categoryObject.put("color", category.colorString());
+ categoryObject.put("color", category.getColorString());
barTypesObject.put(categoryKey, categoryObject);
if (category.behaviour != CategoryBehaviour.IGNORE) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java
index 56dc52977..bee1d6a98 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java
@@ -6,7 +6,12 @@ import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
-import android.text.Html;
+import android.graphics.Color;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
import android.widget.EditText;
import androidx.annotation.NonNull;
@@ -32,11 +37,9 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController
/**
* Not thread safe. All fields/methods must be accessed from the main thread.
- *
- * @noinspection deprecation
*/
public class SponsorBlockUtils {
- private static final String LOCKED_COLOR = "#FFC83D";
+ private static final int LOCKED_COLOR = Color.parseColor("#FFC83D");
private static final String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss";
private static final Pattern manualEditTimePattern
@@ -162,28 +165,34 @@ public class SponsorBlockUtils {
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
: SegmentVote.values();
- CharSequence[] items = new CharSequence[voteOptions.length];
+ final int voteOptionsLength = voteOptions.length;
+ final boolean userIsVip = Settings.SB_USER_IS_VIP.get();
+ CharSequence[] items = new CharSequence[voteOptionsLength];
- for (int i = 0; i < voteOptions.length; i++) {
+ for (int i = 0; i < voteOptionsLength; i++) {
SegmentVote voteOption = voteOptions[i];
- String title = voteOption.title.toString();
- if (Settings.SB_USER_IS_VIP.get() && segment.isLocked && voteOption.shouldHighlight) {
- items[i] = Html.fromHtml(String.format("%s ", LOCKED_COLOR, title));
- } else {
- items[i] = title;
+ CharSequence title = voteOption.title.toString();
+ if (userIsVip && segment.isLocked && voteOption.highlightIfVipAndVideoIsLocked) {
+ SpannableString coloredTitle = new SpannableString(title);
+ coloredTitle.setSpan(new ForegroundColorSpan(LOCKED_COLOR),
+ 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ title = coloredTitle;
}
+ items[i] = title;
}
- new AlertDialog.Builder(context)
- .setItems(items, (dialog1, which1) -> {
- SegmentVote voteOption = voteOptions[which1];
- switch (voteOption) {
- case UPVOTE, DOWNVOTE ->
- SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
- case CATEGORY_CHANGE -> onNewCategorySelect(segment, context);
- }
- })
- .show();
+ new AlertDialog.Builder(context).setItems(items, (dialog1, which1) -> {
+ SegmentVote voteOption = voteOptions[which1];
+ switch (voteOption) {
+ case UPVOTE:
+ case DOWNVOTE:
+ SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
+ break;
+ case CATEGORY_CHANGE:
+ onNewCategorySelect(segment, context);
+ break;
+ }
+ }).show();
} catch (Exception ex) {
Logger.printException(() -> "segmentVoteClickListener failure", ex);
}
@@ -287,22 +296,33 @@ public class SponsorBlockUtils {
if (segment.category == SegmentCategory.UNSUBMITTED) {
continue;
}
- StringBuilder htmlBuilder = new StringBuilder();
- htmlBuilder.append(String.format("⬤ %s ",
- segment.category.color, segment.category.title));
- htmlBuilder.append(formatSegmentTime(segment.start));
- if (segment.category != SegmentCategory.HIGHLIGHT) {
- htmlBuilder.append(" to ").append(formatSegmentTime(segment.end));
+
+ SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
+
+ spannableBuilder.append(segment.category.getTitleWithColorDot());
+ spannableBuilder.append('\n');
+
+ String startTime = formatSegmentTime(segment.start);
+ if (segment.category == SegmentCategory.HIGHLIGHT) {
+ spannableBuilder.append(startTime);
+ } else {
+ String toFromString = str("revanced_sb_vote_segment_time_to_from",
+ startTime, formatSegmentTime(segment.end));
+ spannableBuilder.append(toFromString);
}
- htmlBuilder.append(" ");
- if (i + 1 != numberOfSegments) // prevents trailing new line after last segment
- htmlBuilder.append(" ");
- titles[i] = Html.fromHtml(htmlBuilder.toString());
+
+ if (i + 1 != numberOfSegments) {
+ // prevents trailing new line after last segment
+ spannableBuilder.append('\n');
+ }
+
+ spannableBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
+ 0, spannableBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ titles[i] = spannableBuilder;
}
- new AlertDialog.Builder(context)
- .setItems(titles, segmentVoteClickListener)
- .show();
+ new AlertDialog.Builder(context).setItems(titles, segmentVoteClickListener).show();
} catch (Exception ex) {
Logger.printException(() -> "onVotingClicked failure", ex);
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java
index 3d1e90f66..c16fbe3d2 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java
@@ -3,30 +3,41 @@ package app.revanced.extension.youtube.sponsorblock.objects;
import static app.revanced.extension.shared.utils.StringRef.sf;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR;
+import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_OPACITY;
import android.graphics.Color;
import android.graphics.Paint;
-import android.text.Html;
-import android.text.Spanned;
+import android.text.Spannable;
+import android.text.SpannableString;
import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -34,45 +45,46 @@ import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
-@SuppressWarnings({"deprecation", "StaticFieldLeak"})
+@SuppressWarnings("StaticFieldLeak")
public enum SegmentCategory {
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
- SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR),
+ SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR, SB_CATEGORY_SPONSOR_OPACITY),
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
- SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR),
+ SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR, SB_CATEGORY_SELF_PROMO_OPACITY),
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
- SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR),
+ SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR, SB_CATEGORY_INTERACTION_OPACITY),
/**
* Unique category that is treated differently than the rest.
*/
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
- SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR),
+ SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR, SB_CATEGORY_HIGHLIGHT_OPACITY),
INTRO("intro", sf("revanced_sb_segments_intro"),
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
- SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR),
+ SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR, SB_CATEGORY_INTRO_OPACITY),
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
- SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR),
+ SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR, SB_CATEGORY_OUTRO_OPACITY),
PREVIEW("preview", sf("revanced_sb_segments_preview"),
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
- SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR),
+ SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR, SB_CATEGORY_PREVIEW_OPACITY),
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
- SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR),
+ SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR, SB_CATEGORY_FILLER_OPACITY),
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
- SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR),
+ SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY),
UNSUBMITTED("unsubmitted", StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
- SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR),
- ;
+ SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR, SB_CATEGORY_UNSUBMITTED_OPACITY);
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
@@ -111,12 +123,10 @@ public enum SegmentCategory {
mValuesMap.put(value.keyValue, value);
}
- @NonNull
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted;
}
- @NonNull
public static SegmentCategory[] categoriesWithoutHighlights() {
return categoriesWithoutHighlights;
}
@@ -127,7 +137,7 @@ public enum SegmentCategory {
}
/**
- * Must be called if behavior of any category is changed
+ * Must be called if behavior of any category is changed.
*/
public static void updateEnabledCategories() {
Utils.verifyOnMainThread();
@@ -154,30 +164,32 @@ public enum SegmentCategory {
updateEnabledCategories();
}
- @NonNull
- public final String keyValue;
- @NonNull
- public final StringSetting behaviorSetting;
- @NonNull
- private final StringSetting colorSetting;
+ public static int applyOpacityToColor(int color, float opacity) {
+ if (opacity < 0 || opacity > 1.0f) {
+ throw new IllegalArgumentException("Invalid opacity: " + opacity);
+ }
+ final int opacityInt = (int) (255 * opacity);
+ return (color & 0x00FFFFFF) | (opacityInt << 24);
+ }
+
+ public final String keyValue;
+ public final StringSetting behaviorSetting; // TODO: Replace with EnumSetting.
+ private final StringSetting colorSetting;
+ private final FloatSetting opacitySetting;
- @NonNull
public final StringRef title;
/**
* Skip button text, if the skip occurs in the first quarter of the video
*/
- @NonNull
public final StringRef skipButtonTextBeginning;
/**
* Skip button text, if the skip occurs in the middle half of the video
*/
- @NonNull
public final StringRef skipButtonTextMiddle;
/**
* Skip button text, if the skip occurs in the last quarter of the video
*/
- @NonNull
public final StringRef skipButtonTextEnd;
/**
* Skipped segment toast, if the skip occurred in the first quarter of the video
@@ -198,10 +210,7 @@ public enum SegmentCategory {
@NonNull
public final Paint paint;
- /**
- * Value must be changed using {@link #setColor(String)}.
- */
- public int color;
+ private int color;
/**
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
@@ -213,17 +222,20 @@ public enum SegmentCategory {
SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonText,
StringRef skippedToastText,
- StringSetting behavior, StringSetting color) {
+ StringSetting behavior,
+ StringSetting color, FloatSetting opacity) {
this(keyValue, title,
skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText,
- behavior, color);
+ behavior,
+ color, opacity);
}
SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
- StringSetting behavior, StringSetting color) {
+ StringSetting behavior,
+ StringSetting color, FloatSetting opacity) {
this.keyValue = Objects.requireNonNull(keyValue);
this.title = Objects.requireNonNull(title);
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
@@ -234,6 +246,7 @@ public enum SegmentCategory {
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviorSetting = Objects.requireNonNull(behavior);
this.colorSetting = Objects.requireNonNull(color);
+ this.opacitySetting = Objects.requireNonNull(opacity);
this.paint = new Paint();
loadFromSettings();
}
@@ -250,11 +263,14 @@ public enum SegmentCategory {
this.behaviour = savedBehavior;
String colorString = colorSetting.get();
+ final float opacity = opacitySetting.get();
try {
setColor(colorString);
+ setOpacity(opacity);
} catch (Exception ex) {
- Logger.printException(() -> "Invalid color: " + colorString, ex);
+ Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
colorSetting.resetToDefault();
+ opacitySetting.resetToDefault();
loadFromSettings();
}
}
@@ -264,45 +280,77 @@ public enum SegmentCategory {
this.behaviorSetting.save(behaviour.reVancedKeyValue);
}
- /**
- * @return HTML color format string
- */
- @NonNull
- public String colorString() {
- return String.format("#%06X", color);
- }
-
- public void setColor(@NonNull String colorString) throws IllegalArgumentException {
- final int color = Color.parseColor(colorString) & 0xFFFFFF;
- this.color = color;
+ private void updateColor() {
+ color = applyOpacityToColor(color, opacitySetting.get());
paint.setColor(color);
- paint.setAlpha(255);
- colorSetting.save(colorString); // Save after parsing.
}
- public void resetColor() {
+ /**
+ * @param opacity Segment color opacity between [0, 1].
+ */
+ public void setOpacity(float opacity) throws IllegalArgumentException {
+ if (opacity < 0 || opacity > 1) {
+ throw new IllegalArgumentException("Invalid opacity: " + opacity);
+ }
+
+ opacitySetting.save(opacity);
+ updateColor();
+ }
+
+ public float getOpacity() {
+ return opacitySetting.get();
+ }
+
+ public void resetColorAndOpacity() {
setColor(colorSetting.defaultValue);
+ setOpacity(opacitySetting.defaultValue);
}
- @NonNull
- private static String getCategoryColorDotHTML(int color) {
- color &= 0xFFFFFF;
- return String.format("⬤ ", color);
+ /**
+ * @param colorString Segment color with #RRGGBB format.
+ */
+ public void setColor(String colorString) throws IllegalArgumentException {
+ color = Color.parseColor(colorString);
+ colorSetting.save(colorString);
+
+ updateColor();
}
- @NonNull
- public static Spanned getCategoryColorDot(int color) {
- return Html.fromHtml(getCategoryColorDotHTML(color));
+ /**
+ * @return Integer color of #RRGGBB format.
+ */
+ public int getColorNoOpacity() {
+ return color & 0x00FFFFFF;
}
- @NonNull
- public Spanned getCategoryColorDot() {
+ /**
+ * @return Hex color string of #RRGGBB format with no opacity level.
+ */
+ public String getColorString() {
+ return String.format(Locale.US, "#%06X", getColorNoOpacity());
+ }
+
+ private static SpannableString getCategoryColorDotSpan(String text, int color) {
+ SpannableString dotSpan = new SpannableString('⬤' + text);
+ dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return dotSpan;
+ }
+
+ public static SpannableString getCategoryColorDot(int color) {
+ return getCategoryColorDotSpan("", color);
+ }
+
+ public SpannableString getCategoryColorDot() {
return getCategoryColorDot(color);
}
- @NonNull
- public Spanned getTitleWithColorDot() {
- return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title);
+ public SpannableString getTitleWithColorDot(int categoryColor) {
+ return getCategoryColorDotSpan(" " + title, categoryColor);
+ }
+
+ public SpannableString getTitleWithColorDot() {
+ return getTitleWithColorDot(color);
}
/**
@@ -310,7 +358,6 @@ public enum SegmentCategory {
* @param videoLength length of the video
* @return the skip button text
*/
- @NonNull
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
return (this == SegmentCategory.HIGHLIGHT)
@@ -319,7 +366,7 @@ public enum SegmentCategory {
}
if (videoLength == 0) {
- return skipButtonTextBeginning; // video is still loading. Assume it's the beginning
+ return skipButtonTextBeginning; // Video is still loading. Assume it's the beginning.
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
@@ -335,10 +382,9 @@ public enum SegmentCategory {
* @param videoLength length of the video
* @return 'skipped segment' toast message
*/
- @NonNull
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
- return skippedToastBeginning; // video is still loading. Assume it's the beginning
+ return skippedToastBeginning; // Video is still loading. Assume it's the beginning.
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java
index 51208c1cc..0bd5aa435 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java
@@ -24,12 +24,15 @@ public class SponsorSegment implements Comparable {
@NonNull
public final StringRef title;
public final int apiVoteType;
- public final boolean shouldHighlight;
+ /**
+ * If the option should be highlighted for VIP users.
+ */
+ public final boolean highlightIfVipAndVideoIsLocked;
- SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) {
+ SegmentVote(@NonNull StringRef title, int apiVoteType, boolean highlightIfVipAndVideoIsLocked) {
this.title = title;
this.apiVoteType = apiVoteType;
- this.shouldHighlight = shouldHighlight;
+ this.highlightIfVipAndVideoIsLocked = highlightIfVipAndVideoIsLocked;
}
}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt
index 3d3c5d83d..dbe9cf385 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt
@@ -125,7 +125,7 @@ class SwipeControlsConfigurationProvider(
* get the background color for text on the overlay, as a color int
*/
val overlayTextBackgroundColor: Int
- get() = Color.argb(Settings.SWIPE_OVERLAY_BACKGROUND_ALPHA.get(), 0, 0, 0)
+ get() = overlayBackgroundOpacity
/**
* get the foreground color for text on the overlay, as a color int
@@ -133,6 +133,59 @@ class SwipeControlsConfigurationProvider(
val overlayForegroundColor: Int
get() = Color.WHITE
+ /**
+ * Gets the opacity value (0-100%) is converted to an alpha value (0-255) for transparency.
+ * If the opacity value is out of range, it resets to the default and displays a warning message.
+ */
+ val overlayBackgroundOpacity: Int
+ get() {
+ var opacity = validateValue(
+ Settings.SWIPE_OVERLAY_BACKGROUND_OPACITY,
+ 0,
+ 100,
+ "revanced_swipe_overlay_background_opacity_invalid_toast"
+ )
+
+ opacity = opacity * 255 / 100
+ return Color.argb(opacity, 0, 0, 0)
+ }
+
+ /**
+ * The color of the progress overlay.
+ */
+ val overlayProgressColor: Int
+ get() = 0xBFFFFFFF.toInt()
+
+ /**
+ * The color used for the background of the progress overlay fill.
+ */
+ val overlayFillBackgroundPaint: Int
+ get() = 0x80D3D3D3.toInt()
+
+ /**
+ * The color used for the text and icons in the overlay.
+ */
+ val overlayTextColor: Int
+ get() = Color.WHITE
+
+ /**
+ * A flag that determines whether to use the alternate UI.
+ */
+ val isAlternativeUI: Boolean
+ get() = Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get()
+
+ /**
+ * A flag that determines if the overlay should only show the icon.
+ */
+ val overlayShowOverlayMinimalStyle: Boolean
+ get() = isAlternativeUI && Settings.SWIPE_OVERLAY_MINIMAL_STYLE.get()
+
+ /**
+ * A flag that determines if the progress bar should be circular.
+ */
+ val isCircularProgressBar: Boolean
+ get() = isAlternativeUI && Settings.SWIPE_SHOW_CIRCULAR_OVERLAY.get()
+
// endregion
// region behaviour
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt
index 3ebebc252..f2b5e4aa4 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt
@@ -24,7 +24,7 @@ import java.lang.ref.WeakReference
* The main controller for volume and brightness swipe controls.
* note that the superclass is overwritten to the superclass of the MainActivity at patch time
*
- * @smali Lapp/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity;
+ * @smali Lapp/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity;
*/
class SwipeControlsHostActivity : Activity() {
/**
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt
index 6d2cef606..f3ba568b5 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt
@@ -1,14 +1,18 @@
package app.revanced.extension.youtube.swipecontrols.views
+import android.annotation.SuppressLint
import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Handler
import android.os.Looper
+import android.util.AttributeSet
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.View
-import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import app.revanced.extension.shared.utils.ResourceUtils.ResourceType
@@ -17,6 +21,7 @@ import app.revanced.extension.shared.utils.StringRef.str
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider
import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay
import app.revanced.extension.youtube.swipecontrols.misc.applyDimension
+import kotlin.math.min
import kotlin.math.round
/**
@@ -33,36 +38,82 @@ class SwipeControlsOverlayLayout(
*/
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
- private val feedbackTextView: TextView
private val autoBrightnessIcon: Drawable
+ private val lowBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_low")
+ private val mediumBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_medium")
+ private val highBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_high")
+ private val fullBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_full")
private val manualBrightnessIcon: Drawable
private val mutedVolumeIcon: Drawable
+ private val lowVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_low")
private val normalVolumeIcon: Drawable
+ private val feedbackTextView: TextView
+ private val fullVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_high")
- private fun getDrawable(name: String, width: Int, height: Int): Drawable {
- return resources.getDrawable(
+ private val circularProgressView: CircularProgressView = CircularProgressView(
+ context,
+ config.overlayBackgroundOpacity,
+ config.overlayShowOverlayMinimalStyle,
+ config.overlayProgressColor,
+ config.overlayFillBackgroundPaint,
+ config.overlayTextColor
+ ).apply {
+ layoutParams = LayoutParams(300, 300).apply {
+ addRule(CENTER_IN_PARENT, TRUE)
+ }
+ visibility = GONE // Initially hidden
+ }
+ private val horizontalProgressView: HorizontalProgressView
+
+ private val isAlternativeUI: Boolean = config.isAlternativeUI
+
+ private fun getDrawable(name: String, width: Int? = null, height: Int? = null): Drawable {
+ val drawable = resources.getDrawable(
getIdentifier(name, ResourceType.DRAWABLE, context),
- context.theme
- ).apply {
- setTint(config.overlayForegroundColor)
- setBounds(
+ context.theme,
+ )
+
+ if (width != null && height != null) {
+ drawable.setTint(config.overlayForegroundColor)
+ drawable.setBounds(
0,
0,
width,
height,
)
+ } else {
+ drawable.setTint(config.overlayTextColor)
}
+ return drawable
}
init {
+ // Initialize horizontal progress bar
+ val screenWidth = resources.displayMetrics.widthPixels
+ val layoutWidth = (screenWidth * 2 / 3).toInt() // 2/3 of screen width
+ horizontalProgressView = HorizontalProgressView(
+ context,
+ config.overlayBackgroundOpacity,
+ config.overlayShowOverlayMinimalStyle,
+ config.overlayProgressColor,
+ config.overlayFillBackgroundPaint,
+ config.overlayTextColor
+ ).apply {
+ layoutParams = LayoutParams(layoutWidth, 100).apply {
+ addRule(CENTER_HORIZONTAL)
+ topMargin = 40 // Top margin
+ }
+ visibility = GONE // Initially hidden
+ }
+
// init views
val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
feedbackTextView = TextView(context).apply {
layoutParams = LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT,
).apply {
addRule(CENTER_IN_PARENT, TRUE)
setPadding(
@@ -81,19 +132,36 @@ class SwipeControlsOverlayLayout(
compoundDrawablePadding = compoundIconPadding
visibility = GONE
}
- addView(feedbackTextView)
- // get icons scaled, assuming square icons
- val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
- autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight)
- manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
- mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight)
- normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight)
+ if (isAlternativeUI) {
+ addView(circularProgressView)
+ addView(horizontalProgressView)
+
+ autoBrightnessIcon = getDrawable("revanced_ic_sc_brightness_auto")
+ manualBrightnessIcon = getDrawable("revanced_ic_sc_brightness_manual")
+ mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute")
+ normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal")
+ } else {
+ addView(feedbackTextView)
+ // get icons scaled, assuming square icons
+ val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
+ autoBrightnessIcon =
+ getDrawable("revanced_ic_sc_brightness_auto", iconHeight, iconHeight)
+ manualBrightnessIcon =
+ getDrawable("revanced_ic_sc_brightness_manual", iconHeight, iconHeight)
+ mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute", iconHeight, iconHeight)
+ normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal", iconHeight, iconHeight)
+ }
}
private val feedbackHideHandler = Handler(Looper.getMainLooper())
private val feedbackHideCallback = Runnable {
- feedbackTextView.visibility = View.GONE
+ if (isAlternativeUI) {
+ circularProgressView.visibility = GONE
+ horizontalProgressView.visibility = GONE
+ } else {
+ feedbackTextView.visibility = GONE
+ }
}
/**
@@ -117,21 +185,81 @@ class SwipeControlsOverlayLayout(
}
}
+ /**
+ * Displays the progress bar with the appropriate value, icon, and type (brightness or volume).
+ */
+ private fun showFeedbackView(
+ value: String,
+ progress: Int,
+ max: Int,
+ icon: Drawable,
+ isBrightness: Boolean
+ ) {
+ feedbackHideHandler.removeCallbacks(feedbackHideCallback)
+ feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis)
+
+ val viewToShow =
+ if (config.isCircularProgressBar) circularProgressView else horizontalProgressView
+ viewToShow.apply {
+ setProgress(progress, max, value, isBrightness)
+ this.icon = icon
+ visibility = VISIBLE
+ }
+ }
+
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
- showFeedbackView(
- "$newVolume",
- if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
- )
+ if (isAlternativeUI) {
+ val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
+ val icon = when {
+ newVolume == 0 -> mutedVolumeIcon
+ volumePercentage < 33 -> lowVolumeIcon
+ volumePercentage < 66 -> normalVolumeIcon
+ else -> fullVolumeIcon
+ }
+ showFeedbackView("$newVolume", newVolume, maximumVolume, icon, isBrightness = false)
+ } else {
+ showFeedbackView(
+ "$newVolume",
+ if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
+ )
+ }
}
override fun onBrightnessChanged(brightness: Double) {
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
- showFeedbackView(
- str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
- autoBrightnessIcon,
- )
+ if (isAlternativeUI) {
+ showFeedbackView(
+ str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
+ 0,
+ 100,
+ autoBrightnessIcon,
+ isBrightness = true,
+ )
+ } else {
+ showFeedbackView(
+ str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
+ autoBrightnessIcon,
+ )
+ }
} else if (brightness >= 0) {
- showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
+ if (isAlternativeUI) {
+ val brightnessValue = round(brightness).toInt()
+ val icon = when {
+ brightnessValue < 25 -> lowBrightnessIcon
+ brightnessValue < 50 -> mediumBrightnessIcon
+ brightnessValue < 75 -> highBrightnessIcon
+ else -> fullBrightnessIcon
+ }
+ showFeedbackView(
+ "$brightnessValue%",
+ brightnessValue,
+ 100,
+ icon,
+ isBrightness = true
+ )
+ } else {
+ showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
+ }
}
}
@@ -145,3 +273,255 @@ class SwipeControlsOverlayLayout(
}
}
}
+
+/**
+ * Abstract base class for progress views.
+ */
+abstract class AbstractProgressView(
+ context: Context,
+ overlayBackgroundOpacity: Int,
+ protected val overlayShowOverlayMinimalStyle: Boolean,
+ overlayProgressColor: Int,
+ overlayFillBackgroundPaint: Int,
+ protected val overlayTextColor: Int,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+ // Combined paint creation function for both fill and stroke styles
+ private fun createPaint(
+ color: Int,
+ style: Paint.Style = Paint.Style.FILL,
+ strokeCap: Paint.Cap = Paint.Cap.BUTT,
+ strokeWidth: Float = 0f
+ ) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ this.style = style
+ this.color = color
+ this.strokeCap = strokeCap
+ this.strokeWidth = strokeWidth
+ }
+
+ // Initialize paints
+ val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL)
+ val progressPaint = createPaint(
+ overlayProgressColor,
+ style = Paint.Style.STROKE,
+ strokeCap = Paint.Cap.ROUND,
+ strokeWidth = 20f
+ )
+ val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL)
+ val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ color = overlayTextColor
+ textAlign = Paint.Align.CENTER
+ textSize = 40f // Can adjust based on need
+ }
+
+ protected var progress = 0
+ protected var maxProgress = 100
+ protected var displayText: String = "0"
+ protected var isBrightness = true
+ var icon: Drawable? = null
+
+ fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) {
+ progress = value
+ maxProgress = max
+ displayText = text
+ isBrightness = isBrightnessMode
+ invalidate()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ // Base class implementation can be empty
+ }
+}
+
+/**
+ * Custom view for rendering a circular progress indicator with icons and text.
+ */
+@SuppressLint("ViewConstructor")
+class CircularProgressView(
+ context: Context,
+ overlayBackgroundOpacity: Int,
+ overlayShowOverlayMinimalStyle: Boolean,
+ overlayProgressColor: Int,
+ overlayFillBackgroundPaint: Int,
+ overlayTextColor: Int,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : AbstractProgressView(
+ context,
+ overlayBackgroundOpacity,
+ overlayShowOverlayMinimalStyle,
+ overlayProgressColor,
+ overlayFillBackgroundPaint,
+ overlayTextColor,
+ attrs,
+ defStyleAttr
+) {
+ private val rectF = RectF()
+
+ init {
+ textPaint.textSize = 40f // Override default text size for circular view
+ progressPaint.strokeWidth = 20f
+ fillBackgroundPaint.strokeWidth = 20f
+ progressPaint.strokeCap = Paint.Cap.ROUND
+ fillBackgroundPaint.strokeCap = Paint.Cap.BUTT
+ progressPaint.style = Paint.Style.STROKE
+ fillBackgroundPaint.style = Paint.Style.STROKE
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ val size = min(width, height).toFloat()
+ rectF.set(20f, 20f, size - 20f, size - 20f)
+
+ canvas.drawOval(rectF, fillBackgroundPaint) // Draw the outer ring.
+ canvas.drawCircle(
+ width / 2f,
+ height / 2f,
+ size / 3,
+ backgroundPaint
+ ) // Draw the inner circle.
+
+ // Select the paint for drawing based on whether it's brightness or volume.
+ val sweepAngle = (progress.toFloat() / maxProgress) * 360
+ canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint) // Draw the progress arc.
+
+ // Draw the icon in the center.
+ icon?.let {
+ val iconSize = if (overlayShowOverlayMinimalStyle) 100 else 80
+ val iconX = (width - iconSize) / 2
+ val iconY = (height / 2) - if (overlayShowOverlayMinimalStyle) 50 else 80
+ it.setBounds(iconX, iconY, iconX + iconSize, iconY + iconSize)
+ it.draw(canvas)
+ }
+
+ // If not a minimal style mode, draw the text inside the ring.
+ if (!overlayShowOverlayMinimalStyle) {
+ canvas.drawText(displayText, width / 2f, height / 2f + 60f, textPaint)
+ }
+ }
+}
+
+/**
+ * Custom view for rendering a rectangular progress bar with icons and text.
+ */
+@SuppressLint("ViewConstructor")
+class HorizontalProgressView(
+ context: Context,
+ overlayBackgroundOpacity: Int,
+ overlayShowOverlayMinimalStyle: Boolean,
+ overlayProgressColor: Int,
+ overlayFillBackgroundPaint: Int,
+ overlayTextColor: Int,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : AbstractProgressView(
+ context,
+ overlayBackgroundOpacity,
+ overlayShowOverlayMinimalStyle,
+ overlayProgressColor,
+ overlayFillBackgroundPaint,
+ overlayTextColor,
+ attrs,
+ defStyleAttr
+) {
+
+ private val iconSize = 60f
+ private val padding = 40f
+
+ init {
+ textPaint.textSize = 36f // Override default text size for horizontal view
+ progressPaint.strokeWidth = 0f
+ progressPaint.strokeCap = Paint.Cap.BUTT
+ progressPaint.style = Paint.Style.FILL
+ fillBackgroundPaint.style = Paint.Style.FILL
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ val width = width.toFloat()
+ val height = height.toFloat()
+
+ // Radius for rounded corners
+ val cornerRadius = min(width, height) / 2
+
+ // Calculate the total width for the elements
+ val minimalElementWidth = 5 * padding + iconSize
+
+ // Calculate the starting point (X) to center the elements
+ val minimalStartX = (width - minimalElementWidth) / 2
+
+ // Draw the background
+ if (!overlayShowOverlayMinimalStyle) {
+ canvas.drawRoundRect(0f, 0f, width, height, cornerRadius, cornerRadius, backgroundPaint)
+ } else {
+ canvas.drawRoundRect(
+ minimalStartX,
+ 0f,
+ minimalStartX + minimalElementWidth,
+ height,
+ cornerRadius,
+ cornerRadius,
+ backgroundPaint
+ )
+ }
+
+ if (!overlayShowOverlayMinimalStyle) {
+ // Draw the fill background
+ val startX = 2 * padding + iconSize
+ val endX = width - 4 * padding
+ val fillWidth = endX - startX
+
+ canvas.drawRoundRect(
+ startX,
+ height / 2 - 5f,
+ endX,
+ height / 2 + 5f,
+ 10f, 10f,
+ fillBackgroundPaint
+ )
+
+ // Draw the progress
+ val progressWidth = (progress.toFloat() / maxProgress) * fillWidth
+ canvas.drawRoundRect(
+ startX,
+ height / 2 - 5f,
+ startX + progressWidth,
+ height / 2 + 5f,
+ 10f, 10f,
+ progressPaint
+ )
+ }
+
+ // Draw the icon
+ icon?.let {
+ val iconX = if (!overlayShowOverlayMinimalStyle) {
+ padding
+ } else {
+ padding + minimalStartX
+ }
+ val iconY = height / 2 - iconSize / 2
+ it.setBounds(
+ iconX.toInt(),
+ iconY.toInt(),
+ (iconX + iconSize).toInt(),
+ (iconY + iconSize).toInt()
+ )
+ it.draw(canvas)
+ }
+
+ // Draw the text on the right
+ val textX = if (!overlayShowOverlayMinimalStyle) {
+ width - 2 * padding
+ } else {
+ minimalStartX + minimalElementWidth - 2 * padding
+ }
+ val textY = height / 2 + textPaint.textSize / 3
+
+ // Draw the text
+ canvas.drawText(displayText, textX, textY, textPaint)
+ }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java
index fae38500b..378ddea77 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java
@@ -2,8 +2,27 @@ package app.revanced.extension.youtube.utils;
import static app.revanced.extension.shared.utils.StringRef.str;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
import androidx.annotation.NonNull;
+import java.util.Map;
+
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.IntegerSetting;
@@ -12,14 +31,20 @@ import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings;
public class ExtendedUtils extends PackageUtils {
+
+ private static boolean isVersionOrGreater(String version) {
+ return getAppVersionName().compareTo(version) >= 0;
+ }
+
@SuppressWarnings("unused")
- public static final boolean IS_19_17_OR_GREATER = getAppVersionName().compareTo("19.17.00") >= 0;
- public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0;
- public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0;
- public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0;
- public static final boolean IS_19_28_OR_GREATER = getAppVersionName().compareTo("19.28.00") >= 0;
- public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0;
- public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0;
+ public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
+ public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
+ public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
+ public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
+ public static final boolean IS_19_28_OR_GREATER = isVersionOrGreater("19.28.00");
+ public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
+ public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
+ public static final boolean IS_20_09_OR_GREATER = isVersionOrGreater("20.09.00");
public static int validateValue(IntegerSetting settings, int min, int max, String message) {
int value = settings.get();
@@ -114,4 +139,88 @@ public class ExtendedUtils extends PackageUtils {
}
return additionalSettingsEnabled;
}
+
+ public static void showBottomSheetDialog(Context mContext, ScrollView mScrollView,
+ Map actionsMap) {
+ runOnMainThreadDelayed(() -> {
+ AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+ builder.setView(mScrollView);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+
+ actionsMap.forEach((view, action) ->
+ view.setOnClickListener(v -> {
+ action.run();
+ dialog.dismiss();
+ })
+ );
+ actionsMap.clear();
+
+ Window window = dialog.getWindow();
+ if (window == null) {
+ return;
+ }
+
+ // round corners
+ GradientDrawable dialogBackground = new GradientDrawable();
+ dialogBackground.setCornerRadius(32);
+ window.setBackgroundDrawable(dialogBackground);
+
+ // fit screen width
+ int dialogWidth = (int) (mContext.getResources().getDisplayMetrics().widthPixels * 0.95);
+ window.setLayout(dialogWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ // move dialog to bottom
+ WindowManager.LayoutParams layoutParams = window.getAttributes();
+ layoutParams.gravity = Gravity.BOTTOM;
+
+ // adjust the vertical offset
+ layoutParams.y = dpToPx(5);
+
+ window.setAttributes(layoutParams);
+ }, 250);
+ }
+
+ public static LinearLayout createItemLayout(Context mContext, String title, int iconId) {
+ // Item Layout
+ LinearLayout itemLayout = new LinearLayout(mContext);
+ itemLayout.setOrientation(LinearLayout.HORIZONTAL);
+ itemLayout.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(12));
+ itemLayout.setGravity(Gravity.CENTER_VERTICAL);
+ itemLayout.setClickable(true);
+ itemLayout.setFocusable(true);
+
+ // Create a StateListDrawable for the background
+ StateListDrawable background = new StateListDrawable();
+ ColorDrawable pressedDrawable = new ColorDrawable(ThemeUtils.getPressedElementColor());
+ ColorDrawable defaultDrawable = new ColorDrawable(ThemeUtils.getBackgroundColor());
+ background.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable);
+ background.addState(new int[]{}, defaultDrawable);
+ itemLayout.setBackground(background);
+
+ // Icon
+ ColorFilter cf = new PorterDuffColorFilter(ThemeUtils.getForegroundColor(), PorterDuff.Mode.SRC_ATOP);
+ ImageView iconView = new ImageView(mContext);
+ iconView.setImageResource(iconId);
+ iconView.setColorFilter(cf);
+ LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dpToPx(24), dpToPx(24));
+ iconParams.setMarginEnd(dpToPx(16));
+ iconView.setLayoutParams(iconParams);
+ itemLayout.addView(iconView);
+
+ // Text container
+ LinearLayout textContainer = new LinearLayout(mContext);
+ textContainer.setOrientation(LinearLayout.VERTICAL);
+ TextView titleView = new TextView(mContext);
+ titleView.setText(title);
+ titleView.setTextSize(16);
+ titleView.setTextColor(ThemeUtils.getForegroundColor());
+ textContainer.addView(titleView);
+
+ itemLayout.addView(textContainer);
+
+ return itemLayout;
+ }
+
}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java
index 68f364829..6d8d407c4 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java
@@ -63,7 +63,7 @@ public class VideoUtils extends IntentUtils {
return builder.toString();
}
- private static String getVideoScheme(String videoId, boolean isShorts) {
+ public static String getVideoScheme(String videoId, boolean isShorts) {
return String.format(
Locale.ENGLISH,
isShorts ? VIDEO_SCHEME_INTENT_FORMAT : VIDEO_SCHEME_LINK_FORMAT,
@@ -128,6 +128,22 @@ public class VideoUtils extends IntentUtils {
launchView(getChannelUrl(channelId), getContext().getPackageName());
}
+ public static void openPlaylist(@NonNull String playlistId) {
+ openPlaylist(playlistId, "");
+ }
+
+ public static void openPlaylist(@NonNull String playlistId, @NonNull String videoId) {
+ final StringBuilder sb = new StringBuilder();
+ if (videoId.isEmpty()) {
+ sb.append(getPlaylistUrl(playlistId));
+ } else {
+ sb.append(getVideoScheme(videoId, false));
+ sb.append("&list=");
+ sb.append(playlistId);
+ }
+ launchView(sb.toString(), getContext().getPackageName());
+ }
+
public static void openVideo() {
openVideo(VideoInformation.getVideoId());
}
@@ -177,8 +193,8 @@ public class VideoUtils extends IntentUtils {
}
public static void showPlaybackSpeedDialog(@NonNull Context context) {
- final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedListEntries();
- final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedListEntryValues();
+ final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedEntries();
+ final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedEntryValues();
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed));
@@ -186,6 +202,7 @@ public class VideoUtils extends IntentUtils {
new AlertDialog.Builder(context)
.setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> {
final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f");
+ VideoInformation.setPlaybackSpeed(selectedPlaybackSpeed);
VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed);
userSelectedPlaybackSpeed(selectedPlaybackSpeed);
mDialog.dismiss();
diff --git a/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java
index a969c6bd4..f9c0c50f3 100644
--- a/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java
+++ b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java
@@ -1,7 +1,9 @@
package com.google.android.apps.youtube.app.settings.videoquality;
+import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
+import android.content.res.Resources;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.View;
@@ -25,8 +27,8 @@ import app.revanced.extension.youtube.utils.ThemeUtils;
@SuppressWarnings("deprecation")
public class VideoQualitySettingsActivity extends Activity {
- private static final String rvxSettingsLabel = ResourceUtils.getString("revanced_extended_settings_title");
- private static final String searchLabel = ResourceUtils.getString("revanced_extended_settings_search_title");
+ private static String rvxSettingsLabel;
+ private static String searchLabel;
private static WeakReference searchViewRef = new WeakReference<>(null);
private static WeakReference closeButtonRef = new WeakReference<>(null);
private ReVancedPreferenceFragment fragment;
@@ -71,6 +73,10 @@ public class VideoQualitySettingsActivity extends Activity {
return;
}
+ // Set label
+ rvxSettingsLabel = getString("revanced_extended_settings_title");
+ searchLabel = getString("revanced_extended_settings_search_title");
+
// Set toolbar
setToolbar();
@@ -85,6 +91,14 @@ public class VideoQualitySettingsActivity extends Activity {
}
}
+ @SuppressLint("DiscouragedApi")
+ private String getString(String str) {
+ Context baseContext = getBaseContext();
+ Resources resources = baseContext.getResources();
+ int identifier = resources.getIdentifier(str, "string", baseContext.getPackageName());
+ return resources.getString(identifier);
+ }
+
private void filterPreferences(String query) {
if (fragment == null) return;
fragment.filterPreferences(query);
diff --git a/gradle.properties b/gradle.properties
index 8b8969587..fbb95566f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,5 +4,5 @@ org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
kotlin.jvm.target.validation.mode = IGNORE
-version = 5.5.1
+version = 5.6.1
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4376956d5..616c34f24 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,12 +6,14 @@ smali = "3.0.5"
gson = "2.12.1"
agp = "8.2.2"
annotation = "1.9.1"
+collections4 = "4.5.0-M3"
lang3 = "3.17.0"
preference = "1.2.1"
[libraries]
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
+collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "collections4" }
lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" }
preference = { module = "androidx.preference:preference", version.ref = "preference" }
diff --git a/patches.json b/patches.json
index 93b46884c..cff3a24c6 100644
--- a/patches.json
+++ b/patches.json
@@ -11,13 +11,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -32,13 +30,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -59,7 +55,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -73,13 +69,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -101,7 +95,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -116,13 +110,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -141,7 +133,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -157,13 +149,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -180,13 +170,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -199,7 +187,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": [
@@ -227,13 +216,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -256,7 +243,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -272,13 +259,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -300,7 +285,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -314,13 +299,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -356,13 +339,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -394,13 +375,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -457,7 +436,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -505,7 +484,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": [
@@ -532,13 +512,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -574,7 +552,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -617,13 +595,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -647,13 +623,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -687,7 +661,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -720,7 +694,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -768,13 +742,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -794,7 +766,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -816,7 +788,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -838,7 +810,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -853,13 +825,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -881,11 +851,19 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
},
+ {
+ "name": "Disable edge-to-edge display",
+ "description": "Disable forced edge-to-edge display on Android 15+ by changing the app\u0027s target SDK version. This patch does not work if the app is installed by mounting.",
+ "use": false,
+ "dependencies": [],
+ "compatiblePackages": null,
+ "options": []
+ },
{
"name": "Disable forced auto audio tracks",
"description": "Adds an option to disable audio tracks from being automatically enabled.",
@@ -895,13 +873,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -923,7 +899,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -938,13 +914,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -958,13 +932,29 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
+ ]
+ },
+ "options": []
+ },
+ {
+ "name": "Disable layout updates",
+ "description": "Adds an option to disable layout updates by server.",
+ "use": true,
+ "dependencies": [
+ "Settings for YouTube"
+ ],
+ "compatiblePackages": {
+ "com.google.android.youtube": [
+ "19.05.36",
+ "19.16.39",
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -987,7 +977,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1001,13 +991,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1022,13 +1010,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1038,13 +1024,13 @@
"description": "Adds an option to disable the popup that appears when taking a screenshot.",
"use": true,
"dependencies": [
- "Settings for Reddit",
- "ResourcePatch"
+ "Settings for Reddit"
],
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -1059,13 +1045,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1087,7 +1071,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1102,13 +1086,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1129,7 +1111,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1143,13 +1125,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1163,13 +1143,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1191,7 +1169,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1219,7 +1197,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1233,13 +1211,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1260,13 +1236,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1288,7 +1262,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -1348,13 +1322,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -1414,7 +1386,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -1428,13 +1401,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1448,13 +1419,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1476,7 +1445,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1502,7 +1471,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1519,13 +1488,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1553,7 +1520,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1568,7 +1535,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -1587,13 +1555,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1610,13 +1576,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1637,13 +1601,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1658,13 +1620,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1689,7 +1649,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1708,13 +1668,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1729,7 +1687,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -1751,7 +1710,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1769,13 +1728,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1793,13 +1750,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1818,7 +1773,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -1833,7 +1788,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -1848,13 +1804,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -1905,13 +1859,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1921,19 +1873,18 @@
"description": "Adds support to download videos with an external downloader app using the in-app download button.",
"use": true,
"dependencies": [
+ "BytecodePatch",
"BytecodePatch",
"ResourcePatch",
"Settings for YouTube"
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1948,13 +1899,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -1970,13 +1919,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2001,7 +1948,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2019,13 +1966,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2041,7 +1986,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -2057,7 +2003,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -2072,13 +2019,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2092,18 +2037,17 @@
"BytecodePatch",
"BytecodePatch",
"ResourcePatch",
+ "BytecodePatch",
"ResourcePatch",
"Settings for YouTube"
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -2174,7 +2118,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2198,13 +2142,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2217,7 +2159,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -2238,7 +2181,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2254,13 +2197,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2275,7 +2216,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -2297,7 +2239,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2312,13 +2254,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2339,7 +2279,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2361,7 +2301,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2379,13 +2319,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2408,7 +2346,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2423,13 +2361,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2451,7 +2387,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2466,7 +2402,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": []
@@ -2481,13 +2418,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2507,13 +2442,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2529,7 +2462,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
- "2025.05.1"
+ "2025.05.1",
+ "2025.12.0"
]
},
"options": [
@@ -2560,13 +2494,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -2632,7 +2564,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -2673,13 +2605,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2694,13 +2624,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -2792,7 +2720,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2808,13 +2736,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -2851,7 +2777,10 @@
"compatiblePackages": {
"com.google.android.apps.youtube.music": [
"6.51.53",
- "7.16.53"
+ "7.16.53",
+ "7.25.53",
+ "8.05.51",
+ "8.10.52"
]
},
"options": []
@@ -2868,38 +2797,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
- ]
- },
- "options": []
- },
- {
- "name": "Spoof client",
- "description": "Adds options to spoof the client to allow playback.",
- "use": false,
- "dependencies": [
- "Settings for YouTube Music",
- "ResourcePatch",
- "BytecodePatch",
- "ResourcePatch",
- "BytecodePatch"
- ],
- "compatiblePackages": {
- "com.google.android.apps.youtube.music": [
- "6.20.51",
- "6.29.59",
- "6.42.55",
- "6.51.53",
- "7.16.53",
- "7.25.53",
- "8.05.51",
- "8.10.51"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2922,7 +2824,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -2940,16 +2842,24 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
- "options": []
+ "options": [
+ {
+ "key": "useIOSClient",
+ "title": "Use iOS client",
+ "description": "Add setting to set iOS client (Deprecated) as default client.",
+ "required": false,
+ "type": "kotlin.Boolean",
+ "default": false,
+ "values": null
+ }
+ ]
},
{
"name": "Swipe controls",
@@ -2965,13 +2875,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -2986,13 +2894,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -3048,13 +2954,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -3068,13 +2972,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -3093,7 +2995,7 @@
"description": "A list of translations to be added for the RVX settings, separated by commas.",
"required": true,
"type": "kotlin.String",
- "default": "ar, bg-rBG, de-rDE, el-rGR, es-rES, fr-rFR, hu-rHU, it-rIT, ja-rJP, ko-rKR, pl-rPL, pt-rBR, ru-rRU, tr-rTR, uk-rUA, vi-rVN, zh-rCN, zh-rTW",
+ "default": "ar, bg-rBG, de-rDE, el-rGR, es-rES, fr-rFR, hu-rHU, id-rID, in, it-rIT, ja-rJP, ko-rKR, pl-rPL, pt-rBR, ru-rRU, tr-rTR, uk-rUA, vi-rVN, zh-rCN, zh-rTW",
"values": null
},
{
@@ -3123,7 +3025,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -3174,7 +3076,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -3197,13 +3099,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
@@ -3217,13 +3117,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": [
@@ -3270,7 +3168,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": [
@@ -3308,7 +3206,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51"
+ "8.12.53"
]
},
"options": []
@@ -3323,13 +3221,11 @@
],
"compatiblePackages": {
"com.google.android.youtube": [
- "18.29.38",
- "18.33.40",
- "18.38.44",
- "18.48.39",
"19.05.36",
"19.16.39",
- "19.44.39"
+ "19.43.41",
+ "19.44.39",
+ "19.47.53"
]
},
"options": []
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 30b027aeb..11f542538 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -6,6 +6,10 @@ public final class app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWi
public static final fun getSpoofWifiPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/all/misc/display/edgetoedge/EdgeToEdgeDisplayPatchKt {
+ public static final fun getEdgeToEdgeDisplayPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
+}
+
public abstract interface class app/revanced/patches/all/misc/transformation/IMethodCall {
public abstract fun getDefinedClassName ()Ljava/lang/String;
public abstract fun getMethodName ()Ljava/lang/String;
@@ -253,6 +257,7 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
public static final fun getBottomSheetRecyclerView ()J
public static final fun getButtonContainer ()J
public static final fun getButtonIconPaddingMedium ()J
+ public static final fun getChannelHandle ()J
public static final fun getChipCloud ()J
public static final fun getColorGrey ()J
public static final fun getDarkBackground ()J
@@ -281,6 +286,7 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
public static final fun getPrivacyTosFooter ()J
public static final fun getQualityAuto ()J
public static final fun getRemixGenericButtonSize ()J
+ public static final fun getSearchButton ()J
public static final fun getSlidingDialogAnimation ()J
public static final fun getTapBloomView ()J
public static final fun getText1 ()J
@@ -452,10 +458,6 @@ public final class app/revanced/patches/reddit/utils/extension/SharedExtensionPa
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
-public final class app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatchKt {
- public static final fun getScreenShotShareBanner ()J
-}
-
public final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt {
public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun is_2024_26_or_greater ()Z
@@ -550,10 +552,9 @@ public final class app/revanced/patches/shared/mapping/ResourceElement {
}
public final class app/revanced/patches/shared/mapping/ResourceMappingPatchKt {
- public static final fun get (Ljava/util/List;Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J
- public static final fun get (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)J
+ public static final fun getResourceId (Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J
+ public static final fun getResourceId (Ljava/lang/String;Ljava/lang/String;)J
public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
- public static final fun getResourceMappings ()Ljava/util/List;
}
public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum {
@@ -715,6 +716,10 @@ public final class app/revanced/patches/youtube/general/toolbar/ToolBarComponent
public static final fun getToolBarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/youtube/general/updates/LayoutUpdatesPatchKt {
+ public static final fun getLayoutUpdatesPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatchKt {
public static final fun getShortsActionButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
@@ -866,6 +871,7 @@ public final class app/revanced/patches/youtube/player/seekbar/SeekbarComponents
public final class app/revanced/patches/youtube/shorts/components/FingerprintsKt {
public static final fun indexOfAddLiveHeaderElementsContainerInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
+ public static final fun indexOfDismissInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
}
public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt {
@@ -1013,6 +1019,10 @@ public final class app/revanced/patches/youtube/utils/playertype/PlayerTypeHookP
public static final fun getPlayerTypeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/youtube/utils/playlist/PlaylistPatchKt {
+ public static final fun getPlaylistPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPatchKt {
public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun is_18_31_or_greater ()Z
@@ -1020,12 +1030,16 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
public static final fun is_18_39_or_greater ()Z
public static final fun is_18_42_or_greater ()Z
public static final fun is_18_49_or_greater ()Z
+ public static final fun is_19_01_or_greater ()Z
public static final fun is_19_02_or_greater ()Z
public static final fun is_19_04_or_greater ()Z
+ public static final fun is_19_05_or_greater ()Z
public static final fun is_19_09_or_greater ()Z
+ public static final fun is_19_11_or_greater ()Z
public static final fun is_19_15_or_greater ()Z
public static final fun is_19_16_or_greater ()Z
public static final fun is_19_17_or_greater ()Z
+ public static final fun is_19_18_or_greater ()Z
public static final fun is_19_23_or_greater ()Z
public static final fun is_19_25_or_greater ()Z
public static final fun is_19_26_or_greater ()Z
@@ -1040,10 +1054,13 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
public static final fun is_19_44_or_greater ()Z
public static final fun is_19_46_or_greater ()Z
public static final fun is_19_49_or_greater ()Z
+ public static final fun is_19_50_or_greater ()Z
public static final fun is_20_02_or_greater ()Z
public static final fun is_20_03_or_greater ()Z
public static final fun is_20_05_or_greater ()Z
+ public static final fun is_20_09_or_greater ()Z
public static final fun is_20_10_or_greater ()Z
+ public static final fun is_20_12_or_greater ()Z
}
public final class app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatchKt {
@@ -1145,7 +1162,6 @@ public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceI
public static final fun getRelatedChipCloudMargin ()J
public static final fun getRightComment ()J
public static final fun getScrimOverlay ()J
- public static final fun getScrubbing ()J
public static final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J
public static final fun getSeekUndoEduOverlayStub ()J
public static final fun getSettingsFragment ()J
@@ -1160,13 +1176,17 @@ public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceI
public static final fun getTotalTime ()J
public static final fun getTouchArea ()J
public static final fun getVarispeedUnavailableTitle ()J
+ public static final fun getVerticalTouchOffsetToEnterFineScrubbing ()J
+ public static final fun getVerticalTouchOffsetToStartFineScrubbing ()J
public static final fun getVideoQualityBottomSheet ()J
public static final fun getVideoQualityUnavailableAnnouncement ()J
public static final fun getVideoZoomSnapIndicator ()J
public static final fun getVoiceSearch ()J
public static final fun getYouTubeControlsOverlaySubtitleButton ()J
public static final fun getYouTubeLogo ()J
+ public static final fun getYtCallToAction ()J
public static final fun getYtFillBell ()J
+ public static final fun getYtOutlineLibrary ()J
public static final fun getYtOutlineMoonZ ()J
public static final fun getYtOutlinePictureInPictureWhite ()J
public static final fun getYtOutlineVideoCamera ()J
diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt
index 3758030cd..354e91f3e 100644
--- a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt
@@ -152,7 +152,10 @@ private enum class MethodCall(
RegisterNetworkCallback1(
"Landroid/net/ConnectivityManager;",
"registerNetworkCallback",
- arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
+ arrayOf(
+ "Landroid/net/NetworkRequest;",
+ "Landroid/net/ConnectivityManager\$NetworkCallback;"
+ ),
"V",
),
RegisterNetworkCallback2(
@@ -174,13 +177,20 @@ private enum class MethodCall(
RequestNetwork1(
"Landroid/net/ConnectivityManager;",
"requestNetwork",
- arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
+ arrayOf(
+ "Landroid/net/NetworkRequest;",
+ "Landroid/net/ConnectivityManager\$NetworkCallback;"
+ ),
"V",
),
RequestNetwork2(
"Landroid/net/ConnectivityManager;",
"requestNetwork",
- arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"),
+ arrayOf(
+ "Landroid/net/NetworkRequest;",
+ "Landroid/net/ConnectivityManager\$NetworkCallback;",
+ "I"
+ ),
"V",
),
RequestNetwork3(
diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/display/edgetoedge/EdgeToEdgeDisplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/display/edgetoedge/EdgeToEdgeDisplayPatch.kt
new file mode 100644
index 000000000..9fdb3fd84
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/display/edgetoedge/EdgeToEdgeDisplayPatch.kt
@@ -0,0 +1,39 @@
+package app.revanced.patches.all.misc.display.edgetoedge
+
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.util.Utils.printWarn
+import app.revanced.util.getNode
+import org.w3c.dom.Element
+
+@Suppress("unused")
+val edgeToEdgeDisplayPatch = resourcePatch(
+ name = "Disable edge-to-edge display",
+ description = "Disable forced edge-to-edge display on Android 15+ by changing the app's target SDK version. " +
+ "This patch does not work if the app is installed by mounting.",
+ use = false,
+) {
+ execute {
+ document("AndroidManifest.xml").use { document ->
+ // Ideally, the patch should only be applied when targetSdkVersion is 35 or greater.
+ // Since ApkTool does not add targetSdkVersion to AndroidManifest, there is no way to check targetSdkVersion.
+ // Instead, it checks compileSdkVersion and prints a warning.
+ try {
+ val manifestElement = document.getNode("manifest") as Element
+ val compileSdkVersion =
+ Integer.parseInt(manifestElement.getAttribute("android:compileSdkVersion"))
+ if (compileSdkVersion < 35) {
+ printWarn("This app may not be forcing edge to edge display (compileSdkVersion: $compileSdkVersion)")
+ }
+ } catch (_: Exception) {
+ printWarn("Failed to check compileSdkVersion")
+ }
+
+ // Change targetSdkVersion to 34.
+ document.getElementsByTagName("manifest").item(0).also {
+ it.appendChild(it.ownerDocument.createElement("uses-sdk").also { element ->
+ element.setAttribute("android:targetSdkVersion", "34")
+ })
+ }
+ }
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/account/components/AccountComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/account/components/AccountComponentsPatch.kt
index f15dc184b..f5a9970cd 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/account/components/AccountComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/account/components/AccountComponentsPatch.kt
@@ -4,23 +4,27 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
+import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR
import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS
+import app.revanced.patches.music.utils.resourceid.channelHandle
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
-import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
+import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
+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
@Suppress("unused")
@@ -84,17 +88,50 @@ val accountComponentsPatch = bytecodePatch(
}
// account switcher
- namesInactiveAccountThumbnailSizeFingerprint.matchOrThrow().let {
- it.method.apply {
- val targetIndex = it.patternMatch!!.startIndex
- val targetRegister = getInstruction(targetIndex).registerA
+ val textViewField = with(
+ channelHandleFingerprint
+ .methodOrThrow(namesInactiveAccountThumbnailSizeFingerprint)
+ ) {
+ val literalIndex = indexOfFirstLiteralInstructionOrThrow(channelHandle)
+ getInstruction(
+ indexOfFirstInstructionOrThrow(literalIndex) {
+ opcode == Opcode.IPUT_OBJECT &&
+ getReference()?.type == "Landroid/widget/TextView;"
+ },
+ ).getReference()
+ }
- addInstructions(
- targetIndex, """
- invoke-static {v$targetRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
- move-result v$targetRegister
- """
- )
+ namesInactiveAccountThumbnailSizeFingerprint.methodOrThrow().apply {
+ var hook = false
+
+ implementation!!.instructions
+ .withIndex()
+ .filter { (_, instruction) ->
+ val reference =
+ (instruction as? ReferenceInstruction)?.reference
+ instruction.opcode == Opcode.IGET_OBJECT &&
+ reference is FieldReference &&
+ reference == textViewField
+ }
+ .map { (index, _) -> index }
+ .forEach { index ->
+ val insertIndex = index - 1
+ if (!hook && getInstruction(insertIndex).opcode == Opcode.IF_NEZ) {
+ val insertRegister =
+ getInstruction(insertIndex).registerA
+
+ addInstructions(
+ insertIndex, """
+ invoke-static {v$insertRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
+ move-result v$insertRegister
+ """
+ )
+ hook = true
+ }
+ }
+
+ if (!hook) {
+ throw PatchException("Could not find TextUtils.isEmpty() index")
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/account/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/account/components/Fingerprints.kt
index 8af714611..bb28af644 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/account/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/account/components/Fingerprints.kt
@@ -1,11 +1,11 @@
package app.revanced.patches.music.account.components
import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility
+import app.revanced.patches.music.utils.resourceid.channelHandle
import app.revanced.patches.music.utils.resourceid.menuEntry
import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize
import app.revanced.patches.music.utils.resourceid.tosFooter
import app.revanced.util.fingerprint.legacyFingerprint
-import com.android.tools.smali.dexlib2.Opcode
internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
name = "accountSwitcherAccessibilityLabelFingerprint",
@@ -14,6 +14,12 @@ internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
literals = listOf(accountSwitcherAccessibility)
)
+internal val channelHandleFingerprint = legacyFingerprint(
+ name = "channelHandleFingerprint",
+ returnType = "V",
+ literals = listOf(channelHandle),
+)
+
internal val menuEntryFingerprint = legacyFingerprint(
name = "menuEntryFingerprint",
returnType = "V",
@@ -24,19 +30,6 @@ internal val namesInactiveAccountThumbnailSizeFingerprint = legacyFingerprint(
name = "namesInactiveAccountThumbnailSizeFingerprint",
returnType = "V",
parameters = listOf("L", "Ljava/lang/Object;"),
- opcodes = listOf(
- Opcode.IF_NEZ,
- Opcode.IGET_OBJECT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.IGET_OBJECT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.GOTO,
- Opcode.IGET_OBJECT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT_OBJECT,
- Opcode.IF_EQZ
- ),
literals = listOf(namesInactiveAccountThumbnailSize)
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ads/general/AdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/AdsPatch.kt
index 23df8ddbd..84381b787 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/ads/general/AdsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/AdsPatch.kt
@@ -115,7 +115,8 @@ val adsPatch = bytecodePatch(
.methodOrThrow(getPremiumDialogParentFingerprint)
.apply {
val setContentViewIndex = indexOfSetContentViewInstruction(this)
- val dialogInstruction = getInstruction(setContentViewIndex)
+ val dialogInstruction =
+ getInstruction(setContentViewIndex)
val dialogRegister = dialogInstruction.registerC
val viewRegister = dialogInstruction.registerD
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ads/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/Fingerprints.kt
index a70e8e57a..8c05ff53a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/ads/general/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/Fingerprints.kt
@@ -97,8 +97,6 @@ internal val showDialogCommandFingerprint = legacyFingerprint(
name = "showDialogCommandFingerprint",
returnType = "V",
opcodes = listOf(
- Opcode.IF_EQ,
- Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IGET, // get dialog code
),
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/components/Fingerprints.kt
index 664966147..319724ff3 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/general/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/general/components/Fingerprints.kt
@@ -5,6 +5,7 @@ import app.revanced.patches.music.utils.resourceid.historyMenuItem
import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf
import app.revanced.patches.music.utils.resourceid.offlineSettingsMenuItem
import app.revanced.patches.music.utils.resourceid.playerOverlayChip
+import app.revanced.patches.music.utils.resourceid.searchButton
import app.revanced.patches.music.utils.resourceid.toolTipContentView
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
import app.revanced.util.fingerprint.legacyFingerprint
@@ -118,6 +119,17 @@ internal val preferenceScreenFingerprint = legacyFingerprint(
}
)
+internal val searchActionViewFingerprint = legacyFingerprint(
+ name = "searchActionViewFingerprint",
+ returnType = "Landroid/view/View;",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = emptyList(),
+ literals = listOf(searchButton),
+ customFingerprint = { _, classDef ->
+ classDef.type.endsWith("/SearchActionProvider;")
+ }
+)
+
internal val searchBarFingerprint = legacyFingerprint(
name = "searchBarFingerprint",
returnType = "V",
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/components/LayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/components/LayoutComponentsPatch.kt
index 3154b1828..ad11402eb 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/general/components/LayoutComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/general/components/LayoutComponentsPatch.kt
@@ -20,6 +20,7 @@ import app.revanced.patches.music.utils.playservice.is_8_05_or_greater
import app.revanced.patches.music.utils.playservice.versionCheckPatch
import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf
import app.revanced.patches.music.utils.resourceid.playerOverlayChip
+import app.revanced.patches.music.utils.resourceid.searchButton
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
import app.revanced.patches.music.utils.settings.CategoryType
@@ -198,6 +199,23 @@ val layoutComponentsPatch = bytecodePatch(
// endregion
+ // region patch for hide search button
+
+ searchActionViewFingerprint.methodOrThrow().apply {
+ val constIndex =
+ indexOfFirstLiteralInstructionOrThrow(searchButton)
+ val targetIndex =
+ indexOfFirstInstructionOrThrow(constIndex, Opcode.MOVE_RESULT_OBJECT)
+ val targetRegister = getInstruction(targetIndex).registerA
+
+ addInstruction(
+ targetIndex + 1,
+ "invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->hideSearchButton(Landroid/view/View;)V"
+ )
+ }
+
+ // endregion
+
// region patch for hide sound search button
if (is_6_48_or_greater) {
@@ -353,6 +371,11 @@ val layoutComponentsPatch = bytecodePatch(
"revanced_hide_samples_shelf",
"false"
)
+ addSwitchPreference(
+ CategoryType.GENERAL,
+ "revanced_hide_search_button",
+ "false"
+ )
if (is_6_48_or_greater) {
addSwitchPreference(
CategoryType.GENERAL,
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt
index f910e84d1..895555ee0 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt
@@ -32,25 +32,32 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
)
execute {
- if (!is_6_43_or_greater || is_7_25_or_greater) {
+ if (!is_6_43_or_greater) {
return@execute
}
- if (is_7_17_or_greater) {
- findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
- name == "SpoofAppVersionDefaultString"
- }.replaceInstruction(
- 0,
- "const-string v0, \"7.16.53\""
- )
+ var defaultVersionString = "6.42.55"
+
+ if (is_7_17_or_greater && !is_7_25_or_greater) {
+ defaultVersionString = "7.16.53"
+ defaultValue = "true"
+
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
name == "SpoofAppVersionDefaultBoolean"
}.replaceInstruction(
0,
"const/4 v0, 0x1"
)
-
- defaultValue = "true"
}
+ if (is_7_25_or_greater) {
+ defaultVersionString = "7.17.52"
+ }
+
+ findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
+ name == "SpoofAppVersionDefaultString"
+ }.replaceInstruction(
+ 0,
+ "const-string v0, \"$defaultVersionString\""
+ )
}
}
@@ -63,6 +70,9 @@ val spoofAppVersionPatch = resourcePatch(
YOUTUBE_MUSIC_PACKAGE_NAME(
"6.51.53",
"7.16.53",
+ "7.25.53",
+ "8.05.51",
+ "8.10.52",
),
)
@@ -73,13 +83,19 @@ val spoofAppVersionPatch = resourcePatch(
)
execute {
- if (!is_6_43_or_greater || is_7_25_or_greater) {
- printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.43.53 ~ 7.24.51.")
+ if (!is_6_43_or_greater) {
+ printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.51.53 or later.")
return@execute
}
- if (is_7_17_or_greater) {
+ if (!is_7_17_or_greater) {
+ appendAppVersion("6.42.55")
+ }
+ if (is_7_17_or_greater && !is_7_25_or_greater) {
appendAppVersion("7.16.53")
}
+ if (is_7_25_or_greater) {
+ appendAppVersion("7.17.52")
+ }
addSwitchPreference(
CategoryType.GENERAL,
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt
index 7d0b38f5a..3ae23745c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt
@@ -206,7 +206,8 @@ val changeHeaderPatch = resourcePatch(
printWarn(warnings)
}
- val isLegacyLogoExists = get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists()
+ val isLegacyLogoExists =
+ get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists()
if (is_7_27_or_greater && isLegacyLogoExists) {
document("res/layout/signin_fragment.xml").use { document ->
document.doRecursively node@{ node ->
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
index d918c8163..c2e71c3bf 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
@@ -24,7 +24,7 @@ import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val EXTENSION_METHOD_DESCRIPTOR =
@@ -41,7 +41,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
"7.16.53",
"7.25.53",
"8.05.51",
- "8.10.51",
+ "8.12.53",
),
)
@@ -57,7 +57,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
return@execute
} else if (!is_7_20_or_greater) {
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
- 45635386L,
+ CAIRO_SPLASH_ANIMATION_FEATURE_FLAG,
EXTENSION_METHOD_DESCRIPTOR
)
} else {
@@ -69,18 +69,13 @@ val cairoSplashAnimationPatch = bytecodePatch(
opcode == Opcode.INVOKE_VIRTUAL &&
getReference()?.name == "setContentView"
} + 1
- val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) {
- val reference = getReference()
- opcode == Opcode.INVOKE_VIRTUAL &&
- reference?.name == "findViewById" &&
- reference.definingClass != "Landroid/view/View;"
- }
+ val freeIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST)
val freeRegister =
- getInstruction(viewStubFindViewByIdIndex).registerD
- val jumpIndex = indexOfFirstInstructionReversedOrThrow(
- viewStubFindViewByIdIndex,
- Opcode.IGET_OBJECT
- )
+ getInstruction(freeIndex).registerA
+ val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
+ } + 1
addInstructionsWithLabels(
insertIndex, """
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/Fingerprints.kt
index 05fbdf843..0c04718df 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/Fingerprints.kt
@@ -5,6 +5,8 @@ import app.revanced.patches.music.utils.resourceid.mainActivityLaunchAnimation
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.indexOfFirstLiteralInstruction
+internal const val CAIRO_SPLASH_ANIMATION_FEATURE_FLAG = 45635386L
+
/**
* This fingerprint is compatible with YouTube Music v7.06.53+
*/
@@ -20,7 +22,7 @@ internal val cairoSplashAnimationConfigFingerprint = legacyFingerprint(
if (is_7_20_or_greater) {
method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0
} else {
- method.indexOfFirstLiteralInstruction(45635386) >= 0
+ method.indexOfFirstLiteralInstruction(CAIRO_SPLASH_ANIMATION_FEATURE_FLAG) >= 0
}
}
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/watchhistory/WatchHistoryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/watchhistory/WatchHistoryPatch.kt
index fdc321bca..9eb30851d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/watchhistory/WatchHistoryPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/watchhistory/WatchHistoryPatch.kt
@@ -1,13 +1,13 @@
package app.revanced.patches.music.misc.watchhistory
import app.revanced.patcher.patch.bytecodePatch
-import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
-import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.patch.PatchList.WATCH_HISTORY
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
+import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
@Suppress("unused")
val watchHistoryPatch = bytecodePatch(
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt
index e4b459e25..8cc05af21 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt
@@ -124,14 +124,17 @@ val navigationBarComponentsPatch = bytecodePatch(
opcode == Opcode.IGET_OBJECT &&
getReference()?.type == "Ljava/lang/String;"
}
- val browseIdReference = getInstruction(browseIdIndex).reference as FieldReference
+ val browseIdReference =
+ getInstruction(browseIdIndex).reference as FieldReference
val fieldName = browseIdReference.name
val componentIndex = indexOfFirstInstructionOrThrow(stringIndex) {
opcode == Opcode.IGET_OBJECT &&
getReference()?.toString() == browseIdReference.toString()
}
- val browseIdRegister = getInstruction(componentIndex).registerA
- val componentRegister = getInstruction(componentIndex).registerB
+ val browseIdRegister =
+ getInstruction(componentIndex).registerA
+ val componentRegister =
+ getInstruction(componentIndex).registerB
val enumIndex = it.patternMatch!!.startIndex + 3
val enumRegister = getInstruction(enumIndex).registerA
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
index 20921e3c4..be66489ad 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
@@ -54,7 +54,7 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
parameters = emptyList(),
customFingerprint = { method, _ ->
AccessFlags.FINAL.isSet(method.accessFlags) &&
- method.containsLiteralInstruction(1) &&
+ method.containsLiteralInstruction(1) &&
method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference()?.name == "booleanValue"
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt
index c39664845..cbc3ceffe 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt
@@ -746,15 +746,22 @@ val playerComponentsPatch = bytecodePatch(
val freeRegister =
getInstruction(bottomSheetBehaviorIndex).registerD
+ val getFieldIndex = bottomSheetBehaviorIndex - 2
+ val getFieldReference =
+ getInstruction(getFieldIndex).reference
+ val getFieldInstruction = getInstruction(getFieldIndex)
+
addInstructionsWithLabels(
- bottomSheetBehaviorIndex - 2,
+ getFieldIndex + 1,
"""
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z
move-result v$freeRegister
if-nez v$freeRegister, :dismiss
+ iget-object v${getFieldInstruction.registerA}, v${getFieldInstruction.registerB}, $getFieldReference
""",
ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
)
+ removeInstruction(getFieldIndex)
} ?: throw PatchException("Could not find targetMethod")
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
index 3169e345f..eb3882209 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
@@ -16,7 +16,7 @@ internal object Constants {
"7.16.53", // This is the latest version that supports the 'Spoof app version' patch.
"7.25.53", // This is the last supported version for 2024.
"8.05.51", // This was the latest version supported by the previous RVX patch.
- "8.10.51", // This is the latest version supported by the RVX patch.
+ "8.12.53", // This is the latest version supported by the RVX patch.
)
)
}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/SharedExtensionPatch.kt
index dd6883783..7e155f261 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/SharedExtensionPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/SharedExtensionPatch.kt
@@ -1,14 +1,8 @@
package app.revanced.patches.music.utils.extension
import app.revanced.patches.music.utils.extension.hooks.applicationInitHook
-import app.revanced.patches.music.utils.extension.hooks.mainActivityBaseContextHook
-import app.revanced.patches.shared.extension.hooks.cronetEngineContextHook
-import app.revanced.patches.shared.extension.hooks.firebaseInitProviderContextHook
import app.revanced.patches.shared.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch(
applicationInitHook,
- cronetEngineContextHook,
- firebaseInitProviderContextHook,
- mainActivityBaseContextHook,
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/hooks/MainActivityBaseContextHook.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/hooks/MainActivityBaseContextHook.kt
deleted file mode 100644
index 549eff2ba..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/hooks/MainActivityBaseContextHook.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package app.revanced.patches.music.utils.extension.hooks
-
-import app.revanced.patches.shared.extension.extensionHook
-import app.revanced.util.getReference
-import app.revanced.util.indexOfFirstInstructionOrThrow
-import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.reference.MethodReference
-
-private var attachBaseContextIndex = -1
-
-internal val mainActivityBaseContextHook = extensionHook(
- insertIndexResolver = { method ->
- attachBaseContextIndex = method.indexOfFirstInstructionOrThrow {
- getReference()?.name == "attachBaseContext"
- }
-
- attachBaseContextIndex + 1
- },
- contextRegisterResolver = { method ->
- val overrideInstruction =
- method.implementation!!.instructions.elementAt(attachBaseContextIndex)
- as FiveRegisterInstruction
- "v${overrideInstruction.registerD}"
- },
-) {
- returns("V")
- parameters("Landroid/content/Context;")
- custom { method, classDef ->
- classDef.type == "Lcom/google/android/apps/youtube/music/activities/MusicActivity;" &&
- method.name == "attachBaseContext"
- }
-}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
index 840bf6be2..2a77ff5d3 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
@@ -22,7 +22,6 @@ import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
-import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
@@ -31,6 +30,7 @@ import app.revanced.patches.shared.indexOfBrandInstruction
import app.revanced.patches.shared.indexOfManufacturerInstruction
import app.revanced.patches.shared.indexOfModelInstruction
import app.revanced.patches.shared.indexOfReleaseInstruction
+import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
@@ -63,9 +63,11 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
@Suppress("unused")
val spoofClientPatch = bytecodePatch(
- SPOOF_CLIENT.title,
- SPOOF_CLIENT.summary,
- false,
+ // Removed from the patch list to avoid user confusion:
+ // https://github.com/inotia00/ReVanced_Extended/issues/2832#issuecomment-2745941171
+ // SPOOF_CLIENT.title,
+ // SPOOF_CLIENT.summary,
+ // false,
) {
compatibleWith(COMPATIBLE_PACKAGE)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
index e1b1bce52..7d0682188 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
@@ -9,9 +9,8 @@ import app.revanced.patches.shared.mapping.ResourceType.ID
import app.revanced.patches.shared.mapping.ResourceType.LAYOUT
import app.revanced.patches.shared.mapping.ResourceType.STRING
import app.revanced.patches.shared.mapping.ResourceType.STYLE
-import app.revanced.patches.shared.mapping.get
+import app.revanced.patches.shared.mapping.getResourceId
import app.revanced.patches.shared.mapping.resourceMappingPatch
-import app.revanced.patches.shared.mapping.resourceMappings
var accountSwitcherAccessibility = -1L
private set
@@ -25,6 +24,8 @@ var buttonContainer = -1L
private set
var buttonIconPaddingMedium = -1L
private set
+var channelHandle = -1L
+ private set
var chipCloud = -1L
private set
var colorGrey = -1L
@@ -83,6 +84,8 @@ var qualityAuto = -1L
private set
var remixGenericButtonSize = -1L
private set
+var searchButton = -1L
+ private set
var slidingDialogAnimation = -1L
private set
var tapBloomView = -1L
@@ -124,213 +127,61 @@ internal val sharedResourceIdPatch = resourcePatch(
dependsOn(resourceMappingPatch)
execute {
- accountSwitcherAccessibility = resourceMappings[
- STRING,
- "account_switcher_accessibility_label",
- ]
- actionBarLogo = resourceMappings[
- DRAWABLE,
- "action_bar_logo",
- ]
- actionBarLogoRingo2 = resourceMappings[
- DRAWABLE,
- "action_bar_logo_ringo2",
- ]
- bottomSheetRecyclerView = resourceMappings[
- LAYOUT,
- "bottom_sheet_recycler_view"
- ]
- buttonContainer = resourceMappings[
- ID,
- "button_container"
- ]
- buttonIconPaddingMedium = resourceMappings[
- DIMEN,
- "button_icon_padding_medium"
- ]
- chipCloud = resourceMappings[
- LAYOUT,
- "chip_cloud"
- ]
- colorGrey = resourceMappings[
- COLOR,
- "ytm_color_grey_12"
- ]
- darkBackground = resourceMappings[
- ID,
- "dark_background"
- ]
- designBottomSheetDialog = resourceMappings[
- LAYOUT,
- "design_bottom_sheet_dialog"
- ]
- elementsContainer = resourceMappings[
- ID,
- "elements_container"
- ]
- endButtonsContainer = resourceMappings[
- ID,
- "end_buttons_container"
- ]
- floatingLayout = resourceMappings[
- ID,
- "floating_layout"
- ]
- historyMenuItem = resourceMappings[
- ID,
- "history_menu_item"
- ]
- inlineTimeBarAdBreakMarkerColor = resourceMappings[
- COLOR,
- "inline_time_bar_ad_break_marker_color"
- ]
- inlineTimeBarProgressColor = resourceMappings[
- COLOR,
- "inline_time_bar_progress_color"
- ]
- interstitialsContainer = resourceMappings[
- ID,
- "interstitials_container"
- ]
- isTablet = resourceMappings[
- BOOL,
- "is_tablet"
- ]
- likeDislikeContainer = resourceMappings[
- ID,
- "like_dislike_container"
- ]
- mainActivityLaunchAnimation = resourceMappings[
- LAYOUT,
- "main_activity_launch_animation"
- ]
- menuEntry = resourceMappings[
- LAYOUT,
- "menu_entry"
- ]
- miniPlayerDefaultText = resourceMappings[
- STRING,
- "mini_player_default_text"
- ]
- miniPlayerMdxPlaying = resourceMappings[
- STRING,
- "mini_player_mdx_playing"
- ]
- miniPlayerPlayPauseReplayButton = resourceMappings[
- ID,
- "mini_player_play_pause_replay_button"
- ]
- miniPlayerViewPager = resourceMappings[
- ID,
- "mini_player_view_pager"
- ]
- modernDialogBackground = resourceMappings[
- DRAWABLE,
- "modern_dialog_background"
- ]
- musicNotifierShelf = resourceMappings[
- LAYOUT,
- "music_notifier_shelf"
- ]
- musicTasteBuilderShelf = resourceMappings[
- LAYOUT,
- "music_tastebuilder_shelf"
- ]
- namesInactiveAccountThumbnailSize = resourceMappings[
- DIMEN,
- "names_inactive_account_thumbnail_size"
- ]
- offlineSettingsMenuItem = resourceMappings[
- ID,
- "offline_settings_menu_item"
- ]
- playerOverlayChip = resourceMappings[
- ID,
- "player_overlay_chip"
- ]
- playerViewPager = resourceMappings[
- ID,
- "player_view_pager"
- ]
- privacyTosFooter = resourceMappings[
- ID,
- "privacy_tos_footer"
- ]
- qualityAuto = resourceMappings[
- STRING,
- "quality_auto"
- ]
- remixGenericButtonSize = resourceMappings[
- DIMEN,
- "remix_generic_button_size"
- ]
- slidingDialogAnimation = resourceMappings[
- STYLE,
- "SlidingDialogAnimation"
- ]
- tapBloomView = resourceMappings[
- ID,
- "tap_bloom_view"
- ]
- text1 = resourceMappings[
- ID,
- "text1"
- ]
- toolTipContentView = resourceMappings[
- LAYOUT,
- "tooltip_content_view"
- ]
- topEnd = resourceMappings[
- ID,
- "TOP_END"
- ]
- topStart = resourceMappings[
- ID,
- "TOP_START"
- ]
- topBarMenuItemImageView = resourceMappings[
- ID,
- "top_bar_menu_item_image_view"
- ]
- tosFooter = resourceMappings[
- ID,
- "tos_footer"
- ]
- touchOutside = resourceMappings[
- ID,
- "touch_outside"
- ]
- trimSilenceSwitch = resourceMappings[
- ID,
- "trim_silence_switch"
- ]
- varispeedUnavailableTitle = resourceMappings[
- STRING,
- "varispeed_unavailable_title"
- ]
- ytFillSamples = resourceMappings[
- DRAWABLE,
- "yt_fill_samples_vd_theme_24",
- ]
- ytFillYouTubeMusic = resourceMappings[
- DRAWABLE,
- "yt_fill_youtube_music_vd_theme_24",
- ]
- ytOutlineSamples = resourceMappings[
- DRAWABLE,
- "yt_outline_samples_vd_theme_24",
- ]
- ytOutlineYouTubeMusic = resourceMappings[
- DRAWABLE,
- "yt_outline_youtube_music_vd_theme_24",
- ]
- ytmLogo = resourceMappings[
- DRAWABLE,
- "ytm_logo",
- ]
- ytmLogoRingo2 = resourceMappings[
- DRAWABLE,
- "ytm_logo_ringo2",
- ]
+ accountSwitcherAccessibility = getResourceId(STRING, "account_switcher_accessibility_label")
+ actionBarLogo = getResourceId(DRAWABLE, "action_bar_logo")
+ actionBarLogoRingo2 = getResourceId(DRAWABLE, "action_bar_logo_ringo2")
+ bottomSheetRecyclerView = getResourceId(LAYOUT, "bottom_sheet_recycler_view")
+ buttonContainer = getResourceId(ID, "button_container")
+ buttonIconPaddingMedium = getResourceId(DIMEN, "button_icon_padding_medium")
+ channelHandle = getResourceId(ID, "channel_handle")
+ chipCloud = getResourceId(LAYOUT, "chip_cloud")
+ colorGrey = getResourceId(COLOR, "ytm_color_grey_12")
+ darkBackground = getResourceId(ID, "dark_background")
+ designBottomSheetDialog = getResourceId(LAYOUT, "design_bottom_sheet_dialog")
+ elementsContainer = getResourceId(ID, "elements_container")
+ endButtonsContainer = getResourceId(ID, "end_buttons_container")
+ floatingLayout = getResourceId(ID, "floating_layout")
+ historyMenuItem = getResourceId(ID, "history_menu_item")
+ inlineTimeBarAdBreakMarkerColor =
+ getResourceId(COLOR, "inline_time_bar_ad_break_marker_color")
+ inlineTimeBarProgressColor = getResourceId(COLOR, "inline_time_bar_progress_color")
+ interstitialsContainer = getResourceId(ID, "interstitials_container")
+ isTablet = getResourceId(BOOL, "is_tablet")
+ likeDislikeContainer = getResourceId(ID, "like_dislike_container")
+ mainActivityLaunchAnimation = getResourceId(LAYOUT, "main_activity_launch_animation")
+ menuEntry = getResourceId(LAYOUT, "menu_entry")
+ miniPlayerDefaultText = getResourceId(STRING, "mini_player_default_text")
+ miniPlayerMdxPlaying = getResourceId(STRING, "mini_player_mdx_playing")
+ miniPlayerPlayPauseReplayButton = getResourceId(ID, "mini_player_play_pause_replay_button")
+ miniPlayerViewPager = getResourceId(ID, "mini_player_view_pager")
+ modernDialogBackground = getResourceId(DRAWABLE, "modern_dialog_background")
+ musicNotifierShelf = getResourceId(LAYOUT, "music_notifier_shelf")
+ musicTasteBuilderShelf = getResourceId(LAYOUT, "music_tastebuilder_shelf")
+ namesInactiveAccountThumbnailSize =
+ getResourceId(DIMEN, "names_inactive_account_thumbnail_size")
+ offlineSettingsMenuItem = getResourceId(ID, "offline_settings_menu_item")
+ playerOverlayChip = getResourceId(ID, "player_overlay_chip")
+ playerViewPager = getResourceId(ID, "player_view_pager")
+ privacyTosFooter = getResourceId(ID, "privacy_tos_footer")
+ qualityAuto = getResourceId(STRING, "quality_auto")
+ remixGenericButtonSize = getResourceId(DIMEN, "remix_generic_button_size")
+ searchButton = getResourceId(LAYOUT, "search_button")
+ slidingDialogAnimation = getResourceId(STYLE, "SlidingDialogAnimation")
+ tapBloomView = getResourceId(ID, "tap_bloom_view")
+ text1 = getResourceId(ID, "text1")
+ toolTipContentView = getResourceId(LAYOUT, "tooltip_content_view")
+ topEnd = getResourceId(ID, "TOP_END")
+ topStart = getResourceId(ID, "TOP_START")
+ topBarMenuItemImageView = getResourceId(ID, "top_bar_menu_item_image_view")
+ tosFooter = getResourceId(ID, "tos_footer")
+ touchOutside = getResourceId(ID, "touch_outside")
+ trimSilenceSwitch = getResourceId(ID, "trim_silence_switch")
+ varispeedUnavailableTitle = getResourceId(STRING, "varispeed_unavailable_title")
+ ytFillSamples = getResourceId(DRAWABLE, "yt_fill_samples_vd_theme_24")
+ ytFillYouTubeMusic = getResourceId(DRAWABLE, "yt_fill_youtube_music_vd_theme_24")
+ ytOutlineSamples = getResourceId(DRAWABLE, "yt_outline_samples_vd_theme_24")
+ ytOutlineYouTubeMusic = getResourceId(DRAWABLE, "yt_outline_youtube_music_vd_theme_24")
+ ytmLogo = getResourceId(DRAWABLE, "ytm_logo")
+ ytmLogoRingo2 = getResourceId(DRAWABLE, "ytm_logo_ringo2")
}
}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/VideoTypeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/VideoTypeHookPatch.kt
index bc0ec4167..dc4643d26 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/VideoTypeHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/VideoTypeHookPatch.kt
@@ -25,7 +25,8 @@ val videoTypeHookPatch = bytecodePatch(
videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply {
val getEnumIndex = indexOfGetEnumInstruction(this)
- val enumClass = (getInstruction(getEnumIndex).reference as MethodReference).definingClass
+ val enumClass =
+ (getInstruction(getEnumIndex).reference as MethodReference).definingClass
val referenceIndex = indexOfFirstInstructionOrThrow(getEnumIndex) {
opcode == Opcode.SGET_OBJECT &&
getReference()?.type == enumClass
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
index 85e51a62b..647b9cc2b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
@@ -71,7 +71,8 @@ val playerResponseMethodHookPatch = bytecodePatch(
val beforeVideoIdHooks =
hooks.filterIsInstance().asReversed()
val videoIdHooks = hooks.filterIsInstance().asReversed()
- val videoIdAndPlaylistIdHooks = hooks.filterIsInstance().asReversed()
+ val videoIdAndPlaylistIdHooks =
+ hooks.filterIsInstance().asReversed()
val afterVideoIdHooks = hooks.filterIsInstance().asReversed()
// Add the hooks in this specific order as they insert instructions at the beginning of the method.
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt
index 51c8c7e54..74a75cc5e 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt
@@ -27,14 +27,6 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR =
"$PATCHES_PATH/GeneralAdsPatch;"
-private val isCommentAdsMethod: Method.() -> Boolean = {
- parameterTypes.size == 1 &&
- parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
- accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
- returnType == "V" &&
- indexOfFirstStringInstruction("ad") >= 0
-}
-
@Suppress("unused")
val adsPatch = bytecodePatch(
HIDE_ADS.title,
@@ -94,11 +86,20 @@ val adsPatch = bytecodePatch(
if (is_2025_06_or_greater) {
listOf(
commentAdCommentScreenAdViewFingerprint,
- commentAdDetailListHeaderViewFingerprint
+ commentAdDetailListHeaderViewFingerprint,
+ commentsViewModelFingerprint
).forEach { fingerprint ->
fingerprint.methodOrThrow().hook()
}
} else {
+ val isCommentAdsMethod: Method.() -> Boolean = {
+ parameterTypes.size == 1 &&
+ parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
+ accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
+ returnType == "V" &&
+ indexOfFirstStringInstruction("ad") >= 0
+ }
+
classes.forEach { classDef ->
classDef.methods.forEach { method ->
if (method.isCommentAdsMethod()) {
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt
index 70df18225..9d434fbd5 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt
@@ -8,6 +8,7 @@ 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.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
internal val adPostFingerprint = legacyFingerprint(
name = "adPostFingerprint",
@@ -49,6 +50,20 @@ internal val commentAdDetailListHeaderViewFingerprint = legacyFingerprint(
},
)
+internal val commentsViewModelFingerprint = legacyFingerprint(
+ name = "commentsViewModelFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("L", "Z", "L", "I"),
+ customFingerprint = { method, classDef ->
+ classDef.superclass == "Lcom/reddit/screen/presentation/CompositionViewModel;" &&
+ method.indexOfFirstInstruction {
+ opcode == Opcode.NEW_INSTANCE &&
+ getReference()?.type?.startsWith("Lcom/reddit/postdetail/comment/refactor/CommentsViewModel\$LoadAdsSeparately\$") == true
+ } >= 0
+ },
+)
+
internal val newAdPostFingerprint = legacyFingerprint(
name = "newAdPostFingerprint",
returnType = "L",
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt
index 473e2498d..4ab992ec7 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt
@@ -56,7 +56,8 @@ val navigationButtonsPatch = bytecodePatch(
if (bottomNavScreenFingerprint.resolvable()) {
val bottomNavScreenMutableClass = with(bottomNavScreenFingerprint.methodOrThrow()) {
val startIndex = indexOfGetDimensionPixelSizeInstruction(this)
- val targetIndex = indexOfFirstInstructionOrThrow(startIndex, Opcode.NEW_INSTANCE)
+ val targetIndex =
+ indexOfFirstInstructionOrThrow(startIndex, Opcode.NEW_INSTANCE)
val targetReference =
getInstruction(targetIndex).reference.toString()
@@ -65,7 +66,9 @@ val navigationButtonsPatch = bytecodePatch(
?: throw ClassNotFoundException("Failed to find class $targetReference")
}
- bottomNavScreenOnGlobalLayoutFingerprint.second.matchOrNull(bottomNavScreenMutableClass)
+ bottomNavScreenOnGlobalLayoutFingerprint.second.matchOrNull(
+ bottomNavScreenMutableClass
+ )
?.let {
it.method.apply {
val startIndex = it.patternMatch!!.startIndex
@@ -82,7 +85,8 @@ val navigationButtonsPatch = bytecodePatch(
// Legacy method.
bottomNavScreenHandlerFingerprint.methodOrThrow().apply {
val targetIndex = indexOfGetItemsInstruction(this) + 1
- val targetRegister = getInstruction(targetIndex).registerA
+ val targetRegister =
+ getInstruction(targetIndex).registerA
addInstructions(
targetIndex + 1, """
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt
index 13cc83e84..01f630b2b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt
@@ -1,36 +1,15 @@
package app.revanced.patches.reddit.layout.screenshotpopup
-import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
-import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
-import com.android.tools.smali.dexlib2.Opcode
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
-/**
- * Reddit 2025.06.0 ~
- */
-internal val screenshotTakenBannerFingerprint = legacyFingerprint(
+internal val screenshotBannerContainerFingerprint = legacyFingerprint(
name = "screenshotTakenBannerFingerprint",
- returnType = "L",
- opcodes = listOf(
- Opcode.CONST_4,
- Opcode.IF_NE,
- ),
- customFingerprint = { method, classDef ->
- method.containsLiteralInstruction(screenShotShareBanner) &&
- classDef.type.startsWith("Lcom/reddit/sharing/screenshot/composables/") &&
- method.name == "invoke"
- }
-)
-
-/**
- * ~ Reddit 2025.05.1
- */
-internal val screenshotTakenBannerLegacyFingerprint = legacyFingerprint(
- name = "screenshotTakenBannerLegacyFingerprint",
returnType = "V",
- parameters = listOf("Landroidx/compose/runtime/", "I"),
- customFingerprint = { method, classDef ->
- classDef.type.endsWith("\$ScreenshotTakenBannerKt\$lambda-1\$1;") &&
- method.name == "invoke"
- }
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ strings = listOf(
+ "bannerContainer",
+ "scope",
+ )
)
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt
index a8ceffdb9..4de050ce9 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt
@@ -2,22 +2,26 @@ package app.revanced.patches.reddit.layout.screenshotpopup
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH
import app.revanced.patches.reddit.utils.patch.PatchList.DISABLE_SCREENSHOT_POPUP
-import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
-import app.revanced.patches.reddit.utils.resourceid.sharedResourceIdPatch
-import app.revanced.patches.reddit.utils.settings.is_2025_06_or_greater
import app.revanced.patches.reddit.utils.settings.settingsPatch
import app.revanced.patches.reddit.utils.settings.updatePatchStatus
+import app.revanced.util.findMutableMethodOf
+import app.revanced.util.fingerprint.methodCall
import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionOrThrow
-import app.revanced.util.indexOfFirstInstructionReversedOrThrow
-import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
+import app.revanced.util.indexOfFirstStringInstruction
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.reference.MethodReference
private const val EXTENSION_METHOD_DESCRIPTOR =
"$PATCHES_PATH/ScreenshotPopupPatch;->disableScreenshotPopup()Z"
@@ -29,41 +33,80 @@ val screenshotPopupPatch = bytecodePatch(
) {
compatibleWith(COMPATIBLE_PACKAGE)
- dependsOn(
- settingsPatch,
- sharedResourceIdPatch,
- )
+ dependsOn(settingsPatch)
execute {
- if (is_2025_06_or_greater) {
- screenshotTakenBannerFingerprint.methodOrThrow().apply {
- val literalIndex = indexOfFirstLiteralInstructionOrThrow(screenShotShareBanner)
- val insertIndex = indexOfFirstInstructionReversedOrThrow(literalIndex, Opcode.CONST_4)
- val insertRegister = getInstruction(insertIndex).registerA
- val jumpIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.SGET_OBJECT)
+ val screenshotTriggerSharingListenerMethodCall =
+ screenshotBannerContainerFingerprint.methodCall()
- addInstructionsWithLabels(
- insertIndex, """
- invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
- move-result v$insertRegister
- if-nez v$insertRegister, :hidden
- """, ExternalLabel("hidden", getInstruction(jumpIndex))
- )
+ fun indexOfScreenshotTriggerInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ getReference()?.toString() == screenshotTriggerSharingListenerMethodCall
}
- } else {
- screenshotTakenBannerLegacyFingerprint.methodOrThrow().apply {
+
+ val isScreenshotTriggerMethod: Method.() -> Boolean = {
+ indexOfScreenshotTriggerInstruction(this) >= 0
+ }
+
+ var hookCount = 0
+
+ fun MutableMethod.hook() {
+ if (returnType == "V") {
addInstructionsWithLabels(
0, """
invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
move-result v0
- if-eqz v0, :dismiss
+ if-eqz v0, :shown
return-void
- """, ExternalLabel("dismiss", getInstruction(0))
+ """, ExternalLabel("shown", getInstruction(0))
)
+
+ hookCount++
+ } else if (returnType.startsWith("L")) { // Reddit 2025.06+
+ val insertIndex =
+ indexOfFirstStringInstruction("screenshotTriggerSharingListener")
+
+ if (insertIndex >= 0) {
+ val insertRegister =
+ getInstruction(insertIndex).registerA
+ val triggerIndex =
+ indexOfScreenshotTriggerInstruction(this)
+ val jumpIndex =
+ indexOfFirstInstructionOrThrow(triggerIndex, Opcode.RETURN_OBJECT)
+
+ addInstructionsWithLabels(
+ insertIndex, """
+ invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
+ move-result v$insertRegister
+ if-nez v$insertRegister, :hidden
+ """, ExternalLabel("hidden", getInstruction(jumpIndex))
+ )
+
+ hookCount++
+ }
}
}
+ screenshotBannerContainerFingerprint
+ .methodOrThrow()
+ .hook()
+
+ classes.forEach { classDef ->
+ classDef.methods.forEach { method ->
+ if (method.isScreenshotTriggerMethod()) {
+ proxy(classDef)
+ .mutableClass
+ .findMutableMethodOf(method)
+ .hook()
+ }
+ }
+ }
+
+ if (hookCount == 0) {
+ throw PatchException("Failed to find hook method")
+ }
+
updatePatchStatus(
"enableScreenshotPopup",
DISABLE_SCREENSHOT_POPUP
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt
index 7d51cb4a0..ccd7e4064 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt
@@ -42,7 +42,8 @@ val subRedditDialogPatch = bytecodePatch(
.apply {
listOfIsLoggedInInstruction(this)
.forEach { index ->
- val register = getInstruction(index + 1).registerA
+ val register =
+ getInstruction(index + 1).registerA
addInstructions(
index + 2, """
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt
index 9589f9dd7..acb655efa 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt
@@ -10,7 +10,8 @@ internal object Constants {
REDDIT_PACKAGE_NAME,
setOf(
"2024.17.0", // This is the last version that can be patched without anti-split.
- "2025.05.1", // This is the latest version supported by the RVX patch.
+ "2025.05.1", // This was the latest version supported by the previous RVX patch.
+ "2025.12.0", // This is the latest version supported by the RVX patch.
)
)
}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt
deleted file mode 100644
index 8fcffa95d..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package app.revanced.patches.reddit.utils.resourceid
-
-import app.revanced.patcher.patch.resourcePatch
-import app.revanced.patches.shared.mapping.ResourceType.STRING
-import app.revanced.patches.shared.mapping.get
-import app.revanced.patches.shared.mapping.resourceMappingPatch
-import app.revanced.patches.shared.mapping.resourceMappings
-
-var screenShotShareBanner = -1L
- private set
-
-internal val sharedResourceIdPatch = resourcePatch(
- description = "sharedResourceIdPatch"
-) {
- dependsOn(resourceMappingPatch)
-
- execute {
- screenShotShareBanner = resourceMappings[
- STRING,
- "screenshot_share_banner_title",
- ]
- }
-}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt
index 55f2354d3..16c749ce0 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt
@@ -62,7 +62,7 @@ fun baseAdsPatch(
)
}
- val getAdvertisingIdMethod = with (advertisingIdFingerprint.methodOrThrow()) {
+ val getAdvertisingIdMethod = with(advertisingIdFingerprint.methodOrThrow()) {
val getAdvertisingIdIndex = indexOfGetAdvertisingIdInstruction(this)
getWalkerMethod(getAdvertisingIdIndex)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/extension/hooks/CronetEngineContextHook.kt b/patches/src/main/kotlin/app/revanced/patches/shared/extension/hooks/CronetEngineContextHook.kt
deleted file mode 100644
index e31cf801b..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/shared/extension/hooks/CronetEngineContextHook.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package app.revanced.patches.shared.extension.hooks
-
-import app.revanced.patches.shared.extension.extensionHook
-import app.revanced.util.getReference
-import app.revanced.util.indexOfFirstInstruction
-import app.revanced.util.indexOfFirstInstructionOrThrow
-import com.android.tools.smali.dexlib2.AccessFlags
-import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction3rc
-import com.android.tools.smali.dexlib2.iface.reference.MethodReference
-
-private var initIndex = -1
-private var isRange = true
-
-internal val cronetEngineContextHook = extensionHook(
- insertIndexResolver = { method ->
- initIndex = method.indexOfFirstInstruction(Opcode.INVOKE_DIRECT_RANGE)
-
- if (initIndex < 0) {
- initIndex = method.indexOfFirstInstructionOrThrow(Opcode.INVOKE_DIRECT)
- isRange = false
- }
-
- initIndex
- },
- contextRegisterResolver = { method ->
- val initInstruction =
- method.implementation!!.instructions.elementAt(initIndex)
- if (isRange) {
- val overrideInstruction = initInstruction as Instruction3rc
- "v${overrideInstruction.startRegister + 1}"
- } else {
- val overrideInstruction = initInstruction as FiveRegisterInstruction
- "v${overrideInstruction.registerD}"
- }
- },
-) {
- returns("Lorg/chromium/net/CronetEngine;")
- accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
- strings("Could not create CronetEngine")
- custom { method, classDef ->
- method.indexOfFirstInstruction {
- (opcode == Opcode.INVOKE_DIRECT || opcode == Opcode.INVOKE_DIRECT_RANGE) &&
- getReference()?.parameterTypes?.firstOrNull() == "Landroid/content/Context;"
- } >= 0
- }
-}
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/extension/hooks/FirebaseInitProviderContextHook.kt b/patches/src/main/kotlin/app/revanced/patches/shared/extension/hooks/FirebaseInitProviderContextHook.kt
deleted file mode 100644
index e52f23dfa..000000000
--- a/patches/src/main/kotlin/app/revanced/patches/shared/extension/hooks/FirebaseInitProviderContextHook.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package app.revanced.patches.shared.extension.hooks
-
-import app.revanced.patches.shared.extension.extensionHook
-import app.revanced.util.getReference
-import app.revanced.util.indexOfFirstInstruction
-import com.android.tools.smali.dexlib2.Opcode
-import com.android.tools.smali.dexlib2.iface.Method
-import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
-import com.android.tools.smali.dexlib2.iface.reference.MethodReference
-
-private var getResourcesIndex = -1
-
-internal val firebaseInitProviderContextHook = extensionHook(
- insertIndexResolver = { method ->
- getResourcesIndex = indexOfGerResourcesInstruction(method)
-
- getResourcesIndex + 2
- },
- contextRegisterResolver = { method ->
- val overrideInstruction =
- method.implementation!!.instructions.elementAt(getResourcesIndex)
- as FiveRegisterInstruction
-
- "v${overrideInstruction.registerC}"
- },
-) {
- strings("firebase_database_url")
- custom { method, _ ->
- indexOfGerResourcesInstruction(method) >= 0
- }
-}
-
-private fun indexOfGerResourcesInstruction(method: Method) =
- method.indexOfFirstInstruction {
- opcode == Opcode.INVOKE_VIRTUAL &&
- getReference()?.toString() =="Landroid/content/Context;->getResources()Landroid/content/res/Resources;"
- }
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt
index 952b0c350..1e79c58e2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt
@@ -6,8 +6,6 @@ import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.or
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.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt
index 0a5d12a9a..9fb601010 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt
@@ -83,9 +83,9 @@ fun gmsCoreSupportPatch(
key = "gmsCoreVendorGroupId",
default = "app.revanced",
values =
- mapOf(
- "ReVanced" to "app.revanced",
- ),
+ mapOf(
+ "ReVanced" to "app.revanced",
+ ),
title = "GmsCore vendor group ID",
description = "The vendor's group ID for GmsCore.",
required = true,
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/mapping/ResourceMappingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/mapping/ResourceMappingPatch.kt
index f48e8f201..03c4b24af 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/mapping/ResourceMappingPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/mapping/ResourceMappingPatch.kt
@@ -2,71 +2,50 @@ package app.revanced.patches.shared.mapping
import app.revanced.patcher.patch.resourcePatch
import org.w3c.dom.Element
-import java.util.Collections
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-// TODO: Probably renaming the patch/this is a good idea.
-lateinit var resourceMappings: List
- private set
+data class ResourceElement(val type: String, val name: String, val id: Long)
+
+private lateinit var resourceMappings: MutableMap
+
+private fun setResourceId(type: String, name: String, id: Long) {
+ resourceMappings[type + name] = ResourceElement(type, name, id)
+}
+
+fun getResourceId(resourceType: ResourceType, name: String) =
+ getResourceId(resourceType.value, name)
+
+/**
+ * @return A resource id of the given resource type and name.
+ * @throws PatchException if the resource is not found.
+ */
+fun getResourceId(type: String, name: String) = resourceMappings[type + name]?.id
+ ?: -1L
val resourceMappingPatch = resourcePatch(
description = "resourceMappingPatch"
) {
- val threadCount = Runtime.getRuntime().availableProcessors()
- val threadPoolExecutor = Executors.newFixedThreadPool(threadCount)
-
- val resourceMappings = Collections.synchronizedList(mutableListOf())
-
execute {
- // Save the file in memory to concurrently read from it.
- val resourceXmlFile = get("res/values/public.xml").readBytes()
+ document("res/values/public.xml").use { document ->
+ val resources = document.documentElement.childNodes
+ val resourcesLength = resources.length
+ resourceMappings = HashMap(2 * resourcesLength)
- for (threadIndex in 0 until threadCount) {
- threadPoolExecutor.execute thread@{
- document(resourceXmlFile.inputStream()).use { document ->
+ for (i in 0 until resourcesLength) {
+ val node = resources.item(i) as? Element ?: continue
+ if (node.nodeName != "public") continue
- val resources = document.documentElement.childNodes
- val resourcesLength = resources.length
- val jobSize = resourcesLength / threadCount
+ val nameAttribute = node.getAttribute("name")
+ if (nameAttribute.startsWith("APKTOOL")) continue
- val batchStart = jobSize * threadIndex
- val batchEnd = jobSize * (threadIndex + 1)
- element@ for (i in batchStart until batchEnd) {
- // Prevent out of bounds.
- if (i >= resourcesLength) return@thread
+ val typeAttribute = node.getAttribute("type")
+ val id = node.getAttribute("id").substring(2).toLong(16)
- val node = resources.item(i)
- if (node !is Element) continue
-
- val nameAttribute = node.getAttribute("name")
- val typeAttribute = node.getAttribute("type")
-
- if (node.nodeName != "public" || nameAttribute.startsWith("APKTOOL")) continue
-
- val id = node.getAttribute("id").substring(2).toLong(16)
-
- resourceMappings.add(ResourceElement(typeAttribute, nameAttribute, id))
- }
- }
+ setResourceId(typeAttribute, nameAttribute, id)
}
}
-
- threadPoolExecutor.also { it.shutdown() }.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS)
-
- app.revanced.patches.shared.mapping.resourceMappings = resourceMappings
}
}
-operator fun List.get(type: String, name: String) = resourceMappings.firstOrNull {
- it.type == type && it.name == name
-}?.id ?: -1L
-
-operator fun List.get(resourceType: ResourceType, name: String) =
- get(resourceType.value, name)
-
-data class ResourceElement(val type: String, val name: String, val id: Long)
-
enum class ResourceType(val value: String) {
ATTR("attr"),
BOOL("bool"),
diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt
index 82ca978e6..39e251e22 100644
--- a/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt
@@ -147,7 +147,8 @@ fun ResourcePatchContext.baseTranslationsPatch(
val length = text.length
if (!text.endsWith("DEFAULT") &&
length >= 2 &&
- text.subSequence(length - 2, length) !in filteredAppLanguages) {
+ text.subSequence(length - 2, length) !in filteredAppLanguages
+ ) {
nodesToRemove.add(item)
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/Fingerprints.kt
index 315b1d9ac..c8aa32933 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/Fingerprints.kt
@@ -57,8 +57,6 @@ internal val showDialogCommandFingerprint = legacyFingerprint(
name = "showDialogCommandFingerprint",
returnType = "V",
opcodes = listOf(
- Opcode.IF_EQ,
- Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IGET, // get dialog code
),
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt
index 655606d54..736e1d580 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt
@@ -262,8 +262,10 @@ val feedComponentsPatch = bytecodePatch(
val insertIndex = indexOfBufferParserInstruction(this)
if (is_19_46_or_greater) {
- val objectIndex = indexOfFirstInstructionReversedOrThrow(insertIndex, Opcode.IGET_OBJECT)
- val objectRegister = getInstruction(objectIndex).registerA
+ val objectIndex =
+ indexOfFirstInstructionReversedOrThrow(insertIndex, Opcode.IGET_OBJECT)
+ val objectRegister =
+ getInstruction(objectIndex).registerA
addInstructionsWithLabels(
insertIndex, """
@@ -275,7 +277,8 @@ val feedComponentsPatch = bytecodePatch(
)
} else {
val objectIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_OBJECT)
- val objectRegister = getInstruction(objectIndex).registerA
+ val objectRegister =
+ getInstruction(objectIndex).registerA
val jumpIndex = it.patternMatch!!.startIndex
addInstructionsWithLabels(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt
index e9e4a2e29..8782607c6 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt
@@ -6,6 +6,7 @@ import app.revanced.patches.youtube.utils.resourceid.compactListItem
import app.revanced.patches.youtube.utils.resourceid.editSettingsAction
import app.revanced.patches.youtube.utils.resourceid.fab
import app.revanced.patches.youtube.utils.resourceid.toolTipContentView
+import app.revanced.patches.youtube.utils.resourceid.ytCallToAction
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionReversed
@@ -19,13 +20,7 @@ internal val accountListFingerprint = legacyFingerprint(
name = "accountListFingerprint",
returnType = "V",
accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL or AccessFlags.SYNTHETIC,
- opcodes = listOf(
- Opcode.IGET_OBJECT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.IGET_OBJECT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.IGET
- )
+ literals = listOf(ytCallToAction),
)
internal val accountListParentFingerprint = legacyFingerprint(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt
index 7782e7741..a48e5eab6 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt
@@ -20,6 +20,7 @@ import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.resourceid.accountSwitcherAccessibility
import app.revanced.patches.youtube.utils.resourceid.fab
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
+import app.revanced.patches.youtube.utils.resourceid.ytCallToAction
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
@@ -102,6 +103,7 @@ val layoutComponentsPatch = bytecodePatch(
"$GENERAL_CLASS_DESCRIPTOR->disableTranslucentStatusBar(Z)Z"
)
+ settingArray += "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS"
settingArray += "SETTINGS: DISABLE_TRANSLUCENT_STATUS_BAR"
}
@@ -122,17 +124,19 @@ val layoutComponentsPatch = bytecodePatch(
// region patch for hide account menu
// for you tab
- accountListFingerprint.matchOrThrow(accountListParentFingerprint).let {
- it.method.apply {
- val targetIndex = it.patternMatch!!.startIndex + 3
- val targetInstruction = getInstruction(targetIndex)
-
- addInstruction(
- targetIndex,
- "invoke-static {v${targetInstruction.registerC}, v${targetInstruction.registerD}}, " +
- "$GENERAL_CLASS_DESCRIPTOR->hideAccountList(Landroid/view/View;Ljava/lang/CharSequence;)V"
- )
+ accountListFingerprint.methodOrThrow(accountListParentFingerprint).apply {
+ val literalIndex = indexOfFirstLiteralInstructionOrThrow(ytCallToAction)
+ val targetIndex = indexOfFirstInstructionOrThrow(literalIndex) {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.name == "setText"
}
+ val targetInstruction = getInstruction(targetIndex)
+
+ addInstruction(
+ targetIndex,
+ "invoke-static {v${targetInstruction.registerC}, v${targetInstruction.registerD}}, " +
+ "$GENERAL_CLASS_DESCRIPTOR->hideAccountList(Landroid/view/View;Ljava/lang/CharSequence;)V"
+ )
}
// for tablet and old clients
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt
index d6f4d54b4..7a36adba1 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt
@@ -11,6 +11,7 @@ import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PAC
import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH
import app.revanced.patches.youtube.utils.patch.PatchList.HOOK_DOWNLOAD_ACTIONS
import app.revanced.patches.youtube.utils.pip.pipStateHookPatch
+import app.revanced.patches.youtube.utils.playlist.playlistPatch
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
@@ -41,6 +42,7 @@ val downloadActionsPatch = bytecodePatch(
dependsOn(
pipStateHookPatch,
+ playlistPatch,
sharedResourceIdPatch,
settingsPatch,
)
@@ -52,7 +54,7 @@ val downloadActionsPatch = bytecodePatch(
offlineVideoEndpointFingerprint.methodOrThrow().apply {
addInstructionsWithLabels(
0, """
- invoke-static/range {p3 .. p3}, $EXTENSION_CLASS_DESCRIPTOR->inAppVideoDownloadButtonOnClick(Ljava/lang/String;)Z
+ invoke-static/range {p1 .. p3}, $EXTENSION_CLASS_DESCRIPTOR->inAppVideoDownloadButtonOnClick(Ljava/util/Map;Ljava/lang/Object;Ljava/lang/String;)Z
move-result v0
if-eqz v0, :show_native_downloader
return-void
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt
index cb105e27d..fa987061f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatch.kt
@@ -90,7 +90,8 @@ val openChannelOfLiveAvatarPatch = bytecodePatch(
)
val playbackStartIndex = indexOfPlaybackStartDescriptorInstruction(this) + 1
- val playbackStartRegister = getInstruction(playbackStartIndex).registerA
+ val playbackStartRegister =
+ getInstruction(playbackStartIndex).registerA
val mapIndex = indexOfFirstInstructionOrThrow(playbackStartIndex) {
val reference = getReference()
@@ -169,15 +170,24 @@ val openChannelOfLiveAvatarPatch = bytecodePatch(
val playbackStartIndex = indexOfFirstInstructionOrThrow {
getReference()?.returnType == PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
}
- val mapIndex = indexOfFirstInstructionReversedOrThrow(playbackStartIndex, Opcode.IPUT)
+ val mapIndex =
+ indexOfFirstInstructionReversedOrThrow(playbackStartIndex, Opcode.IPUT)
val mapRegister = getInstruction(mapIndex).registerA
- val playbackStartRegister = getInstruction(playbackStartIndex + 1).registerA
- val videoIdRegister = getInstruction(playbackStartIndex).registerC
+ val playbackStartRegister =
+ getInstruction(playbackStartIndex + 1).registerA
+ val videoIdRegister =
+ getInstruction(playbackStartIndex).registerC
addInstructionsWithLabels(
playbackStartIndex + 2, """
move-object/from16 v$mapRegister, p2
- ${fetchChannelIdInstructions(playbackStartRegister, mapRegister, videoIdRegister)}
+ ${
+ fetchChannelIdInstructions(
+ playbackStartRegister,
+ mapRegister,
+ videoIdRegister
+ )
+ }
"""
)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt
index 33821a219..23fbde7f5 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt
@@ -1,6 +1,7 @@
package app.revanced.patches.youtube.general.navigation
import app.revanced.patches.youtube.utils.resourceid.ytFillBell
+import app.revanced.patches.youtube.utils.resourceid.ytOutlineLibrary
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
@@ -74,6 +75,11 @@ internal val setEnumMapFingerprint = legacyFingerprint(
literals = listOf(ytFillBell),
)
+internal val setEnumMapSecondaryFingerprint = legacyFingerprint(
+ name = "setEnumMapSecondaryFingerprint",
+ literals = listOf(ytOutlineLibrary),
+)
+
internal const val TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAG = 45630927L
internal val translucentNavigationBarFingerprint = legacyFingerprint(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt
index ee584d225..e96975ac3 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt
@@ -15,6 +15,7 @@ import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_28_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
+import app.revanced.patches.youtube.utils.resourceid.ytOutlineLibrary
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.util.ResourceGroup
@@ -25,6 +26,7 @@ import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
+import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import app.revanced.util.indexOfFirstStringInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
@@ -56,6 +58,14 @@ private val navigationBarComponentsResourcePatch = resourcePatch(
)
)
}
+
+ copyResources(
+ "youtube/navigationbuttons",
+ ResourceGroup(
+ "drawable-xxxhdpi",
+ "yt_outline_library_cairo_black_24.png"
+ )
+ )
}
}
}
@@ -204,6 +214,18 @@ val navigationBarComponentsPatch = bytecodePatch(
"""
)
}
+
+ setEnumMapSecondaryFingerprint.methodOrThrow().apply {
+ val index = indexOfFirstLiteralInstructionOrThrow(ytOutlineLibrary)
+ val register = getInstruction(index).registerA
+
+ addInstructions(
+ index + 1, """
+ invoke-static {v$register}, $GENERAL_CLASS_DESCRIPTOR->getLibraryDrawableId(I)I
+ move-result v$register
+ """
+ )
+ }
}
// endregion
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/snackbar/SnackBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/snackbar/SnackBarComponentsPatch.kt
index 83821d0e9..1ae0ee983 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/snackbar/SnackBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/snackbar/SnackBarComponentsPatch.kt
@@ -80,7 +80,8 @@ private val snackBarComponentsBytecodePatch = bytecodePatch(
bottomUiContainerThemeFingerprint.matchOrThrow().let {
it.method.apply {
val darkThemeIndex = it.patternMatch!!.startIndex + 2
- val darkThemeReference = getInstruction(darkThemeIndex).reference.toString()
+ val darkThemeReference =
+ getInstruction(darkThemeIndex).reference.toString()
implementation!!.instructions
.withIndex()
@@ -91,7 +92,8 @@ private val snackBarComponentsBytecodePatch = bytecodePatch(
.map { (index, _) -> index }
.reversed()
.forEach { index ->
- val appThemeIndex = indexOfFirstInstructionReversedOrThrow(index, Opcode.MOVE_RESULT_OBJECT)
+ val appThemeIndex =
+ indexOfFirstInstructionReversedOrThrow(index, Opcode.MOVE_RESULT_OBJECT)
val appThemeRegister =
getInstruction(appThemeIndex).registerA
val darkThemeRegister =
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt
index 5177e36c7..ae0259731 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt
@@ -16,7 +16,7 @@ import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_APP_VERSION
import app.revanced.patches.youtube.utils.playservice.is_18_34_or_greater
import app.revanced.patches.youtube.utils.playservice.is_18_39_or_greater
import app.revanced.patches.youtube.utils.playservice.is_18_49_or_greater
-import app.revanced.patches.youtube.utils.playservice.is_19_17_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_19_01_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_28_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater
@@ -45,6 +45,15 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
dependsOn(versionCheckPatch)
execute {
+ if (is_19_01_or_greater) {
+ findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
+ name == "SpoofAppVersionDefaultString"
+ }.replaceInstruction(
+ 0,
+ "const-string v0, \"19.01.34\""
+ )
+ }
+
if (!is_19_23_or_greater) {
return@execute
}
@@ -72,13 +81,6 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
""", ExternalLabel("ignore", getInstruction(jumpIndex))
)
}
-
- findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
- name == "SpoofAppVersionDefaultString"
- }.replaceInstruction(
- 0,
- "const-string v0, \"18.38.45\""
- )
}
}
@@ -108,35 +110,43 @@ val spoofAppVersionPatch = resourcePatch(
SPOOF_APP_VERSION
)
- if (!is_19_17_or_greater) {
+ // TODO: Remove this when the legacy code for YouTube 18.xx is cleaned up.
+ if (!is_19_01_or_greater) {
appendAppVersion("17.41.37")
appendAppVersion("18.05.40")
appendAppVersion("18.17.43")
- if (!is_18_34_or_greater) {
+
+ if (is_18_34_or_greater) {
+ appendAppVersion("18.33.40")
+ } else {
return@execute
}
- appendAppVersion("18.33.40")
- }
- if (!is_18_39_or_greater) {
+ if (is_18_39_or_greater) {
+ appendAppVersion("18.38.45")
+ } else {
+ return@execute
+ }
+
+ if (is_18_49_or_greater) {
+ appendAppVersion("18.48.39")
+ }
+
return@execute
}
- appendAppVersion("18.38.45")
- if (!is_18_49_or_greater) {
+ appendAppVersion("19.01.34")
+
+ if (is_19_28_or_greater) {
+ appendAppVersion("19.26.42")
+ } else {
return@execute
}
- appendAppVersion("18.48.39")
- if (!is_19_28_or_greater) {
+ if (is_19_34_or_greater) {
+ appendAppVersion("19.33.37")
+ } else {
return@execute
}
- appendAppVersion("19.26.42")
-
- if (!is_19_34_or_greater) {
- return@execute
- }
- appendAppVersion("19.33.37")
-
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
index f928fd94d..49753224d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
@@ -38,6 +38,7 @@ import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow
import app.revanced.util.getReference
import app.revanced.util.getWalkerMethod
+import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import app.revanced.util.replaceLiteralInstructionCall
@@ -268,29 +269,54 @@ val toolBarComponentsPatch = bytecodePatch(
createSearchSuggestionsFingerprint.methodOrThrow().apply {
val iteratorIndex = indexOfIteratorInstruction(this)
- val replaceIndex = indexOfFirstInstructionOrThrow(iteratorIndex) {
+ val replaceIndex = indexOfFirstInstruction(iteratorIndex) {
opcode == Opcode.IGET_OBJECT &&
getReference()?.type == "Landroid/widget/ImageView;"
}
- val jumpIndex = indexOfFirstInstructionOrThrow(replaceIndex) {
- opcode == Opcode.INVOKE_STATIC &&
- getReference()?.toString() == "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;"
- } + 4
- val replaceIndexInstruction = getInstruction(replaceIndex)
- val freeRegister = replaceIndexInstruction.registerA
- val classRegister = replaceIndexInstruction.registerB
- val replaceIndexReference =
- getInstruction(replaceIndex).reference
+ if (replaceIndex > -1) {
+ val uriIndex = indexOfFirstInstructionOrThrow(replaceIndex) {
+ opcode == Opcode.INVOKE_STATIC &&
+ getReference()?.toString() == "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;"
+ }
+ val jumpIndex = indexOfFirstInstructionOrThrow(uriIndex, Opcode.CONST_4)
+ val replaceIndexInstruction = getInstruction(replaceIndex)
+ val freeRegister = replaceIndexInstruction.registerA
+ val classRegister = replaceIndexInstruction.registerB
+ val replaceIndexReference =
+ getInstruction(replaceIndex).reference
- addInstructionsWithLabels(
- replaceIndex + 1, """
- invoke-static { }, $GENERAL_CLASS_DESCRIPTOR->hideSearchTermThumbnail()Z
- move-result v$freeRegister
- if-nez v$freeRegister, :hidden
- iget-object v$freeRegister, v$classRegister, $replaceIndexReference
- """, ExternalLabel("hidden", getInstruction(jumpIndex))
- )
- removeInstruction(replaceIndex)
+ addInstructionsWithLabels(
+ replaceIndex + 1, """
+ invoke-static { }, $GENERAL_CLASS_DESCRIPTOR->hideSearchTermThumbnail()Z
+ move-result v$freeRegister
+ if-nez v$freeRegister, :hidden
+ iget-object v$freeRegister, v$classRegister, $replaceIndexReference
+ """, ExternalLabel("hidden", getInstruction(jumpIndex))
+ )
+ removeInstruction(replaceIndex)
+ } else { // only for YT 20.03
+ val insertIndex = indexOfFirstInstructionOrThrow(iteratorIndex) {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.toString() == "Landroid/widget/ImageView;->setVisibility(I)V"
+ } - 1
+ if (getInstruction(insertIndex).opcode != Opcode.CONST_4) {
+ throw PatchException("Failed to find insert index")
+ }
+ val freeRegister = getInstruction(insertIndex).registerA
+ val uriIndex = indexOfFirstInstructionOrThrow(insertIndex) {
+ opcode == Opcode.INVOKE_STATIC &&
+ getReference()?.toString() == "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;"
+ }
+ val jumpIndex = indexOfFirstInstructionOrThrow(uriIndex, Opcode.CONST_4)
+
+ addInstructionsWithLabels(
+ insertIndex, """
+ invoke-static { }, $GENERAL_CLASS_DESCRIPTOR->hideSearchTermThumbnail()Z
+ move-result v$freeRegister
+ if-nez v$freeRegister, :hidden
+ """, ExternalLabel("hidden", getInstruction(jumpIndex))
+ )
+ }
}
if (is_19_16_or_greater) {
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/updates/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/updates/Fingerprints.kt
new file mode 100644
index 000000000..beffc631c
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/updates/Fingerprints.kt
@@ -0,0 +1,17 @@
+package app.revanced.patches.youtube.general.updates
+
+import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
+
+internal val cronetHeaderFingerprint = legacyFingerprint(
+ name = "cronetHeaderFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("Ljava/lang/String;", "Ljava/lang/String;"),
+ strings = listOf("Accept-Encoding"),
+ // In YouTube 19.16.39 or earlier, there are two methods with almost the same structure.
+ // Check the fields of the class to identify them correctly.
+ customFingerprint = { _, classDef ->
+ classDef.fields.find { it.type == "J" } != null
+ }
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/updates/LayoutUpdatesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/updates/LayoutUpdatesPatch.kt
new file mode 100644
index 000000000..1543fc746
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/updates/LayoutUpdatesPatch.kt
@@ -0,0 +1,50 @@
+package app.revanced.patches.youtube.general.updates
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR
+import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_LAYOUT_UPDATES
+import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
+import app.revanced.patches.youtube.utils.settings.settingsPatch
+import app.revanced.util.fingerprint.matchOrThrow
+
+@Suppress("unused")
+val layoutUpdatesPatch = bytecodePatch(
+ DISABLE_LAYOUT_UPDATES.title,
+ DISABLE_LAYOUT_UPDATES.summary,
+) {
+ compatibleWith(COMPATIBLE_PACKAGE)
+
+ dependsOn(settingsPatch)
+
+ execute {
+
+ cronetHeaderFingerprint.matchOrThrow().let {
+ it.method.apply {
+ val index = it.stringMatches!!.first().index
+
+ addInstructions(
+ index, """
+ invoke-static {p1, p2}, $GENERAL_CLASS_DESCRIPTOR->disableLayoutUpdates(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
+ move-result-object p2
+ """
+ )
+ }
+ }
+
+ // region add settings
+
+ addPreference(
+ arrayOf(
+ "PREFERENCE_SCREEN: GENERAL",
+ "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS",
+ "SETTINGS: DISABLE_LAYOUT_UPDATES"
+ ),
+ DISABLE_LAYOUT_UPDATES
+ )
+
+ // endregion
+
+ }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt
index af1a960b9..6cb122b1d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt
@@ -86,7 +86,11 @@ val shortsActionButtonsPatch = resourcePatch(
// Some directory is missing in the bundles.
if (inputStreamForLegacy != null && fromFileResolved.exists()) {
- Files.copy(inputStreamForLegacy, fromFileResolved.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ Files.copy(
+ inputStreamForLegacy,
+ fromFileResolved.toPath(),
+ StandardCopyOption.REPLACE_EXISTING
+ )
}
if (is_19_36_or_greater) {
@@ -95,7 +99,11 @@ val shortsActionButtonsPatch = resourcePatch(
// Some directory is missing in the bundles.
if (inputStreamForNew != null && toFileResolved.exists()) {
- Files.copy(inputStreamForNew, toFileResolved.toPath(), StandardCopyOption.REPLACE_EXISTING)
+ Files.copy(
+ inputStreamForNew,
+ toFileResolved.toPath(),
+ StandardCopyOption.REPLACE_EXISTING
+ )
}
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
index a4cedb451..1ccc3e575 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
@@ -10,6 +10,7 @@ import app.revanced.patches.youtube.utils.playservice.is_19_17_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_32_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
+import app.revanced.patches.youtube.utils.settings.ResourceUtils.restoreOldSplashAnimationIncluded
import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusIcon
import app.revanced.patches.youtube.utils.settings.getBytecodeContext
import app.revanced.patches.youtube.utils.settings.settingsPatch
@@ -208,22 +209,38 @@ val customBrandingIconPatch = resourcePatch(
// Change splash screen.
if (restoreOldSplashAnimationOption == true) {
+ restoreOldSplashAnimationIncluded = true
+
oldSplashAnimationResourceGroups.let { resourceGroups ->
resourceGroups.forEach {
copyResources("$appIconResourcePath/splash", it)
}
}
- val styleMap = mutableMapOf()
- styleMap["Base.Theme.YouTube.Launcher"] =
- "@style/Theme.AppCompat.DayNight.NoActionBar"
+ val styleList = if (is_19_32_or_greater)
+ listOf(
+ Triple(
+ "values-night-v31",
+ "Theme.YouTube.Home",
+ "@style/Base.V27.Theme.YouTube.Home"
+ ),
+ Triple(
+ "values-v31",
+ "Theme.YouTube.Home",
+ "@style/Base.V27.Theme.YouTube.Home"
+ ),
+ )
+ else
+ listOf(
+ Triple(
+ "values-v31",
+ "Base.Theme.YouTube.Launcher",
+ "@style/Theme.AppCompat.DayNight.NoActionBar"
+ ),
+ )
- if (is_19_32_or_greater) {
- styleMap["Theme.YouTube.Home"] = "@style/Base.V27.Theme.YouTube.Home"
- }
-
- styleMap.forEach { (nodeAttributeName, nodeAttributeParent) ->
- document("res/values-v31/styles.xml").use { document ->
+ styleList.forEach { (directory, nodeAttributeName, nodeAttributeParent) ->
+ document("res/$directory/styles.xml").use { document ->
val resourcesNode =
document.getElementsByTagName("resources").item(0) as Element
@@ -231,21 +248,27 @@ val customBrandingIconPatch = resourcePatch(
style.setAttribute("name", nodeAttributeName)
style.setAttribute("parent", nodeAttributeParent)
- val primaryItem = document.createElement("item")
- primaryItem.setAttribute("name", "android:windowSplashScreenAnimatedIcon")
- primaryItem.textContent = "@drawable/avd_anim"
- val secondaryItem = document.createElement("item")
- secondaryItem.setAttribute(
+ val splashScreenAnimatedIcon = document.createElement("item")
+ splashScreenAnimatedIcon.setAttribute(
+ "name",
+ "android:windowSplashScreenAnimatedIcon"
+ )
+ splashScreenAnimatedIcon.textContent = "@drawable/avd_anim"
+
+ // Deprecated in Android 13+
+ val splashScreenAnimationDuration = document.createElement("item")
+ splashScreenAnimationDuration.setAttribute(
"name",
"android:windowSplashScreenAnimationDuration"
)
- secondaryItem.textContent = if (appIcon.startsWith("revancify"))
- "1500"
- else
- "1000"
+ splashScreenAnimationDuration.textContent =
+ if (appIcon.startsWith("revancify"))
+ "1500"
+ else
+ "1000"
- style.appendChild(primaryItem)
- style.appendChild(secondaryItem)
+ style.appendChild(splashScreenAnimatedIcon)
+ style.appendChild(splashScreenAnimationDuration)
resourcesNode.appendChild(style)
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt
index a27467bcd..cdee030cd 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt
@@ -61,8 +61,10 @@ val sharedThemePatch = resourcePatch(
0 -> when (nodeAttributeName) {
"Base.Theme.YouTube.Launcher.Dark",
"Base.Theme.YouTube.Launcher.Cairo.Dark" -> "@color/yt_black1"
+
"Base.Theme.YouTube.Launcher.Light",
"Base.Theme.YouTube.Launcher.Cairo.Light" -> "@color/yt_white1"
+
else -> "null"
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt
index a72aa6957..a9b768e9d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt
@@ -11,8 +11,8 @@ import app.revanced.patches.youtube.utils.settings.settingsPatch
// Array of supported translations, each represented by its language code.
private val SUPPORTED_TRANSLATIONS = setOf(
- "ar", "bg-rBG", "de-rDE", "el-rGR", "es-rES", "fr-rFR", "hu-rHU", "it-rIT", "ja-rJP", "ko-rKR",
- "pl-rPL", "pt-rBR", "ru-rRU", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
+ "ar", "bg-rBG", "de-rDE", "el-rGR", "es-rES", "fr-rFR", "hu-rHU", "id-rID", "in", "it-rIT", "ja-rJP",
+ "ko-rKR", "pl-rPL", "pt-rBR", "ru-rRU", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
)
@Suppress("unused")
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/accessibility/AccessibilityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/accessibility/AccessibilityPatch.kt
index cf0098362..8a2fd8dde 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/accessibility/AccessibilityPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/accessibility/AccessibilityPatch.kt
@@ -32,8 +32,10 @@ val accessibilityPatch = bytecodePatch(
.methods
.first { method -> method.name == "" }
.apply {
- val lifecycleObserverIndex = indexOfFirstInstructionReversedOrThrow(Opcode.NEW_INSTANCE)
- val lifecycleObserverClass = getInstruction(lifecycleObserverIndex).reference.toString()
+ val lifecycleObserverIndex =
+ indexOfFirstInstructionReversedOrThrow(Opcode.NEW_INSTANCE)
+ val lifecycleObserverClass =
+ getInstruction(lifecycleObserverIndex).reference.toString()
findMethodOrThrow(lifecycleObserverClass) {
accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
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 c6380be34..4865bfbc1 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
@@ -85,17 +85,19 @@ val backgroundPlaybackPatch = bytecodePatch(
backgroundPlaybackManagerCairoFragmentPrimaryFingerprint,
backgroundPlaybackManagerCairoFragmentSecondaryFingerprint
).forEach { fingerprint ->
- fingerprint.matchOrThrow(backgroundPlaybackManagerCairoFragmentParentFingerprint).let {
- it.method.apply {
- val insertIndex = it.patternMatch!!.startIndex + 4
- val insertRegister = getInstruction(insertIndex).registerA
+ fingerprint.matchOrThrow(backgroundPlaybackManagerCairoFragmentParentFingerprint)
+ .let {
+ it.method.apply {
+ val insertIndex = it.patternMatch!!.startIndex + 4
+ val insertRegister =
+ getInstruction(insertIndex).registerA
- addInstruction(
- insertIndex,
- "const/4 v$insertRegister, 0x0"
- )
+ addInstruction(
+ insertIndex,
+ "const/4 v$insertRegister, 0x0"
+ )
+ }
}
- }
}
pipInputConsumerFeatureFlagFingerprint.injectLiteralInstructionBooleanCall(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt
index ce57443ec..692b78489 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt
@@ -57,20 +57,26 @@ val actionButtonsPatch = bytecodePatch(
findMethodOrThrow(parameters[1].type) {
name == "toString"
}
- val identifierReference = with (conversionContextToStringMethod) {
+ val identifierReference = with(conversionContextToStringMethod) {
val identifierStringIndex =
indexOfFirstStringInstructionOrThrow(", identifierProperty=")
val identifierStringAppendIndex =
indexOfFirstInstructionOrThrow(identifierStringIndex, Opcode.INVOKE_VIRTUAL)
- val identifierStringAppendIndexRegister = getInstruction(identifierStringAppendIndex).registerD
+ val identifierStringAppendIndexRegister =
+ getInstruction(identifierStringAppendIndex).registerD
val identifierAppendIndex =
- indexOfFirstInstructionOrThrow(identifierStringAppendIndex + 1, Opcode.INVOKE_VIRTUAL)
- val identifierRegister = getInstruction(identifierAppendIndex).registerD
- val identifierIndex = indexOfFirstInstructionReversedOrThrow(identifierAppendIndex) {
- opcode == Opcode.IGET_OBJECT &&
- getReference()?.type == "Ljava/lang/String;" &&
- (this as? TwoRegisterInstruction)?.registerA == identifierRegister
- }
+ indexOfFirstInstructionOrThrow(
+ identifierStringAppendIndex + 1,
+ Opcode.INVOKE_VIRTUAL
+ )
+ val identifierRegister =
+ getInstruction(identifierAppendIndex).registerD
+ val identifierIndex =
+ indexOfFirstInstructionReversedOrThrow(identifierAppendIndex) {
+ opcode == Opcode.IGET_OBJECT &&
+ getReference()?.type == "Ljava/lang/String;" &&
+ (this as? TwoRegisterInstruction)?.registerA == identifierRegister
+ }
getInstruction(identifierIndex).reference
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/Fingerprints.kt
index a36f776ab..ebfab4be0 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/Fingerprints.kt
@@ -10,11 +10,12 @@ import app.revanced.patches.youtube.utils.resourceid.endScreenElementLayoutCircl
import app.revanced.patches.youtube.utils.resourceid.endScreenElementLayoutIcon
import app.revanced.patches.youtube.utils.resourceid.endScreenElementLayoutVideo
import app.revanced.patches.youtube.utils.resourceid.offlineActionsVideoDeletedUndoSnackbarText
-import app.revanced.patches.youtube.utils.resourceid.scrubbing
import app.revanced.patches.youtube.utils.resourceid.seekEasyHorizontalTouchOffsetToStartScrubbing
import app.revanced.patches.youtube.utils.resourceid.suggestedAction
import app.revanced.patches.youtube.utils.resourceid.tapBloomView
import app.revanced.patches.youtube.utils.resourceid.touchArea
+import app.revanced.patches.youtube.utils.resourceid.verticalTouchOffsetToEnterFineScrubbing
+import app.revanced.patches.youtube.utils.resourceid.verticalTouchOffsetToStartFineScrubbing
import app.revanced.patches.youtube.utils.resourceid.videoZoomSnapIndicator
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
@@ -88,6 +89,8 @@ internal val speedOverlayFingerprint = legacyFingerprint(
literals = listOf(SPEED_OVERLAY_FEATURE_FLAG),
)
+internal const val SPEED_OVERLAY_LEGACY_FEATURE_FLAG = 45411328L
+
/**
* This value is the key for the playback speed overlay value.
* Deprecated in YouTube v19.18.41+.
@@ -97,7 +100,7 @@ internal val speedOverlayFloatValueFingerprint = legacyFingerprint(
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
opcodes = listOf(Opcode.DOUBLE_TO_FLOAT),
- literals = listOf(45411328L),
+ literals = listOf(SPEED_OVERLAY_LEGACY_FEATURE_FLAG),
)
internal val speedOverlayTextValueFingerprint = legacyFingerprint(
@@ -124,6 +127,9 @@ internal val crowdfundingBoxFingerprint = legacyFingerprint(
literals = listOf(donationCompanion),
)
+/**
+ * ~ YouTube 20.11
+ */
internal val filmStripOverlayConfigFingerprint = legacyFingerprint(
name = "filmStripOverlayConfigFingerprint",
returnType = "Z",
@@ -138,11 +144,48 @@ internal val filmStripOverlayInteractionFingerprint = legacyFingerprint(
parameters = listOf("L")
)
-internal val filmStripOverlayParentFingerprint = legacyFingerprint(
- name = "filmStripOverlayParentFingerprint",
+internal val filmStripOverlayEnterParentFingerprint = legacyFingerprint(
+ name = "filmStripOverlayEnterParentFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
- literals = listOf(scrubbing),
+ literals = listOf(verticalTouchOffsetToEnterFineScrubbing),
+)
+
+/**
+ * YouTube 20.12 ~
+ */
+internal val filmStripOverlayMotionEventPrimaryFingerprint = legacyFingerprint(
+ name = "filmStripOverlayMotionEventPrimaryFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("Landroid/view/MotionEvent;"),
+ opcodes = listOf(
+ Opcode.IGET_OBJECT,
+ Opcode.INVOKE_INTERFACE,
+ ),
+)
+
+/**
+ * YouTube 20.12 ~
+ */
+internal val filmStripOverlayMotionEventSecondaryFingerprint = legacyFingerprint(
+ name = "filmStripOverlayMotionEventSecondaryFingerprint",
+ returnType = "Z",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("Landroid/view/MotionEvent;"),
+ opcodes = listOf(
+ Opcode.INVOKE_VIRTUAL,
+ Opcode.MOVE_RESULT,
+ Opcode.IF_EQZ,
+ Opcode.NEG_FLOAT,
+ ),
+)
+
+internal val filmStripOverlayStartParentFingerprint = legacyFingerprint(
+ name = "filmStripOverlayStartParentFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
+ literals = listOf(verticalTouchOffsetToStartFineScrubbing),
)
internal val filmStripOverlayPreviewFingerprint = legacyFingerprint(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt
index 8cc9ce104..2886c7063 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt
@@ -24,12 +24,15 @@ import app.revanced.patches.youtube.utils.engagement.engagementPanelIdRegister
import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.SPANS_PATH
+import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch
import app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen.suggestedVideoEndScreenPatch
import app.revanced.patches.youtube.utils.patch.PatchList.PLAYER_COMPONENTS
import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
+import app.revanced.patches.youtube.utils.playservice.is_19_18_or_greater
import app.revanced.patches.youtube.utils.playservice.is_20_02_or_greater
import app.revanced.patches.youtube.utils.playservice.is_20_03_or_greater
import app.revanced.patches.youtube.utils.playservice.is_20_05_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_20_12_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.resourceid.darkBackground
import app.revanced.patches.youtube.utils.resourceid.fadeDurationFast
@@ -45,23 +48,27 @@ import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT
import app.revanced.util.Utils.printWarn
import app.revanced.util.findMethodOrThrow
+import app.revanced.util.findMutableMethodOf
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.injectLiteralInstructionViewCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.mutableClassOrThrow
-import app.revanced.util.fingerprint.resolvable
import app.revanced.util.getReference
import app.revanced.util.getWalkerMethod
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
+import app.revanced.util.or
+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.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
@@ -70,7 +77,11 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private val speedOverlayPatch = bytecodePatch(
description = "speedOverlayPatch"
) {
- dependsOn(sharedResourceIdPatch)
+ dependsOn(
+ sharedExtensionPatch,
+ sharedResourceIdPatch,
+ versionCheckPatch,
+ )
execute {
fun MutableMethod.hookSpeedOverlay(
@@ -87,11 +98,19 @@ private val speedOverlayPatch = bytecodePatch(
)
}
- val resolvable = restoreSlideToSeekBehaviorFingerprint.resolvable() &&
- speedOverlayFingerprint.resolvable() &&
- speedOverlayFloatValueFingerprint.resolvable()
+ fun MutableMethod.hookRelativeSpeedValue(startIndex: Int) {
+ val relativeIndex = indexOfFirstInstructionOrThrow(startIndex, Opcode.CMPL_FLOAT)
+ val relativeRegister = getInstruction(relativeIndex).registerB
- if (resolvable) {
+ addInstructions(
+ relativeIndex, """
+ invoke-static {v$relativeRegister}, $PLAYER_CLASS_DESCRIPTOR->speedOverlayRelativeValue(F)F
+ move-result v$relativeRegister
+ """
+ )
+ }
+
+ if (!is_19_18_or_greater) {
// Used on YouTube 18.29.38 ~ YouTube 19.17.41
// region patch for Disable speed overlay (Enable slide to seek)
@@ -110,17 +129,53 @@ private val speedOverlayPatch = bytecodePatch(
// region patch for Custom speed overlay float value
- speedOverlayFloatValueFingerprint.matchOrThrow().let {
- it.method.apply {
- val index = it.patternMatch!!.startIndex
- val register = getInstruction(index).registerA
+ val speedFieldReference = with(speedOverlayFloatValueFingerprint.methodOrThrow()) {
+ val literalIndex =
+ indexOfFirstLiteralInstructionOrThrow(SPEED_OVERLAY_LEGACY_FEATURE_FLAG)
+ val floatIndex =
+ indexOfFirstInstructionOrThrow(literalIndex, Opcode.DOUBLE_TO_FLOAT)
+ val floatRegister = getInstruction(floatIndex).registerA
- addInstructions(
- index + 1, """
- invoke-static {v$register}, $PLAYER_CLASS_DESCRIPTOR->speedOverlayValue(F)F
- move-result v$register
+ addInstructions(
+ floatIndex + 1, """
+ invoke-static {v$floatRegister}, $PLAYER_CLASS_DESCRIPTOR->speedOverlayValue(F)F
+ move-result v$floatRegister
"""
- )
+ )
+
+ val speedFieldIndex = indexOfFirstInstructionOrThrow(literalIndex) {
+ opcode == Opcode.IPUT &&
+ getReference()?.type == "F"
+ }
+
+ getInstruction(speedFieldIndex).reference.toString()
+ }
+
+ fun indexOfFirstSpeedFieldInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ opcode == Opcode.IGET &&
+ getReference()?.toString() == speedFieldReference
+ }
+
+ val isSyntheticMethod: Method.() -> Boolean = {
+ name == "run" &&
+ accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
+ parameterTypes.isEmpty() &&
+ indexOfFirstSpeedFieldInstruction(this) >= 0 &&
+ indexOfFirstInstruction(Opcode.CMPL_FLOAT) >= 0
+ }
+
+ classes.forEach { classDef ->
+ classDef.methods.forEach { method ->
+ if (method.isSyntheticMethod()) {
+ proxy(classDef)
+ .mutableClass
+ .findMutableMethodOf(method)
+ .apply {
+ val speedFieldIndex = indexOfFirstSpeedFieldInstruction(this)
+ hookRelativeSpeedValue(speedFieldIndex)
+ }
+ }
}
}
@@ -241,6 +296,8 @@ private val speedOverlayPatch = bytecodePatch(
move-result v$speedOverlayFloatValueRegister
"""
)
+
+ hookRelativeSpeedValue(speedOverlayFloatValueIndex)
}
// Removed in YouTube 20.03+
@@ -345,18 +402,6 @@ val playerComponentsPatch = bytecodePatch(
return ""
}
- fun MutableMethod.hookFilmstripOverlay() {
- addInstructionsWithLabels(
- 0, """
- invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideFilmstripOverlay()Z
- move-result v0
- if-eqz v0, :shown
- const/4 v0, 0x0
- return v0
- """, ExternalLabel("shown", getInstruction(0))
- )
- }
-
// region patch for custom player overlay opacity
youtubeControlsOverlayFingerprint.methodOrThrow().apply {
@@ -527,16 +572,76 @@ val playerComponentsPatch = bytecodePatch(
// region patch for hide filmstrip overlay
- arrayOf(
- filmStripOverlayConfigFingerprint,
- filmStripOverlayInteractionFingerprint,
- filmStripOverlayPreviewFingerprint
- ).forEach { fingerprint ->
- fingerprint.methodOrThrow(filmStripOverlayParentFingerprint).hookFilmstripOverlay()
+ fun MutableMethod.hookFilmstripOverlay(
+ index: Int = 0,
+ register: Int = 0
+ ) {
+ val stringInstructions = if (returnType == "Z")
+ """
+ const/4 v$register, 0x0
+ return v$register
+ """
+ else if (returnType == "V")
+ """
+ return-void
+ """
+ else
+ throw Exception("This case should never happen.")
+
+ addInstructionsWithLabels(
+ index, """
+ invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideFilmstripOverlay()Z
+ move-result v$register
+ if-eqz v$register, :shown
+ """ + stringInstructions + """
+ :shown
+ nop
+ """
+ )
}
- // Removed in YouTube 20.05+
- if (!is_20_05_or_greater) {
+ val filmStripOverlayFingerprints = mutableListOf(
+ filmStripOverlayInteractionFingerprint,
+ filmStripOverlayPreviewFingerprint
+ )
+
+ if (is_20_12_or_greater) {
+ filmStripOverlayMotionEventPrimaryFingerprint.matchOrThrow(
+ filmStripOverlayStartParentFingerprint
+ ).let {
+ it.method.apply {
+ val index = it.patternMatch!!.startIndex
+ val register = getInstruction(index).registerA
+
+ hookFilmstripOverlay(index, register)
+ }
+ }
+
+ filmStripOverlayMotionEventSecondaryFingerprint.matchOrThrow(
+ filmStripOverlayStartParentFingerprint
+ ).let {
+ it.method.apply {
+ val index = it.patternMatch!!.startIndex + 2
+ val register = getInstruction(index).registerA
+
+ addInstructions(
+ index, """
+ invoke-static {v$register}, $PLAYER_CLASS_DESCRIPTOR->hideFilmstripOverlay(Z)Z
+ move-result v$register
+ """
+ )
+ }
+ }
+ } else {
+ filmStripOverlayFingerprints += filmStripOverlayConfigFingerprint
+ }
+
+ filmStripOverlayFingerprints.forEach { fingerprint ->
+ fingerprint.methodOrThrow(filmStripOverlayEnterParentFingerprint).hookFilmstripOverlay()
+ }
+
+ // Removed in YouTube 20.03+
+ if (!is_20_03_or_greater) {
youtubeControlsOverlayFingerprint.methodOrThrow().apply {
val constIndex = indexOfFirstLiteralInstructionOrThrow(fadeDurationFast)
val constRegister = getInstruction(constIndex).registerA
@@ -564,7 +669,7 @@ val playerComponentsPatch = bytecodePatch(
)
removeInstruction(insertIndex)
}
- } else {
+ } else if (is_20_05_or_greater) {
// This is a new film strip overlay added to YouTube 20.05+
// Disabling this flag is not related to the operation of the patch.
filmStripOverlayConfigV2Fingerprint.injectLiteralInstructionBooleanCall(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch.kt
index 12be08c50..ac753a6fe 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch.kt
@@ -15,7 +15,7 @@ import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCR
import app.revanced.patches.youtube.utils.patch.PatchList.DESCRIPTION_COMPONENTS
import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.utils.playservice.is_18_49_or_greater
-import app.revanced.patches.youtube.utils.playservice.is_19_02_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_19_05_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverHook
import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverPatch
@@ -93,8 +93,7 @@ val descriptionComponentsPatch = bytecodePatch(
// region patch for disable video description interaction and expand video description
- // since these patches are still A/B tested, they are classified as 'Experimental flags'.
- if (is_19_02_or_greater) {
+ if (is_19_05_or_greater) {
textViewComponentFingerprint.methodOrThrow().apply {
val insertIndex = indexOfTextIsSelectableInstruction(this)
val insertInstruction = getInstruction(insertIndex)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/Fingerprints.kt
index 0fccda3a4..dd98d2908 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/Fingerprints.kt
@@ -9,7 +9,7 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference
/**
* This fingerprint is compatible with YouTube v18.35.xx~
- * Nonetheless, the patch works in YouTube v19.02.xx~
+ * Nonetheless, the patch works in YouTube v19.05.xx~
*/
internal val textViewComponentFingerprint = legacyFingerprint(
name = "textViewComponentFingerprint",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt
index 19470906e..70d3db075 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt
@@ -1,8 +1,10 @@
package app.revanced.patches.youtube.player.flyoutmenu.hide
+import app.revanced.patches.youtube.utils.indexOfAddHeaderViewInstruction
import app.revanced.patches.youtube.utils.resourceid.bottomSheetFooterText
import app.revanced.patches.youtube.utils.resourceid.subtitleMenuSettingsFooterInfo
import app.revanced.patches.youtube.utils.resourceid.videoQualityBottomSheet
+import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
@@ -18,33 +20,18 @@ internal val advancedQualityBottomSheetFingerprint = legacyFingerprint(
returnType = "L",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("L", "L", "L"),
- opcodes = listOf(
- Opcode.IGET_OBJECT,
- Opcode.INVOKE_STATIC,
- Opcode.CONST,
- Opcode.CONST_4,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT_OBJECT,
- Opcode.CONST,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT_OBJECT,
- Opcode.CONST_16,
- Opcode.INVOKE_VIRTUAL,
- Opcode.CONST,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT_OBJECT,
- Opcode.CHECK_CAST,
- Opcode.CONST,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT_OBJECT,
- Opcode.IGET_OBJECT,
- Opcode.IGET_OBJECT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT_OBJECT,
- Opcode.IGET_OBJECT,
- Opcode.CONST_STRING
- ),
- literals = listOf(videoQualityBottomSheet),
+ customFingerprint = custom@{ method, _ ->
+ if (!method.containsLiteralInstruction(videoQualityBottomSheet)) {
+ return@custom false
+ }
+ if (indexOfAddHeaderViewInstruction(method) < 0) {
+ return@custom false
+ }
+ val implementation = method.implementation
+ ?: return@custom false
+
+ implementation.instructions.elementAt(0).opcode == Opcode.IGET_OBJECT
+ }
)
internal val captionsBottomSheetFingerprint = legacyFingerprint(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt
index 07c2df304..8f7ea6837 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt
@@ -10,6 +10,7 @@ import app.revanced.patches.shared.litho.lithoFilterPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR
+import app.revanced.patches.youtube.utils.indexOfAddHeaderViewInstruction
import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_PLAYER_FLYOUT_MENU
import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.utils.playservice.is_18_39_or_greater
@@ -25,7 +26,6 @@ import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.injectLiteralInstructionViewCall
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
-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.FiveRegisterInstruction
@@ -75,10 +75,7 @@ val playerFlyoutMenuPatch = bytecodePatch(
qualityMenuViewInflateFingerprint
).forEach { fingerprint ->
fingerprint.methodOrThrow().apply {
- val insertIndex = indexOfFirstInstructionOrThrow {
- opcode == Opcode.INVOKE_VIRTUAL &&
- getReference()?.name == "addHeaderView"
- }
+ val insertIndex = indexOfAddHeaderViewInstruction(this)
val insertRegister = getInstruction(insertIndex).registerD
addInstructions(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt
index bda5fdde7..cec249b45 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt
@@ -7,7 +7,6 @@ import app.revanced.patches.youtube.utils.resourceid.quickActionsElementContaine
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
-import com.android.tools.smali.dexlib2.util.MethodUtil
internal val broadcastReceiverFingerprint = legacyFingerprint(
name = "broadcastReceiverFingerprint",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt
index 2a56b37c6..11d7c5e3c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt
@@ -37,7 +37,6 @@ import app.revanced.patches.youtube.utils.youtubeControlsOverlayFingerprint
import app.revanced.patches.youtube.video.information.hookBackgroundPlayVideoInformation
import app.revanced.patches.youtube.video.information.videoEndMethod
import app.revanced.patches.youtube.video.information.videoInformationPatch
-import app.revanced.util.Utils.printWarn
import app.revanced.util.addInstructionsAtControlFlowLabel
import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.methodOrThrow
@@ -313,8 +312,6 @@ val fullscreenComponentsPatch = bytecodePatch(
}
settingArray += "SETTINGS: KEEP_LANDSCAPE_MODE"
- } else {
- printWarn("\"Keep landscape mode\" is not supported in this version. Use YouTube 19.16.39 or earlier.")
}
// endregion
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/Fingerprints.kt
index df9835b17..219f4cd15 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/Fingerprints.kt
@@ -63,6 +63,7 @@ internal val miniplayerResponseModelSizeCheckFingerprint = legacyFingerprint(
// region modern miniplayer
internal const val MINIPLAYER_MODERN_FEATURE_KEY = 45622882L
+
// In later targets this feature flag does nothing and is dead code.
internal const val MINIPLAYER_MODERN_FEATURE_LEGACY_KEY = 45630429L
internal const val MINIPLAYER_DOUBLE_TAP_FEATURE_KEY = 45628823L
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/MiniplayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/MiniplayerPatch.kt
index bdd835f99..18ab867db 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/MiniplayerPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/miniplayer/general/MiniplayerPatch.kt
@@ -50,6 +50,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstructio
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
@@ -145,10 +146,15 @@ val miniplayerPatch = bytecodePatch(
// region Legacy tablet Miniplayer hooks.
miniplayerOverrideFingerprint.matchOrThrow().let {
- val appNameStringIndex = it.stringMatches!!.first().index + 2
-
it.method.apply {
- val walkerMethod = getWalkerMethod(appNameStringIndex)
+ val stringIndex = it.stringMatches!!.first().index
+ val walkerIndex = indexOfFirstInstructionOrThrow(stringIndex) {
+ val reference = getReference()
+ reference?.returnType == "Z" &&
+ reference.parameterTypes.size == 1 &&
+ reference.parameterTypes.firstOrNull() == "Landroid/content/Context;"
+ }
+ val walkerMethod = getWalkerMethod(walkerIndex)
walkerMethod.apply {
findReturnIndicesReversed().forEach { index ->
@@ -233,7 +239,8 @@ val miniplayerPatch = bytecodePatch(
val register = getInstruction(targetIndex).registerA
addInstructions(
- targetIndex + 1, """
+ targetIndex + 1,
+ """
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getMiniplayerDefaultSize(I)I
move-result v$register
""",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt
index 67ce333ab..0dc46c49b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt
@@ -15,6 +15,7 @@ import app.revanced.patches.youtube.utils.patch.PatchList.OVERLAY_BUTTONS
import app.revanced.patches.youtube.utils.pip.pipStateHookPatch
import app.revanced.patches.youtube.utils.playercontrols.hookBottomControlButton
import app.revanced.patches.youtube.utils.playercontrols.playerControlsPatch
+import app.revanced.patches.youtube.utils.playlist.playlistPatch
import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.settingsPatch
@@ -74,6 +75,7 @@ val overlayButtonsPatch = resourcePatch(
cfBottomUIPatch,
pipStateHookPatch,
playerControlsPatch,
+ playlistPatch,
sharedResourceIdPatch,
settingsPatch,
)
@@ -254,7 +256,8 @@ val overlayButtonsPatch = resourcePatch(
width != "0.0dip",
)
- val isButton = id.endsWith("_button") && id != "@id/multiview_button" || id == "@id/youtube_controls_fullscreen_button_stub"
+ val isButton =
+ id.endsWith("_button") && id != "@id/multiview_button" || id == "@id/youtube_controls_fullscreen_button_stub"
// Adjust TimeBar and Chapter bottom padding
val timBarItem = mutableMapOf(
@@ -284,7 +287,10 @@ val overlayButtonsPatch = resourcePatch(
if (id.equals("@+id/bottom_margin")) {
node.setAttribute("android:layout_height", marginBottom)
} else if (id.equals("@id/time_bar_reference_view")) {
- node.setAttribute("yt:layout_constraintBottom_toTopOf", "@id/quick_actions_container")
+ node.setAttribute(
+ "yt:layout_constraintBottom_toTopOf",
+ "@id/quick_actions_container"
+ )
}
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt
index 22584d1ab..aba912974 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt
@@ -7,10 +7,13 @@ import app.revanced.patches.youtube.utils.resourceid.ytTextSecondary
import app.revanced.patches.youtube.utils.resourceid.ytYoutubeMagenta
import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstructionReversed
import app.revanced.util.or
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
-import kotlin.collections.listOf
+import com.android.tools.smali.dexlib2.iface.Method
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal val shortsSeekbarColorFingerprint = legacyFingerprint(
name = "shortsSeekbarColorFingerprint",
@@ -113,27 +116,21 @@ internal val seekbarTappingFingerprint = legacyFingerprint(
name = "seekbarTappingFingerprint",
returnType = "Z",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
- parameters = listOf("L"),
- opcodes = listOf(
- Opcode.IPUT_OBJECT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.RETURN,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT,
- Opcode.IF_EQZ,
- Opcode.INVOKE_VIRTUAL,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT,
- Opcode.IF_EQZ,
- Opcode.INT_TO_FLOAT,
- Opcode.INT_TO_FLOAT,
- Opcode.INVOKE_VIRTUAL,
- Opcode.MOVE_RESULT,
- Opcode.IF_EQZ
- ),
- customFingerprint = { method, _ -> method.name == "onTouchEvent" }
+ parameters = listOf("Landroid/view/MotionEvent;"),
+ customFingerprint = { method, classDef ->
+ classDef.interfaces.contains("Landroid/view/View${'$'}OnLayoutChangeListener;") &&
+ classDef.fields.find { it.type == "[Lcom/google/android/libraries/youtube/player/features/overlay/timebar/TimelineMarker;" } != null &&
+ method.name == "onTouchEvent" &&
+ indexOfPointInstruction(method) >= 0
+ }
)
+internal fun indexOfPointInstruction(method: Method) =
+ method.indexOfFirstInstructionReversed {
+ opcode == Opcode.INVOKE_DIRECT &&
+ getReference()?.toString() == "Landroid/graphics/Point;->(II)V"
+ }
+
internal val seekbarThumbnailsQualityFingerprint = legacyFingerprint(
name = "seekbarThumbnailsQualityFingerprint",
returnType = "Z",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
index 441ec5f4c..9654b5956 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
@@ -11,14 +11,12 @@ import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.shared.drawable.addDrawableColorHook
import app.revanced.patches.shared.drawable.drawableColorHookPatch
import app.revanced.patches.shared.mainactivity.onCreateMethod
-import app.revanced.patches.youtube.layout.branding.icon.customBrandingIconPatch
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR
import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_PATH
import app.revanced.patches.youtube.utils.flyoutmenu.flyoutMenuHookPatch
import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch
-import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE
import app.revanced.patches.youtube.utils.patch.PatchList.SEEKBAR_COMPONENTS
import app.revanced.patches.youtube.utils.playerButtonsResourcesFingerprint
import app.revanced.patches.youtube.utils.playerButtonsVisibilityFingerprint
@@ -37,6 +35,7 @@ import app.revanced.patches.youtube.utils.seekbarFingerprint
import app.revanced.patches.youtube.utils.seekbarOnDrawFingerprint
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext
+import app.revanced.patches.youtube.utils.settings.ResourceUtils.restoreOldSplashAnimationIncluded
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.patches.youtube.utils.totalTimeFingerprint
import app.revanced.patches.youtube.video.information.videoInformationPatch
@@ -48,7 +47,6 @@ import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.fingerprint.resolvable
-import app.revanced.util.getBooleanOptionValue
import app.revanced.util.getReference
import app.revanced.util.getWalkerMethod
import app.revanced.util.indexOfFirstInstructionOrThrow
@@ -122,9 +120,6 @@ val seekbarComponentsPatch = bytecodePatch(
execute {
- val restoreOldSplashAnimationIncluded = CUSTOM_BRANDING_ICON_FOR_YOUTUBE.included == true &&
- customBrandingIconPatch.getBooleanOptionValue("restoreOldSplashAnimation").value == true
-
var settingArray = arrayOf(
"PREFERENCE_SCREEN: PLAYER",
"SETTINGS: SEEKBAR_COMPONENTS"
@@ -132,60 +127,71 @@ val seekbarComponentsPatch = bytecodePatch(
// region patch for enable seekbar tapping patch
- seekbarTappingFingerprint.matchOrThrow().let {
- it.method.apply {
- val tapSeekIndex = it.patternMatch!!.startIndex + 1
- val tapSeekClass = getInstruction(tapSeekIndex)
- .getReference()!!
- .definingClass
+ seekbarTappingFingerprint.methodOrThrow().apply {
+ val pointIndex = indexOfPointInstruction(this)
+ val pointInstruction = getInstruction(pointIndex)
+ val freeRegister = pointInstruction.registerE
+ val xAxisRegister = pointInstruction.registerD
- val tapSeekMethods = findMethodsOrThrow(tapSeekClass)
- var pMethodCall = ""
- var oMethodCall = ""
-
- for (method in tapSeekMethods) {
- if (method.implementation == null)
- continue
-
- val instructions = method.implementation!!.instructions
- // here we make sure we actually find the method because it has more than 7 instructions
- if (instructions.count() != 10)
- continue
-
- // we know that the 7th instruction has the opcode CONST_4
- val instruction = instructions.elementAt(6)
- if (instruction.opcode != Opcode.CONST_4)
- continue
-
- // the literal for this instruction has to be either 1 or 2
- val literal = (instruction as NarrowLiteralInstruction).narrowLiteral
-
- // method founds
- if (literal == 1)
- pMethodCall = "${method.definingClass}->${method.name}(I)V"
- else if (literal == 2)
- oMethodCall = "${method.definingClass}->${method.name}(I)V"
- }
-
- if (pMethodCall.isEmpty()) {
- throw PatchException("pMethod not found")
- }
- if (oMethodCall.isEmpty()) {
- throw PatchException("oMethod not found")
- }
-
- val insertIndex = it.patternMatch!!.startIndex + 2
-
- addInstructionsWithLabels(
- insertIndex, """
- invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSeekbarTapping()Z
- move-result v0
- if-eqz v0, :disabled
- invoke-virtual { p0, v2 }, $pMethodCall
- invoke-virtual { p0, v2 }, $oMethodCall
- """, ExternalLabel("disabled", getInstruction(insertIndex))
- )
+ val tapSeekIndex = indexOfFirstInstructionOrThrow(pointIndex) {
+ val reference = getReference()
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ reference?.returnType == "V" &&
+ reference.parameterTypes.isEmpty()
}
+ val thisInstanceRegister =
+ getInstruction(tapSeekIndex).registerC
+
+ val tapSeekClass = getInstruction(tapSeekIndex)
+ .getReference()!!
+ .definingClass
+
+ val tapSeekMethods = findMethodsOrThrow(tapSeekClass)
+ var pMethodCall = ""
+ var oMethodCall = ""
+
+ for (method in tapSeekMethods) {
+ if (method.implementation == null)
+ continue
+
+ val instructions = method.implementation!!.instructions
+ // here we make sure we actually find the method because it has more than 7 instructions
+ if (instructions.count() != 10)
+ continue
+
+ // we know that the 7th instruction has the opcode CONST_4
+ val instruction = instructions.elementAt(6)
+ if (instruction.opcode != Opcode.CONST_4)
+ continue
+
+ // the literal for this instruction has to be either 1 or 2
+ val literal = (instruction as NarrowLiteralInstruction).narrowLiteral
+
+ // method founds
+ if (literal == 1)
+ pMethodCall = "${method.definingClass}->${method.name}(I)V"
+ else if (literal == 2)
+ oMethodCall = "${method.definingClass}->${method.name}(I)V"
+ }
+
+ if (pMethodCall.isEmpty()) {
+ throw PatchException("pMethod not found")
+ }
+ if (oMethodCall.isEmpty()) {
+ throw PatchException("oMethod not found")
+ }
+
+ val insertIndex = tapSeekIndex + 1
+
+ addInstructionsWithLabels(
+ insertIndex, """
+ invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSeekbarTapping()Z
+ move-result v$freeRegister
+ if-eqz v$freeRegister, :disabled
+ invoke-virtual { v$thisInstanceRegister, v$xAxisRegister }, $pMethodCall
+ invoke-virtual { v$thisInstanceRegister, v$xAxisRegister }, $oMethodCall
+ """, ExternalLabel("disabled", getInstruction(insertIndex))
+ )
}
// endregion
@@ -269,7 +275,10 @@ val seekbarComponentsPatch = bytecodePatch(
playerSeekbarHandleColorPrimaryFingerprint,
playerSeekbarHandleColorSecondaryFingerprint
).forEach {
- it.methodOrThrow().addColorChangeInstructions(ytStaticBrandRed, "getVideoPlayerSeekbarColorAccent")
+ it.methodOrThrow().addColorChangeInstructions(
+ ytStaticBrandRed,
+ "getVideoPlayerSeekbarColorAccent"
+ )
}
// If hiding feed seekbar thumbnails, then turn off the cairo gradient
// of the watch history menu items as they use the same gradient as the
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt
index 00f4cc521..92d882cd9 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt
@@ -20,8 +20,36 @@ import app.revanced.util.or
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.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
-import kotlin.collections.listOf
+
+internal val bottomSheetMenuDismissFingerprint = legacyFingerprint(
+ name = "bottomSheetMenuDismissFingerprint",
+ returnType = "V",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = emptyList(),
+ customFingerprint = { method, _ ->
+ indexOfDismissInstruction(method) >= 0
+ }
+)
+
+fun indexOfDismissInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ val reference = getReference()
+ reference?.name == "dismiss" &&
+ reference.returnType == "V" &&
+ reference.parameterTypes.isEmpty()
+ }
+
+internal val bottomSheetMenuItemClickFingerprint = legacyFingerprint(
+ name = "bottomSheetMenuItemClickFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ returnType = "V",
+ parameters = listOf("Landroid/widget/AdapterView;", "Landroid/view/View;", "I", "J"),
+ customFingerprint = { method, _ ->
+ method.name == "onItemClick"
+ }
+)
internal val bottomSheetMenuListBuilderFingerprint = legacyFingerprint(
name = "bottomSheetMenuListBuilderFingerprint",
@@ -71,6 +99,41 @@ internal val reelEnumStaticFingerprint = legacyFingerprint(
returnType = "L"
)
+/**
+ * YouTube 18.49.36 ~
+ */
+internal val reelPlaybackRepeatFingerprint = legacyFingerprint(
+ name = "reelPlaybackRepeatFingerprint",
+ returnType = "V",
+ parameters = listOf("L"),
+ strings = listOf("YoutubePlayerState is in throwing an Error.")
+)
+
+internal val reelPlaybackFingerprint = legacyFingerprint(
+ name = "reelPlaybackFingerprint",
+ accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+ parameters = listOf("J"),
+ returnType = "V",
+ customFingerprint = { method, _ ->
+ indexOfMilliSecondsInstruction(method) >= 0 &&
+ indexOfInitializationInstruction(method) >= 0
+ }
+)
+
+private fun indexOfMilliSecondsInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ getReference()?.name == "MILLISECONDS"
+ }
+
+internal fun indexOfInitializationInstruction(method: Method) =
+ method.indexOfFirstInstruction {
+ val reference = getReference()
+ opcode == Opcode.INVOKE_DIRECT &&
+ reference?.name == "" &&
+ reference.parameterTypes.size == 3 &&
+ reference.parameterTypes.firstOrNull() == "I"
+ }
+
internal const val SHORTS_HUD_FEATURE_FLAG = 45644023L
/**
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt
index e327d3e75..e124d373a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt
@@ -33,10 +33,13 @@ import app.revanced.patches.youtube.utils.patch.PatchList.SHORTS_COMPONENTS
import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.utils.playservice.is_18_31_or_greater
import app.revanced.patches.youtube.utils.playservice.is_18_34_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_18_49_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_02_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_19_11_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_28_or_greater
import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_20_09_or_greater
import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverHook
import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverPatch
@@ -60,6 +63,7 @@ import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext
import app.revanced.patches.youtube.utils.settings.settingsPatch
import app.revanced.patches.youtube.utils.toolbar.hookToolBar
import app.revanced.patches.youtube.utils.toolbar.toolBarHookPatch
+import app.revanced.patches.youtube.utils.videoIdFingerprintShorts
import app.revanced.patches.youtube.video.information.hookShortsVideoInformation
import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.patches.youtube.video.playbackstart.PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
@@ -75,7 +79,6 @@ import app.revanced.util.cloneMutable
import app.revanced.util.copyResources
import app.revanced.util.findMethodOrThrow
import app.revanced.util.findMutableMethodOf
-import app.revanced.util.fingerprint.definingClassOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
@@ -94,6 +97,7 @@ import app.revanced.util.or
import app.revanced.util.replaceLiteralInstructionCall
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.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
@@ -335,7 +339,35 @@ private val shortsCustomActionsPatch = bytecodePatch(
}
}
- recyclerViewTreeObserverHook("$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V")
+ if (is_19_11_or_greater) {
+ // The type of the Shorts flyout menu is RecyclerView.
+ recyclerViewTreeObserverHook("$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V")
+ } else {
+ // The type of the Shorts flyout menu is ListView.
+ val dismissReference = with(
+ bottomSheetMenuDismissFingerprint.methodOrThrow(
+ bottomSheetMenuListBuilderFingerprint
+ )
+ ) {
+ val dismissIndex = indexOfDismissInstruction(this)
+ getInstruction(dismissIndex).reference
+ }
+
+ bottomSheetMenuItemClickFingerprint
+ .methodOrThrow(bottomSheetMenuListBuilderFingerprint)
+ .addInstructionsWithLabels(
+ 0,
+ """
+ invoke-static/range {p2 .. p2}, $EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->onBottomSheetMenuItemClick(Landroid/view/View;)Z
+ move-result v0
+ if-eqz v0, :ignore
+ invoke-virtual {p0}, $dismissReference
+ return-void
+ :ignore
+ nop
+ """,
+ )
+ }
// endregion
@@ -403,9 +435,7 @@ private val shortsRepeatPatch = bytecodePatch(
"setMainActivity"
)
- val reelEnumClass = reelEnumConstructorFingerprint.definingClassOrThrow()
-
- reelEnumConstructorFingerprint.methodOrThrow().apply {
+ val endScreenReference = with(reelEnumConstructorFingerprint.methodOrThrow()) {
val insertIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN_VOID)
addInstructions(
@@ -413,7 +443,7 @@ private val shortsRepeatPatch = bytecodePatch(
"""
# Pass the first enum value to extension.
# Any enum value of this type will work.
- sget-object v0, $reelEnumClass->a:$reelEnumClass
+ sget-object v0, $definingClass->a:$definingClass
invoke-static { v0 }, $EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR->setYTShortsRepeatEnum(Ljava/lang/Enum;)V
""",
)
@@ -422,50 +452,125 @@ private val shortsRepeatPatch = bytecodePatch(
indexOfFirstStringInstructionOrThrow("REEL_LOOP_BEHAVIOR_END_SCREEN")
val endScreenReferenceIndex =
indexOfFirstInstructionOrThrow(endScreenStringIndex, Opcode.SPUT_OBJECT)
- val endScreenReference =
- getInstruction(endScreenReferenceIndex).reference.toString()
- val enumMethod = reelEnumStaticFingerprint.methodOrThrow(reelEnumConstructorFingerprint)
+ getInstruction(endScreenReferenceIndex).reference.toString()
+ }
+
+ lateinit var insertMethod: MutableMethod
+ var insertMethodFound = false
+
+ if (is_18_49_or_greater) {
+ insertMethod = reelPlaybackRepeatFingerprint.methodOrThrow()
+ } else {
+ val isInsertMethod: Method.() -> Boolean = {
+ parameters.size == 1 &&
+ parameterTypes.first().startsWith("L") &&
+ returnType == "V" &&
+ indexOfFirstInstruction {
+ getReference()?.toString() == endScreenReference
+ } >= 0
+ }
classes.forEach { classDef ->
- classDef.methods.filter { method ->
- method.parameters.size == 1 &&
- method.parameters[0].startsWith("L") &&
- method.returnType == "V" &&
- method.indexOfFirstInstruction {
- getReference()?.toString() == endScreenReference
- } >= 0
- }.forEach { targetMethod ->
- proxy(classDef)
- .mutableClass
- .findMutableMethodOf(targetMethod)
- .apply {
- implementation!!.instructions
- .withIndex()
- .filter { (_, instruction) ->
- val reference =
- (instruction as? ReferenceInstruction)?.reference
- reference is MethodReference &&
- MethodUtil.methodSignaturesMatch(enumMethod, reference)
- }
- .map { (index, _) -> index }
- .reversed()
- .forEach { index ->
- val register =
- getInstruction(index + 1).registerA
-
- addInstructions(
- index + 2, """
- invoke-static {v$register}, $EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum;
- move-result-object v$register
- """
- )
- }
+ if (!insertMethodFound) {
+ classDef.methods.forEach { method ->
+ if (method.isInsertMethod()) {
+ insertMethodFound = true
+ insertMethod = proxy(classDef)
+ .mutableClass
+ .findMutableMethodOf(method)
}
+ }
}
}
}
+ val enumMethod = reelEnumStaticFingerprint.methodOrThrow(reelEnumConstructorFingerprint)
+
+ insertMethod.apply {
+ implementation!!.instructions
+ .withIndex()
+ .filter { (_, instruction) ->
+ val reference =
+ (instruction as? ReferenceInstruction)?.reference
+ reference is MethodReference &&
+ MethodUtil.methodSignaturesMatch(enumMethod, reference)
+ }
+ .map { (index, _) -> index }
+ .reversed()
+ .forEach { index ->
+ val register =
+ getInstruction(index + 1).registerA
+
+ addInstructions(
+ index + 2, """
+ invoke-static {v$register}, $EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum;
+ move-result-object v$register
+ """
+ )
+ }
+ }
+
+ // As of YouTube 20.09, Google has removed the code for 'Autoplay' and 'Pause' from this method.
+ // Manually add the 'Autoplay' code that Google removed.
+ // Tested on YouTube 20.10.
+ if (is_20_09_or_greater) {
+ val (directReference, virtualReference) = with(
+ reelPlaybackFingerprint.methodOrThrow(
+ videoIdFingerprintShorts
+ )
+ ) {
+ val directIndex = indexOfInitializationInstruction(this)
+ val virtualIndex = indexOfFirstInstructionOrThrow(directIndex) {
+ opcode == Opcode.INVOKE_VIRTUAL &&
+ getReference()?.parameterTypes?.size == 1
+ }
+
+ Pair(
+ getInstruction(directIndex).reference as MethodReference,
+ getInstruction(virtualIndex).reference as MethodReference
+ )
+ }
+
+ insertMethod.apply {
+ val extensionIndex = indexOfFirstInstructionOrThrow {
+ opcode == Opcode.INVOKE_STATIC &&
+ getReference()?.definingClass == EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR
+ }
+ val enumRegister =
+ getInstruction(extensionIndex + 1).registerA
+ val freeIndex = indexOfFirstInstructionOrThrow(extensionIndex) {
+ opcode == Opcode.SGET_OBJECT &&
+ getReference()?.name != "a"
+ }
+ val freeRegister = getInstruction(freeIndex).registerA
+ val getIndex = indexOfFirstInstructionOrThrow(extensionIndex) {
+ val reference = getReference()
+ opcode == Opcode.IGET_OBJECT &&
+ reference?.definingClass == definingClass &&
+ reference.type == virtualReference.definingClass
+ }
+ val getReference = getInstruction(getIndex).reference
+
+ addInstructionsWithLabels(
+ extensionIndex + 2, """
+ invoke-static {v$enumRegister}, $EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR->isAutoPlay(Ljava/lang/Enum;)Z
+ move-result v$freeRegister
+ if-eqz v$freeRegister, :ignore
+ new-instance v0, ${directReference.definingClass}
+ const/4 v1, 0x3
+ const/4 v2, 0x0
+ invoke-direct {v0, v1, v2, v2}, $directReference
+ iget-object v3, p0, $getReference
+ invoke-virtual {v3, v0}, $virtualReference
+ return-void
+ :ignore
+ nop
+ """
+ )
+ }
+ }
+
if (is_19_34_or_greater) {
shortsHUDFeatureFingerprint.injectLiteralInstructionBooleanCall(
SHORTS_HUD_FEATURE_FLAG,
@@ -919,7 +1024,8 @@ val shortsComponentPatch = bytecodePatch(
getReference()?.returnType == PLAYBACK_START_DESCRIPTOR_CLASS_DESCRIPTOR
}
val freeRegister = getInstruction(index).registerC
- val playbackStartRegister = getInstruction