Merge branch 'dev' into revanced-extended

This commit is contained in:
inotia00 2025-03-30 19:33:08 +09:00
commit 26bf2f4b82
304 changed files with 12867 additions and 5574 deletions

257
README.md
View File

@ -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 |
</details>
### [📦 `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 |
</details>
### [📦 `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 |
</details>
@ -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": []

View File

@ -26,6 +26,7 @@ android {
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.preference)
implementation(libs.collections4)
implementation(libs.lang3)
compileOnly(project(":extensions:shared:stub"))

View File

@ -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
);
}
}

View File

@ -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 {
* <p>
* 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
* <a href="https://stackoverflow.com/a/4604145">Reference</a>
* <p>
* That's why {@link AlertDialog#show()} is absolutely necessary.
* Instead, use two tricks to hide Alertdialog.

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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();
}

View File

@ -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);

View File

@ -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<ClientType> {
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<ClientType?> = 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<ClientType> = arrayOf(
val CLIENT_ORDER_TO_USE: Array<ClientType> = arrayOf(
ANDROID_VR_NO_AUTH,
ANDROID_UNPLUGGED,
ANDROID_CREATOR,
IOS_UNPLUGGED,
IOS,
ANDROID_VR,
)
val CLIENT_ORDER_TO_USE_IOS: Array<ClientType> = arrayOf(
ANDROID_VR_NO_AUTH,
ANDROID_UNPLUGGED,
ANDROID_CREATOR,
IOS_UNPLUGGED,
IOS_DEPRECATED,
ANDROID_VR,
)
}

View File

@ -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) {

View File

@ -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,

View File

@ -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<String, String>? = 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<String, String>? = 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<String, String>? = 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
}
}

View File

@ -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<String>(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()
}
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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 {

View File

@ -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.

View File

@ -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<String, String> requestHeaders) {
public static void fetchStreams(String url, Map<String, String> 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() {

View File

@ -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
}
}

View File

@ -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<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
requestHeader: Map<String, String>,
) {
private val videoId: String
private val future: Future<ByteBuffer?>
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.ClientType> =
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<String, StreamingDataRequest> = 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<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
fetchHeaders: Map<String, String>,
) {
// 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<String, String>,
visitorId: String,
botGuardPoToken: String
requestHeader: Map<String, String>,
): 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<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
requestHeader: Map<String, String>,
): 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
}
}

View File

@ -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<MusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true);
public static final EnumSetting<YouTubeMusicAppClient.ClientType> 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<YouTubeAppClient.ClientType> 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<YouTubeAppClient.ClientType> 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.

View File

@ -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)

View File

@ -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

View File

@ -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 {

View File

@ -60,6 +60,7 @@ public class Utils {
private static WeakReference<Activity> 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 {
* <br>
* 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.
*<br>
* <br>
* 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.
*<br>
* <br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/

View File

@ -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")

View File

@ -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
}

View File

@ -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 {
* <p>
* Appears to always be called from the main thread.
*/
public static boolean inAppVideoDownloadButtonOnClick(String videoId) {
public static boolean inAppVideoDownloadButtonOnClick(@Nullable Map<Object, Object> 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;
}
}

View File

@ -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;
}

View File

@ -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<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
try {

View File

@ -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)

View File

@ -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;
}
/**

View File

@ -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
);
}

View File

@ -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<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
try {

View File

@ -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;
}

View File

@ -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.

View File

@ -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<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders)
fetch(videoId, requestHeader)
}
val array: Array<ActionButton>
@ -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<String, String>) {
fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
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<String, String>): JSONObject? {
private fun sendRequest(videoId: String, requestHeader: Map<String, String>): 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<String, String>): Array<ActionButton> {
val json = sendRequest(videoId, playerHeaders)
private fun fetch(
videoId: String,
requestHeader: Map<String, String>
): Array<ActionButton> {
val json = sendRequest(videoId, requestHeader)
if (json != null) {
return parseResponse(json)
}

View File

@ -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<LinearLayout, Runnable> toolbarMap = new LinkedHashMap<>(arrSize);
Map<LinearLayout, Runnable> 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",

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<LinearLayout, Runnable> 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<String, String> 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<String, String>[] 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<LinearLayout, Runnable> actionsMap = new LinkedHashMap<>(playlists.length);
int libraryIconId = QueueManager.SAVE_QUEUE.drawableId;
for (Pair<String, String> 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,
};
}
}

View File

@ -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<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Pair<String, String>> = Utils.submitOnBackgroundThread {
fetch(
videoId,
requestHeader,
dataSyncId,
)
}
val playlistId: Pair<String, String>?
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<String, CreatePlaylistRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, CreatePlaylistRequest>(100) {
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, CreatePlaylistRequest>): 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<String, String>,
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<String, String>,
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<String, String>,
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<String, String>,
dataSyncId: String,
): Pair<String, String>? {
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
}
}
}

View File

@ -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<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Boolean> = 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<String, DeletePlaylistRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, DeletePlaylistRequest>(100) {
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, DeletePlaylistRequest>): 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<String, String>,
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<String, String>,
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<String, String>,
dataSyncId: String,
): Boolean? {
val json = sendRequest(
playlistId,
requestHeader,
dataSyncId,
)
if (json != null) {
return parseResponse(json)
}
return null
}
}
}

View File

@ -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<String, String>,
private val dataSyncId: String,
) {
private val future: Future<String> = 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<String, EditPlaylistRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, EditPlaylistRequest>(100) {
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, EditPlaylistRequest>): 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<String, String>,
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<String, String>,
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<String, String>,
dataSyncId: String,
): String? {
val json = sendRequest(
videoId,
playlistId,
setVideoId,
requestHeader,
dataSyncId,
)
if (json != null) {
return parseResponse(json, StringUtils.isNotEmpty(setVideoId))
}
return null
}
}
}

View File

@ -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<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Array<Pair<String, String>>> = Utils.submitOnBackgroundThread {
fetch(
playlistId,
requestHeader,
dataSyncId,
)
}
val playlists: Array<Pair<String, String>>?
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<String, GetPlaylistsRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, GetPlaylistsRequest>(100) {
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, GetPlaylistsRequest>): 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<String, String>,
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<String, String>,
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<Pair<String, String>>? {
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<Pair<String, String>?> =
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<String, String>,
dataSyncId: String,
): Array<Pair<String, String>>? {
val json = sendRequest(playlistId, requestHeader, dataSyncId)
if (json != null) {
return parseResponse(json)
}
return null
}
}
}

View File

@ -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<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Boolean> = 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<String, SavePlaylistRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, SavePlaylistRequest>(100) {
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, SavePlaylistRequest>): 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<String, String>,
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<String, String>,
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<String, String>,
dataSyncId: String,
): Boolean? {
val json = sendRequest(
playlistId,
libraryId,
requestHeader,
dataSyncId,
)
if (json != null) {
return parseResponse(json)
}
return null
}
}
}

View File

@ -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.
* <p>
* 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;
}
}

View File

@ -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);
}

View File

@ -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());

View File

@ -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"));
}
}

View File

@ -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)

View File

@ -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);

View File

@ -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<FormFactor> 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<FullscreenMode> 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<PlaylistIdPrefix> 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<Setting<?>> sbCategories = new HashSet<>(Arrays.asList(
SB_CATEGORY_SPONSOR,

View File

@ -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<Pair<CharSequence, CharSequence>> firstPairs = new ArrayList<>(firstEntriesToPreserve);
List<Pair<CharSequence, CharSequence>> pairsToSort = new ArrayList<>(entrySize);
for (int i = 0; i < entrySize; i++) {
Pair<CharSequence, CharSequence> 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<CharSequence, CharSequence> pair : firstPairs) {
sortedEntries[i] = pair.first;
sortedEntryValues[i] = pair.second;
i++;
}
for (Pair<CharSequence, CharSequence> 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.
*

View File

@ -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
*/

View File

@ -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));
}
}

View File

@ -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) {

View File

@ -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.

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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("<font color=\"%s\">%s</font>", 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("<b><font color=\"#%06X\">⬤</font> %s<br>",
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("</b>");
if (i + 1 != numberOfSegments) // prevents trailing new line after last segment
htmlBuilder.append("<br>");
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);
}

View File

@ -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("<font color=\"#%06X\">⬤</font>", 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) {

View File

@ -24,12 +24,15 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
@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;
}
}

View File

@ -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

View File

@ -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() {
/**

View File

@ -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)
}
}

View File

@ -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<LinearLayout, Runnable> 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;
}
}

View File

@ -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();

View File

@ -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<SearchView> searchViewRef = new WeakReference<>(null);
private static WeakReference<ImageView> 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);

View File

@ -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

View File

@ -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" }

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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(

View File

@ -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")
})
}
}
}
}

View File

@ -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<OneRegisterInstruction>(targetIndex).registerA
val textViewField = with(
channelHandleFingerprint
.methodOrThrow(namesInactiveAccountThumbnailSizeFingerprint)
) {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(channelHandle)
getInstruction(
indexOfFirstInstructionOrThrow(literalIndex) {
opcode == Opcode.IPUT_OBJECT &&
getReference<FieldReference>()?.type == "Landroid/widget/TextView;"
},
).getReference<FieldReference>()
}
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<OneRegisterInstruction>(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")
}
}

View File

@ -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)
)

View File

@ -115,7 +115,8 @@ val adsPatch = bytecodePatch(
.methodOrThrow(getPremiumDialogParentFingerprint)
.apply {
val setContentViewIndex = indexOfSetContentViewInstruction(this)
val dialogInstruction = getInstruction<FiveRegisterInstruction>(setContentViewIndex)
val dialogInstruction =
getInstruction<FiveRegisterInstruction>(setContentViewIndex)
val dialogRegister = dialogInstruction.registerC
val viewRegister = dialogInstruction.registerD

View File

@ -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
),

View File

@ -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",

View File

@ -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<OneRegisterInstruction>(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,

View File

@ -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,

View File

@ -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 ->

View File

@ -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<MethodReference>()?.name == "setContentView"
} + 1
val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.name == "findViewById" &&
reference.definingClass != "Landroid/view/View;"
}
val freeIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST)
val freeRegister =
getInstruction<FiveRegisterInstruction>(viewStubFindViewByIdIndex).registerD
val jumpIndex = indexOfFirstInstructionReversedOrThrow(
viewStubFindViewByIdIndex,
Opcode.IGET_OBJECT
)
getInstruction<OneRegisterInstruction>(freeIndex).registerA
val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
} + 1
addInstructionsWithLabels(
insertIndex, """

View File

@ -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
}
}
)

View File

@ -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(

View File

@ -124,14 +124,17 @@ val navigationBarComponentsPatch = bytecodePatch(
opcode == Opcode.IGET_OBJECT &&
getReference<FieldReference>()?.type == "Ljava/lang/String;"
}
val browseIdReference = getInstruction<ReferenceInstruction>(browseIdIndex).reference as FieldReference
val browseIdReference =
getInstruction<ReferenceInstruction>(browseIdIndex).reference as FieldReference
val fieldName = browseIdReference.name
val componentIndex = indexOfFirstInstructionOrThrow(stringIndex) {
opcode == Opcode.IGET_OBJECT &&
getReference<FieldReference>()?.toString() == browseIdReference.toString()
}
val browseIdRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerA
val componentRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerB
val browseIdRegister =
getInstruction<TwoRegisterInstruction>(componentIndex).registerA
val componentRegister =
getInstruction<TwoRegisterInstruction>(componentIndex).registerB
val enumIndex = it.patternMatch!!.startIndex + 3
val enumRegister = getInstruction<OneRegisterInstruction>(enumIndex).registerA

View File

@ -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<MethodReference>()?.name == "booleanValue"

View File

@ -746,15 +746,22 @@ val playerComponentsPatch = bytecodePatch(
val freeRegister =
getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
val getFieldIndex = bottomSheetBehaviorIndex - 2
val getFieldReference =
getInstruction<ReferenceInstruction>(getFieldIndex).reference
val getFieldInstruction = getInstruction<TwoRegisterInstruction>(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")
}

View File

@ -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.
)
)
}

View File

@ -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,
)

View File

@ -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<MethodReference>()?.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"
}
}

View File

@ -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)

View File

@ -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")
}
}

View File

@ -25,7 +25,8 @@ val videoTypeHookPatch = bytecodePatch(
videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply {
val getEnumIndex = indexOfGetEnumInstruction(this)
val enumClass = (getInstruction<ReferenceInstruction>(getEnumIndex).reference as MethodReference).definingClass
val enumClass =
(getInstruction<ReferenceInstruction>(getEnumIndex).reference as MethodReference).definingClass
val referenceIndex = indexOfFirstInstructionOrThrow(getEnumIndex) {
opcode == Opcode.SGET_OBJECT &&
getReference<FieldReference>()?.type == enumClass

View File

@ -71,7 +71,8 @@ val playerResponseMethodHookPatch = bytecodePatch(
val beforeVideoIdHooks =
hooks.filterIsInstance<Hook.PlayerParameterBeforeVideoId>().asReversed()
val videoIdHooks = hooks.filterIsInstance<Hook.VideoId>().asReversed()
val videoIdAndPlaylistIdHooks = hooks.filterIsInstance<Hook.VideoIdAndPlaylistId>().asReversed()
val videoIdAndPlaylistIdHooks =
hooks.filterIsInstance<Hook.VideoIdAndPlaylistId>().asReversed()
val afterVideoIdHooks = hooks.filterIsInstance<Hook.PlayerParameter>().asReversed()
// Add the hooks in this specific order as they insert instructions at the beginning of the method.

View File

@ -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()) {

Some files were not shown because too many files have changed in this diff Show More