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 | | 💊 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 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 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. | 19.05.36 ~ 19.47.53 |
| `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 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. | 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. | 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. | 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. | 19.05.36 ~ 19.47.53 |
| `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 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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `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. | 18.29.38 ~ 19.44.39 | | `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 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. | 18.29.38 ~ 19.44.39 | | `Disable layout updates` | Adds an option to disable layout updates by server. | 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. | 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. | 19.05.36 ~ 19.47.53 |
| `Disable splash animation` | Adds an option to disable the splash animation 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. | 19.05.36 ~ 19.47.53 |
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 18.29.38 ~ 19.44.39 | | `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 19.05.36 ~ 19.47.53 |
| `Enable debug logging` | Adds an option to enable debug logging. | 18.29.38 ~ 19.44.39 | | `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 gradient loading screen` | Adds an option to enable the gradient loading screen. | 18.29.38 ~ 19.44.39 | | `Enable debug logging` | Adds an option to enable debug logging. | 19.05.36 ~ 19.47.53 |
| `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 18.29.38 ~ 19.44.39 | | `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 19.05.36 ~ 19.47.53 |
| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 18.29.38 ~ 19.44.39 | | `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 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. | 18.29.38 ~ 19.44.39 | | `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 19.05.36 ~ 19.47.53 |
| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts videos. | 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. | 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?'. | 18.29.38 ~ 19.44.39 | | `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 action buttons` | Adds options to hide action buttons under 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?'. | 19.05.36 ~ 19.47.53 |
| `Hide ads` | Adds options to hide ads. | 18.29.38 ~ 19.44.39 | | `Hide action buttons` | Adds options to hide action buttons under videos. | 19.05.36 ~ 19.47.53 |
| `Hide comments components` | Adds options to hide components related to comments. | 18.29.38 ~ 19.44.39 | | `Hide ads` | Adds options to hide ads. | 19.05.36 ~ 19.47.53 |
| `Hide feed components` | Adds options to hide components related to feeds. | 18.29.38 ~ 19.44.39 | | `Hide comments components` | Adds options to hide components related to comments. | 19.05.36 ~ 19.47.53 |
| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 18.29.38 ~ 19.44.39 | | `Hide feed components` | Adds options to hide components related to feeds. | 19.05.36 ~ 19.47.53 |
| `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.44.39 | | `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 19.05.36 ~ 19.47.53 |
| `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.44.39 | | `Hide layout components` | Adds options to hide general layout components. | 19.05.36 ~ 19.47.53 |
| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.44.39 | | `Hide player buttons` | Adds options to hide buttons in the video player. | 19.05.36 ~ 19.47.53 |
| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when the app icon is long pressed. | 18.29.38 ~ 19.44.39 | | `Hide player flyout menu` | Adds options to hide player flyout menu components. | 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. | 18.29.38 ~ 19.44.39 | | `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 download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 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. | 19.05.36 ~ 19.47.53 |
| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 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. | 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. | 18.29.38 ~ 19.44.39 | | `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 19.05.36 ~ 19.47.53 |
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 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. | 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. | 18.29.38 ~ 19.44.39 | | `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 19.05.36 ~ 19.47.53 |
| `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 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. | 19.05.36 ~ 19.47.53 |
| `Player components` | Adds options to hide or change components related to the video player. | 18.29.38 ~ 19.44.39 | | `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 19.05.36 ~ 19.47.53 |
| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 18.29.38 ~ 19.44.39 | | `Player components` | Adds options to hide or change components related to the video player. | 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. | 18.29.38 ~ 19.44.39 | | `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 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. | 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. | 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. | 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. | 19.05.36 ~ 19.47.53 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 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. | 19.05.36 ~ 19.47.53 |
| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 18.29.38 ~ 19.44.39 | | `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 19.05.36 ~ 19.47.53 |
| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 18.29.38 ~ 19.44.39 | | `Seekbar components` | Adds options to hide or change components related to the seekbar. | 19.05.36 ~ 19.47.53 |
| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 18.29.38 ~ 19.44.39 | | `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 19.05.36 ~ 19.47.53 |
| `Snack bar components` | Adds options to hide or change components related to the snack bar. | 18.29.38 ~ 19.44.39 | | `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 19.05.36 ~ 19.47.53 |
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 18.29.38 ~ 19.44.39 | | `Snack bar components` | Adds options to hide or change components related to the snack bar. | 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. | 18.29.38 ~ 19.44.39 | | `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 streaming data` | Adds options to spoof the streaming data to allow playback. | 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. | 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. | 18.29.38 ~ 19.44.39 | | `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 19.05.36 ~ 19.47.53 |
| `Theme` | Changes the app's themes to the values specified in patch options. | 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. | 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. | 18.29.38 ~ 19.44.39 | | `Theme` | Changes the app's themes to the values specified in patch options. | 19.05.36 ~ 19.47.53 |
| `Translations for YouTube` | Add translations or remove string resources. | 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. | 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. | 18.29.38 ~ 19.44.39 | | `Translations for YouTube` | Add translations or remove string resources. | 19.05.36 ~ 19.47.53 |
| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 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. | 19.05.36 ~ 19.47.53 |
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 18.29.38 ~ 19.44.39 | | `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> </details>
### [📦 `com.google.android.apps.youtube.music`](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music) ### [📦 `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 | | 💊 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 | | `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.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.12.53 |
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 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.12.53 |
| `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 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.10.51 | | `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.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.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.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.12.53 |
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 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.12.53 |
| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 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.12.53 |
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.10.51 | | `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.10.51 | | `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.10.51 | | `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.10.51 | | `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.10.51 | | `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.10.51 | | `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.10.51 | | `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.10.51 | | `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.10.51 | | `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.10.51 | | `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.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.12.53 |
| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.10.51 | | `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.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.12.53 |
| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.10.51 | | `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.10.51 | | `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.10.51 | | `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.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.12.53 |
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 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.12.53 |
| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.10.51 | | `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.10.51 | | `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.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.12.53 |
| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 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.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.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.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.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.12.53 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 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.12.53 |
| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 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.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.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.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 ~ 7.16.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 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.12.53 |
| `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.12.53 |
| `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.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.10.51 | | `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.12.53 |
| `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.12.53 |
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.10.51 |
</details> </details>
### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage) ### [📦 `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 | | 💊 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 | | `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.05.1 | | `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.05.1 | | `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.05.1 | | `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.05.1 | | `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.05.1 | | `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.05.1 | | `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.05.1 | | `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.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.12.0 |
| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.05.1 | | `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.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.12.0 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 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.12.0 |
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.05.1 | | `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.12.0 |
</details> </details>
@ -165,13 +165,11 @@ Example:
"use":true, "use":true,
"compatiblePackages": { "compatiblePackages": {
"com.google.android.youtube": [ "com.google.android.youtube": [
"18.29.38",
"18.33.40",
"18.38.44",
"18.48.39",
"19.05.36", "19.05.36",
"19.16.39", "19.16.39",
"19.44.39" "19.43.41",
"19.44.39",
"19.47.53"
] ]
}, },
"options": [] "options": []
@ -189,7 +187,7 @@ Example:
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -201,7 +199,8 @@ Example:
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.0"
] ]
}, },
"options": [] "options": []

View File

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

View File

@ -108,13 +108,13 @@ public class FlyoutPatch {
if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() && if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() &&
textView.getParent() instanceof ViewGroup clickAbleArea) { textView.getParent() instanceof ViewGroup clickAbleArea) {
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label")); 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())); imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
clickAbleArea.setOnClickListener(view -> { clickAbleArea.setOnClickListener(view -> {
clickView(touchOutSideViewRef.get()); clickView(touchOutSideViewRef.get());
VideoUtils.openInYouTube(); VideoUtils.openInYouTube();
}); });
}, 0L }, 0L
); );
} }
} }
@ -126,14 +126,14 @@ public class FlyoutPatch {
textView.getParent() instanceof ViewGroup clickAbleArea textView.getParent() instanceof ViewGroup clickAbleArea
) { ) {
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
textView.setText(str("playback_rate_title")); textView.setText(str("playback_rate_title"));
imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext())); imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
imageView.setColorFilter(cf); imageView.setColorFilter(cf);
clickAbleArea.setOnClickListener(view -> { clickAbleArea.setOnClickListener(view -> {
clickView(touchOutSideViewRef.get()); clickView(touchOutSideViewRef.get());
VideoUtils.showPlaybackSpeedFlyoutMenu(); VideoUtils.showPlaybackSpeedFlyoutMenu();
}); });
}, 0L }, 0L
); );
} }
} }

View File

@ -9,13 +9,11 @@ import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.ResourceUtils;
/**
* @noinspection ALL
*/
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class GeneralPatch { 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() { public static boolean hideSoundSearchButton() {
return Settings.HIDE_SOUND_SEARCH_BUTTON.get(); return Settings.HIDE_SOUND_SEARCH_BUTTON.get();
} }
@ -123,7 +128,7 @@ public class GeneralPatch {
* <p> * <p>
* The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called. * 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. * 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> * <p>
* That's why {@link AlertDialog#show()} is absolutely necessary. * That's why {@link AlertDialog#show()} is absolutely necessary.
* Instead, use two tricks to hide Alertdialog. * Instead, use two tricks to hide Alertdialog.

View File

@ -7,15 +7,12 @@ import androidx.annotation.NonNull;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest; import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation; import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.music.utils.VideoUtils; 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.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class AlbumMusicVideoPatch { public class AlbumMusicVideoPatch {
@ -40,7 +37,7 @@ public class AlbumMusicVideoPatch {
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK"; private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false); private static volatile boolean isVideoLaunched = false;
@NonNull @NonNull
private static volatile String playerResponseVideoId = ""; private static volatile String playerResponseVideoId = "";
@ -100,14 +97,6 @@ public class AlbumMusicVideoPatch {
if (request == null) { if (request == null) {
return; 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(); String songId = request.getStream();
if (songId.isEmpty()) { if (songId.isEmpty()) {
Logger.printDebug(() -> "Official song not found, videoId: " + videoId); Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
@ -149,17 +138,16 @@ public class AlbumMusicVideoPatch {
private static void openMusic(@NonNull String songId) { private static void openMusic(@NonNull String songId) {
try { try {
isVideoLaunched.compareAndSet(false, true);
// The newly opened video is not a music video. // 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 // To prevent fetch requests from being sent, set the video id to the newly opened video
VideoUtils.runOnMainThreadDelayed(() -> { VideoUtils.runOnMainThreadDelayed(() -> {
isVideoLaunched = true;
playerResponseVideoId = songId; playerResponseVideoId = songId;
currentVideoId = songId; currentVideoId = songId;
VideoUtils.openInYouTubeMusic(songId); VideoUtils.openInYouTubeMusic(songId);
}, 1000); VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched = false, 3000);
}, 1500);
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 2500);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "openMusic failure", ex); Logger.printException(() -> "openMusic failure", ex);
} }
@ -191,7 +179,7 @@ public class AlbumMusicVideoPatch {
* Injection point. * Injection point.
*/ */
public static boolean hideSnackBar() { 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 org.apache.commons.lang3.BooleanUtils;
import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;

View File

@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.misc.requests
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.settings.AppLanguage import app.revanced.extension.shared.settings.AppLanguage
import app.revanced.extension.shared.utils.Logger 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" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE, GET_PLAYLIST_PAGE,
clientType clientType
) )
/** /**
* For some reason, the tracks in Top Songs have the playlistId of the album: * 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) * [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. * So we can work around this by setting the language to English when sending the request.
*/ */
val requestBody = val requestBody =
PlayerRoutes.createApplicationRequestBody( createApplicationRequestBody(
clientType = clientType, clientType = clientType,
videoId = videoId, videoId = videoId,
playlistId = playlistId, 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.FALSE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull; 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_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_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_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_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_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); 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 // region Migration
// Old spoof versions that no longer work reliably. // 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"); Logger.printInfo(() -> "Resetting spoof app version target");
SPOOF_APP_VERSION_TARGET.resetToDefault(); 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); public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true);
// Layout // 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_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_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); 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 android.os.Build
import app.revanced.extension.shared.patches.PatchStatus
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.PackageUtils import app.revanced.extension.shared.utils.PackageUtils
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
@ -212,8 +213,15 @@ object YouTubeAppClient {
return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get() 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> { 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)) { if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size) val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
@ -230,7 +238,7 @@ object YouTubeAppClient {
} }
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION", "unused")
enum class ClientType( enum class ClientType(
/** /**
* [YouTube client type](https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients) * [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. * If true, 'Authorization' must be included.
*/ */
val requireAuth: Boolean = false, 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. * Client name for innertube body.
*/ */
@ -363,7 +367,7 @@ object YouTubeAppClient {
else else
"iOS TV" "iOS TV"
), ),
IOS( IOS_DEPRECATED(
id = 5, id = 5,
deviceMake = DEVICE_MAKE_IOS, deviceMake = DEVICE_MAKE_IOS,
deviceModel = DEVICE_MODEL_IOS, deviceModel = DEVICE_MODEL_IOS,
@ -372,7 +376,6 @@ object YouTubeAppClient {
userAgent = USER_AGENT_IOS, userAgent = USER_AGENT_IOS,
clientVersion = CLIENT_VERSION_IOS, clientVersion = CLIENT_VERSION_IOS,
supportsCookies = false, supportsCookies = false,
requirePoToken = true,
clientName = "IOS", clientName = "IOS",
friendlyName = if (forceAVC()) friendlyName = if (forceAVC())
"iOS Force AVC" "iOS Force AVC"
@ -381,12 +384,20 @@ object YouTubeAppClient {
); );
companion object { companion object {
val CLIENT_ORDER_TO_USE_YOUTUBE: Array<ClientType> = arrayOf( val CLIENT_ORDER_TO_USE: Array<ClientType> = arrayOf(
ANDROID_VR_NO_AUTH, ANDROID_VR_NO_AUTH,
ANDROID_UNPLUGGED, ANDROID_UNPLUGGED,
ANDROID_CREATOR, ANDROID_CREATOR,
IOS_UNPLUGGED, 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, 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 android.os.Build;
import java.util.Locale; import java.util.Locale;
public class MusicAppClient { public class YouTubeMusicAppClient {
// Response to the '/next' request is 'Please update to continue using the app': // Response to the '/next' request is 'Please update to continue using the app':
// https://github.com/inotia00/ReVanced_Extended/issues/2743 // 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 DEVICE_MAKE_IOS_MUSIC = "Apple";
private static final String OS_NAME_IOS_MUSIC = "iOS"; private static final String OS_NAME_IOS_MUSIC = "iOS";
private MusicAppClient() { private YouTubeMusicAppClient() {
} }
private static String androidUserAgent(String clientVersion) { 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. * Used to fetch video information.
*/ */
@Suppress("unused") @Suppress("unused")
object YouTubeWebClient { 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 = 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)" "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. * Client version.
*/ */
@JvmField @JvmField
val clientVersion: String val clientVersion: String,
) { ) {
MWEB( MWEB(
id = 2, id = 2,
clientVersion = "2.20241202.07.00" clientVersion = "2.20241202.07.00",
), ),
WEB_REMIX( WEB_REMIX(
id = 29, 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 * Therefore, make sure that the dialog contains the ads at the beginning of the Method
* *
* @param bytes proto buffer array * @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) { public static void checkDialog(byte[] bytes, int type) {
if (!HIDE_FULLSCREEN_ADS) { 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. // Replace this with true If the Spoof streaming data patch succeeds in YouTube.
return false; 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.FALSE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan; import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;

View File

@ -2,8 +2,8 @@ package app.revanced.extension.shared.patches;
import android.net.Uri; import android.net.Uri;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class WatchHistoryPatch { public final class WatchHistoryPatch {

View File

@ -1,11 +1,11 @@
package app.revanced.extension.shared.patches.spoof; package app.revanced.extension.shared.patches.spoof;
import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType; import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofClientPatch extends BlockRequestPatch { 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. * Injection point.

View File

@ -10,21 +10,21 @@ import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; 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.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger; 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.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch { 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 = private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION =
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get(); 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. * Any unreachable ip address. Used to intentionally fail requests.
@ -69,17 +69,27 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* Skip response encryption in OnesiePlayerRequest. * Skip response encryption in OnesiePlayerRequest.
*/ */
public static boolean skipResponseEncryption(boolean original) { public static boolean skipResponseEncryption(boolean original) {
if (SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) { if (!SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
return false; 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. * 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) { if (SPOOF_STREAMING_DATA) {
String id = Utils.getVideoIdFromRequest(url); String id = Utils.getVideoIdFromRequest(url);
if (id == null) { if (id == null) {
@ -89,7 +99,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return; return;
} }
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN); StreamingDataRequest.fetchRequest(id, requestHeader);
} }
} }
@ -210,6 +220,18 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return videoFormat; 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 { public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
@Override @Override
public boolean isAvailable() { 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 package app.revanced.extension.shared.patches.spoof.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.StringRef.str
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import org.apache.commons.lang3.StringUtils
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@ -32,21 +32,19 @@ import java.util.concurrent.TimeoutException
* did use its own client streams. * did use its own client streams.
*/ */
class StreamingDataRequest private constructor( class StreamingDataRequest private constructor(
videoId: String, playerHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String requestHeader: Map<String, String>,
) { ) {
private val videoId: String private val videoId: String
private val future: Future<ByteBuffer?> private val future: Future<ByteBuffer?>
init { init {
Objects.requireNonNull(playerHeaders) Objects.requireNonNull(requestHeader)
this.videoId = videoId this.videoId = videoId
this.future = Utils.submitOnBackgroundThread { this.future = Utils.submitOnBackgroundThread {
fetch( fetch(
videoId, videoId,
playerHeaders, requestHeader,
visitorId,
botGuardPoToken
) )
} }
} }
@ -86,33 +84,16 @@ class StreamingDataRequest private constructor(
companion object { companion object {
private const val AUTHORIZATION_HEADER = "Authorization" private const val AUTHORIZATION_HEADER = "Authorization"
private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id" private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
private val REQUEST_HEADER_KEYS = arrayOf(
AUTHORIZATION_HEADER, // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
VISITOR_ID_HEADER
)
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType = private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get() BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> = private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE) YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean = private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null private var lastSpoofedClientFriendlyName: String? = 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
@GuardedBy("itself") @GuardedBy("itself")
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap( val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
@ -126,22 +107,24 @@ class StreamingDataRequest private constructor(
@JvmStatic @JvmStatic
val lastSpoofedClientName: String val lastSpoofedClientName: String
get() = lastSpoofedClientType get() {
?.friendlyName return if (lastSpoofedClientFriendlyName != null) {
?: "Unknown" lastSpoofedClientFriendlyName!!
} else {
"Unknown"
}
}
@JvmStatic @JvmStatic
fun fetchRequest( fun fetchRequest(
videoId: String, fetchHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String fetchHeaders: Map<String, String>,
) { ) {
// Always fetch, even if there is an existing request for the same video. // Always fetch, even if there is an existing request for the same video.
cache[videoId] = cache[videoId] =
StreamingDataRequest( StreamingDataRequest(
videoId, videoId,
fetchHeaders, fetchHeaders
visitorId,
botGuardPoToken
) )
} }
@ -150,71 +133,40 @@ class StreamingDataRequest private constructor(
return cache[videoId] 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) Logger.printInfo({ toastMessage }, ex)
} }
private fun send( private fun send(
clientType: YouTubeAppClient.ClientType, clientType: YouTubeAppClient.ClientType,
videoId: String, videoId: String,
playerHeaders: Map<String, String>, requestHeader: Map<String, String>,
visitorId: String,
botGuardPoToken: String
): HttpURLConnection? { ): HttpURLConnection? {
Objects.requireNonNull(clientType) Objects.requireNonNull(clientType)
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
Objects.requireNonNull(playerHeaders) Objects.requireNonNull(requestHeader)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" } Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
try { try {
val connection = val connection =
getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType) getInnerTubeResponseConnectionFromRoute(
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS GET_STREAMING_DATA,
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS clientType,
requestHeader
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,
) )
Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
} else { val requestBody = createApplicationRequestBody(
requestBody = clientType = clientType,
createApplicationRequestBody( videoId = videoId,
clientType = clientType, setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
videoId = videoId, )
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
)
}
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -243,15 +195,15 @@ class StreamingDataRequest private constructor(
} }
private fun fetch( private fun fetch(
videoId: String, playerHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String requestHeader: Map<String, String>,
): ByteBuffer? { ): ByteBuffer? {
lastSpoofedClientType = null lastSpoofedClientFriendlyName = null
// Retry with different client if empty response body is received. // Retry with different client if empty response body is received.
for (clientType in CLIENT_ORDER_TO_USE) { for (clientType in CLIENT_ORDER_TO_USE) {
if (clientType.requireAuth && 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" } Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
continue continue
@ -259,9 +211,7 @@ class StreamingDataRequest private constructor(
send( send(
clientType, clientType,
videoId, videoId,
playerHeaders, requestHeader,
visitorId,
botGuardPoToken
)?.let { connection -> )?.let { connection ->
try { try {
// gzip encoding doesn't response with content length (-1), // gzip encoding doesn't response with content length (-1),
@ -271,14 +221,14 @@ class StreamingDataRequest private constructor(
} else { } else {
BufferedInputStream(connection.inputStream).use { inputStream -> BufferedInputStream(connection.inputStream).use { inputStream ->
ByteArrayOutputStream().use { stream -> ByteArrayOutputStream().use { stream ->
val buffer = ByteArray(2048) val buffer = ByteArray(4096)
var bytesRead: Int var bytesRead: Int
while ((inputStream.read(buffer) while ((inputStream.read(buffer)
.also { bytesRead = it }) >= 0 .also { bytesRead = it }) >= 0
) { ) {
stream.write(buffer, 0, bytesRead) stream.write(buffer, 0, bytesRead)
} }
lastSpoofedClientType = clientType lastSpoofedClientFriendlyName = clientType.friendlyName
return ByteBuffer.wrap(stream.toByteArray()) 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 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.FALSE;
import static java.lang.Boolean.TRUE; 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.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType; 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; 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. * 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 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. * These settings are used by YouTube.
@ -43,11 +43,9 @@ public class BaseSettings {
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message"); "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_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_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. // 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 EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, 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);
/** /**
* These settings are used by YouTube and YouTube Music. * 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.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger; 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.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public abstract class AbstractPreferenceFragment extends PreferenceFragment { public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/** /**
* Indicates that if a preference changes, * Indicates that if a preference changes,
* to apply the change from the Setting to the UI component. * 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; 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. * Set by subclasses if Strings cannot be added as a resource.
*/ */
@Nullable @Nullable
@ -52,7 +56,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try { 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) { if (setting == null) {
return; 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'. // 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); updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different. // Update any other preference availability that may now be different.
updateUIAvailability(); updateUIAvailability();
updatingPreference = false;
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
} }
@ -103,36 +117,39 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
Utils.verifyOnMainThread(); Utils.verifyOnMainThread();
final var context = getActivity(); final var context = getActivity();
showingUserDialogMessage = true; final StringRef userDialogMessage = setting.userDialogMessage;
assert setting.userDialogMessage != null; if (context != null && userDialogMessage != null) {
new AlertDialog.Builder(context) showingUserDialogMessage = true;
.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);
// Update availability of other preferences that may be changed. new AlertDialog.Builder(context)
updateUIAvailability(); .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) { // Update availability of other preferences that may be changed.
showRestartDialog(context); updateUIAvailability();
}
}) if (setting.rebootApp) {
.setNegativeButton(android.R.string.cancel, (dialog, id) -> { showRestartDialog(context);
// Restore whatever the setting was before the change. }
updatePreference(pref, setting, true, true); })
}) .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
.setOnDismissListener(dialog -> showingUserDialogMessage = false) // Restore whatever the setting was before the change.
.setCancelable(false) updatePreference(pref, setting, true, true);
.show(); })
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
.setCancelable(false)
.show();
}
} }
/** /**
* Updates all Preferences values and their availability using the current values in {@link Setting}. * Updates all Preferences values and their availability using the current values in {@link Setting}.
*/ */
protected void updateUIToSettingValues() { 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. * @return If the preference is currently set to the default value of the Setting.
*/ */
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) { protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
Object defaultValue = setting.defaultValue;
if (pref instanceof SwitchPreference switchPref) { 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) { if (pref instanceof EditTextPreference editPreference) {
return editPreference.getText().equals(setting.defaultValue.toString()); return editPreference.getText().equals(defaultValueString);
} }
if (pref instanceof ListPreference listPref) { 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 " 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. * 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. * @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, 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); listPreference.setSummary(objectStringValue);
} }
public static void showRestartDialog(@NonNull final Context context) { public static void showRestartDialog(@NonNull Context context) {
if (restartDialogMessage == null) { if (restartDialogMessage == null) {
restartDialogMessage = str("revanced_extended_restart_message"); restartDialogMessage = str("revanced_extended_restart_message");
} }
showRestartDialog(context, restartDialogMessage); 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); 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(); Utils.verifyOnMainThread();
new AlertDialog.Builder(context) new AlertDialog.Builder(context)

View File

@ -10,6 +10,8 @@ import android.util.AttributeSet;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import androidx.annotation.Nullable;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
@ -19,6 +21,12 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public class ResettableEditTextPreference extends EditTextPreference { public class ResettableEditTextPreference extends EditTextPreference {
/**
* Setting to reset.
*/
@Nullable
private Setting<?> setting;
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes); super(context, attrs, defStyleAttr, defStyleRes);
} }
@ -35,6 +43,10 @@ public class ResettableEditTextPreference extends EditTextPreference {
super(context); super(context);
} }
public void setSetting(@Nullable Setting<?> setting) {
this.setting = setting;
}
@Override @Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder); Utils.setEditTextDialogTheme(builder);
@ -44,7 +56,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
if (title != null) { if (title != null) {
builder.setTitle(getTitle()); 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) { if (setting != null) {
builder.setNeutralButton(str("revanced_extended_settings_reset"), null); builder.setNeutralButton(str("revanced_extended_settings_reset"), null);
} }
@ -65,8 +82,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
} }
button.setOnClickListener(v -> { button.setOnClickListener(v -> {
try { try {
Setting<?> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
String defaultStringValue = setting.defaultValue.toString();
EditText editText = getEditText(); EditText editText = getEditText();
editText.setText(defaultStringValue); editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text 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) { public static boolean isPackageEnabled(@NonNull String packageName) {
try { ApplicationInfo applicationInfo = getApplicationInfo(packageName);
return getContext().getPackageManager().getApplicationInfo(packageName, 0).enabled; if (applicationInfo != null) {
} catch (PackageManager.NameNotFoundException ignored) { return applicationInfo.enabled;
} }
return false; return false;
@ -47,6 +57,16 @@ public class PackageUtils extends Utils {
} }
// 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 @Nullable
private static PackageInfo getPackageInfo() { private static PackageInfo getPackageInfo() {
try { try {

View File

@ -60,6 +60,7 @@ public class Utils {
private static WeakReference<Activity> activityRef = new WeakReference<>(null); private static WeakReference<Activity> activityRef = new WeakReference<>(null);
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private static volatile Context context; private static volatile Context context;
private static Locale contextLocale;
protected Utils() { protected Utils() {
} // utility class } // utility class
@ -308,34 +309,51 @@ public class Utils {
* @return Context with locale applied. * @return Context with locale applied.
*/ */
public static Context getLocalizedContext(Context mContext) { public static Context getLocalizedContext(Context mContext) {
Activity mActivity = activityRef.get(); try {
if (mActivity == null) { Activity mActivity = activityRef.get();
return mContext; if (mActivity != null && mContext != null) {
} AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
if (mContext == null) {
return null; // 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. public static void resetLocalizedContext() {
Locale applicationLocale = language == AppLanguage.DEFAULT try {
? mActivity.getResources().getConfiguration().locale if (contextLocale != null) {
: language.getLocale(); Locale.setDefault(contextLocale);
Context mContext = getContext();
// Locale of Context. if (mContext != null) {
Locale contextLocale = mContext.getResources().getConfiguration().locale; Configuration config = mContext.getResources().getConfiguration();
config.setLocale(contextLocale);
// If they are identical, no need to override them. setContext(mContext.createConfigurationContext(config));
if (applicationLocale == contextLocale) { }
return mContext; }
} 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) { public static void setActivity(Activity mainActivity) {
@ -353,14 +371,6 @@ public class Utils {
// Must initially set context to check the app language. // Must initially set context to check the app language.
context = appContext; context = appContext;
Logger.initializationInfo(Utils.class, "Set 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) { public static void setClipboard(@NonNull String text) {
@ -538,14 +548,6 @@ public class Utils {
return Build.VERSION.SDK_INT >= sdk; 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) { public static int dpToPx(int dp) {
if (context == null) { if (context == null) {
return dp; return dp;
@ -608,10 +610,10 @@ public class Utils {
* <br> * <br>
* Be aware the on start action can be called multiple times for some situations, * 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. * 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, * 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. * 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 * For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog. * 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.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter; import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup; import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused") @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.shared.utils.Logger
import app.revanced.extension.youtube.settings.Settings import app.revanced.extension.youtube.settings.Settings
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import kotlin.Boolean
@Suppress("unused") @Suppress("unused")
object ChangeStartPagePatch { object ChangeStartPagePatch {
@ -44,7 +43,7 @@ object ChangeStartPagePatch {
} }
appLaunched = true appLaunched = true
Logger.printDebug{ "Changing browseId to $browseId" } Logger.printDebug { "Changing browseId to $browseId" }
return browseId return browseId
} }

View File

@ -1,18 +1,34 @@
package app.revanced.extension.youtube.patches.general; 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.shared.utils.Logger;
import app.revanced.extension.youtube.patches.utils.PlaylistPatch;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class DownloadActionsPatch extends VideoUtils { public final class DownloadActionsPatch {
private static final BooleanSetting overrideVideoDownloadButton = private static final boolean OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON =
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON; Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON.get();
private static final BooleanSetting overridePlaylistDownloadButton = private static final boolean OVERRIDE_VIDEO_DOWNLOAD_BUTTON =
Settings.OVERRIDE_PLAYLIST_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. * Injection point.
@ -23,17 +39,21 @@ public final class DownloadActionsPatch extends VideoUtils {
* <p> * <p>
* Appears to always be called from the main thread. * 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 { try {
if (!overrideVideoDownloadButton.get()) { if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(videoId)) {
return false; if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER) {
} if (map != null && map.get(ELEMENTS_SENDER_VIEW) instanceof View view) {
if (videoId == null || videoId.isEmpty()) { PlaylistPatch.setContext(view.getContext());
return false; }
} PlaylistPatch.prepareDialogBuilder(videoId);
launchVideoExternalDownloader(videoId); } else {
launchVideoExternalDownloader(videoId);
}
return true; return true;
}
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex); Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex);
} }
@ -49,15 +69,10 @@ public final class DownloadActionsPatch extends VideoUtils {
*/ */
public static String inAppPlaylistDownloadButtonOnClick(String playlistId) { public static String inAppPlaylistDownloadButtonOnClick(String playlistId) {
try { try {
if (!overridePlaylistDownloadButton.get()) { if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
return playlistId; launchPlaylistExternalDownloader(playlistId);
return "";
} }
if (playlistId == null || playlistId.isEmpty()) {
return playlistId;
}
launchPlaylistExternalDownloader(playlistId);
return "";
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex); Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex);
} }
@ -73,15 +88,10 @@ public final class DownloadActionsPatch extends VideoUtils {
*/ */
public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) { public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) {
try { try {
if (!overridePlaylistDownloadButton.get()) { if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
return false; launchPlaylistExternalDownloader(playlistId);
return true;
} }
if (playlistId == null || playlistId.isEmpty()) {
return false;
}
launchPlaylistExternalDownloader(playlistId);
return true;
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex); Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex);
} }
@ -92,7 +102,7 @@ public final class DownloadActionsPatch extends VideoUtils {
* Injection point. * Injection point.
*/ */
public static boolean overridePlaylistDownloadButtonVisibility() { 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.Map;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@ -105,6 +106,34 @@ public class GeneralPatch {
// endregion // 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 // region [Disable splash animation] patch
public static boolean disableSplashAnimation(boolean original) { 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) { public static boolean switchCreateWithNotificationButton(boolean original) {
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original; return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original;
} }

View File

@ -43,8 +43,8 @@ public final class OpenChannelOfLiveAvatarPatch {
/** /**
* Injection point. * Injection point.
* *
* @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor * @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
* @param newlyLoadedVideoId id of the current video * @param newlyLoadedVideoId id of the current video
*/ */
public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) { public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
try { try {

View File

@ -2,8 +2,10 @@ package app.revanced.extension.youtube.patches.general.requests
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils 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" } Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_DETAILS, GET_VIDEO_DETAILS,
clientType clientType
) )
val requestBody = val requestBody = createWebInnertubeBody(clientType, videoId)
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)

View File

@ -15,7 +15,16 @@ public class BackgroundPlaybackPatch {
*/ */
public static boolean isBackgroundPlaybackAllowed(boolean original) { public static boolean isBackgroundPlaybackAllowed(boolean original) {
if (original) return true; 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 androidx.annotation.Nullable;
import app.revanced.extension.shared.utils.Logger; 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.settings.Settings;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils; import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -19,7 +21,14 @@ public class ExternalDownload extends BottomControlButton {
bottomControlsViewGroup, bottomControlsViewGroup,
"external_download_button", "external_download_button",
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER, 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 null
); );
} }

View File

@ -1,5 +1,7 @@
package app.revanced.extension.youtube.patches.player; package app.revanced.extension.youtube.patches.player;
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.REMIX;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
@ -8,8 +10,6 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; 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.settings.BooleanSetting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
@ -106,8 +106,8 @@ public class ActionButtonsPatch {
/** /**
* Injection point. * Injection point.
* *
* @param list Type list of litho components * @param list Type list of litho components
* @param identifier Identifier of litho components * @param identifier Identifier of litho components
*/ */
public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) { public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
try { 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.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.IntegerSetting; 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.Logger;
import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils; 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.EngagementPanel;
import app.revanced.extension.youtube.shared.PlayerType; import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.RootView; 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.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils; import app.revanced.extension.youtube.utils.VideoUtils;
@ -441,7 +439,7 @@ public class PlayerPatch {
if (isLiveChatOrPlaylistPanel) { if (isLiveChatOrPlaylistPanel) {
return true; return true;
} }
return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed(); return isAutoPopupPanel && !RootView.isShortsActive();
} }
/** /**
@ -471,8 +469,8 @@ public class PlayerPatch {
* Used in YouTube 20.05.46+. * Used in YouTube 20.05.46+.
*/ */
public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) { if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) {
Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L); Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L);
} }
@ -518,6 +516,12 @@ public class PlayerPatch {
return SPEED_OVERLAY_VALUE; return SPEED_OVERLAY_VALUE;
} }
public static float speedOverlayRelativeValue(float original) {
return SPEED_OVERLAY_VALUE != 2.0f
? 0f
: original;
}
public static boolean hideChannelWatermark(boolean original) { public static boolean hideChannelWatermark(boolean original) {
return !Settings.HIDE_CHANNEL_WATERMARK.get() && original; return !Settings.HIDE_CHANNEL_WATERMARK.get() && original;
} }
@ -540,6 +544,10 @@ public class PlayerPatch {
return Settings.HIDE_FILMSTRIP_OVERLAY.get(); 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) { public static boolean hideInfoCard(boolean original) {
return !Settings.HIDE_INFO_CARDS.get() && 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. * 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. * Default YouTube seekbar color brightness.

View File

@ -1,8 +1,10 @@
package app.revanced.extension.youtube.patches.player.requests package app.revanced.extension.youtube.patches.player.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
class ActionButtonRequest private constructor( class ActionButtonRequest private constructor(
private val videoId: String, private val videoId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
) { ) {
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread { private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders) fetch(videoId, requestHeader)
} }
val array: Array<ActionButton> val array: Array<ActionButton>
@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
} }
companion object { 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 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
}) })
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) { fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(videoId)) { 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) Logger.printInfo({ toastMessage }, ex)
} }
private val REQUEST_HEADER_KEYS = arrayOf( private fun sendRequest(videoId: String, requestHeader: Map<String, String>): JSONObject? {
"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? {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// '/next' request does not require PoToken. // '/next' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { 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, // 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. // Set the [Authorization] field to property to get the correct action buttons.
for (key in REQUEST_HEADER_KEYS) { val connection = getInnerTubeResponseConnectionFromRoute(
var value = playerHeaders[key] GET_VIDEO_ACTION_BUTTON,
if (value != null) { clientType,
connection.setRequestProperty(key, value) requestHeader,
} )
}
val requestBody = val requestBody = createApplicationRequestBody(
PlayerRoutes.createApplicationRequestBody( clientType = clientType,
clientType = clientType, videoId = videoId
videoId = videoId )
)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
return emptyArray() return emptyArray()
} }
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Array<ActionButton> { private fun fetch(
val json = sendRequest(videoId, playerHeaders) videoId: String,
requestHeader: Map<String, String>
): Array<ActionButton> {
val json = sendRequest(videoId, requestHeader)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

@ -1,25 +1,15 @@
package app.revanced.extension.youtube.patches.shorts; package app.revanced.extension.youtube.patches.shorts;
import static app.revanced.extension.shared.utils.ResourceUtils.getString; 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.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible;
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
import android.app.AlertDialog;
import android.content.Context; 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.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ScrollView; 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.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter; import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState; import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.ThemeUtils;
import app.revanced.extension.youtube.utils.VideoUtils; import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -66,7 +55,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) { if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) {
return; return;
} }
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
if (!isMoreButton(enumString)) { if (!isMoreButton(enumString)) {
@ -90,105 +79,28 @@ public final class CustomActionsPatch {
}), 0); }), 0);
} }
private static void showMoreButtonDialog(Context context) { private static void showMoreButtonDialog(Context mContext) {
ScrollView scrollView = new ScrollView(context); ScrollView mScrollView = new ScrollView(mContext);
LinearLayout container = new LinearLayout(context); LinearLayout mLinearLayout = new LinearLayout(mContext);
mLinearLayout.setOrientation(LinearLayout.VERTICAL);
mLinearLayout.setPadding(0, 0, 0, 0);
container.setOrientation(LinearLayout.VERTICAL); Map<LinearLayout, Runnable> actionsMap = new LinkedHashMap<>(arrSize);
container.setPadding(0, 0, 0, 0);
Map<LinearLayout, Runnable> toolbarMap = new LinkedHashMap<>(arrSize);
for (CustomAction customAction : CustomAction.values()) { for (CustomAction customAction : CustomAction.values()) {
if (customAction.settings.get()) { if (customAction.settings.get()) {
String title = customAction.getLabel(); String title = customAction.getLabel();
int iconId = customAction.getDrawableId(); int iconId = customAction.getDrawableId();
Runnable action = customAction.getOnClickAction(); Runnable action = customAction.getOnClickAction();
LinearLayout itemLayout = createItemLayout(context, title, iconId); LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, iconId);
toolbarMap.putIfAbsent(itemLayout, action); actionsMap.putIfAbsent(itemLayout, action);
container.addView(itemLayout); mLinearLayout.addView(itemLayout);
} }
} }
scrollView.addView(container); mScrollView.addView(mLinearLayout);
AlertDialog.Builder builder = new AlertDialog.Builder(context); ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
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;
} }
private static boolean isMoreButton(String enumString) { private static boolean isMoreButton(String enumString) {
@ -206,7 +118,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return; return;
} }
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
if (bottomSheetMenuObject == null) { if (bottomSheetMenuObject == null) {
@ -224,7 +136,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return; return;
} }
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
for (CustomAction customAction : CustomAction.values()) { for (CustomAction customAction : CustomAction.values()) {
@ -243,6 +155,34 @@ public final class CustomActionsPatch {
Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject); 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. * Injection point.
*/ */
@ -252,7 +192,7 @@ public final class CustomActionsPatch {
} }
recyclerView.getViewTreeObserver().addOnDrawListener(() -> { recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try { try {
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
contextRef = new WeakReference<>(recyclerView.getContext()); contextRef = new WeakReference<>(recyclerView.getContext());
@ -267,8 +207,9 @@ public final class CustomActionsPatch {
if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) { if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) {
childCount = recyclerView.getChildCount(); childCount = recyclerView.getChildCount();
if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) { if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) {
String menuTitle = textView.getText().toString();
for (CustomAction customAction : CustomAction.values()) { for (CustomAction customAction : CustomAction.values()) {
if (customAction.getLabel().equals(textView.getText().toString())) { if (customAction.getLabel().equals(menuTitle)) {
View.OnClickListener onClick = customAction.getOnClickListener(); View.OnClickListener onClick = customAction.getOnClickListener();
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener(); View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
recyclerViewRef = new WeakReference<>(recyclerView); recyclerViewRef = new WeakReference<>(recyclerView);
@ -384,6 +325,11 @@ public final class CustomActionsPatch {
true true
) )
), ),
SPEED_DIALOG(
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG,
"yt_outline_play_arrow_half_circle_black_24",
() -> VideoUtils.showPlaybackSpeedDialog(contextRef.get())
),
REPEAT_STATE( REPEAT_STATE(
Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE, Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE,
"yt_outline_arrow_repeat_1_black_24", "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 android.app.Activity;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.Objects; import java.util.Objects;
@ -29,10 +31,15 @@ public class ShortsRepeatStatePatch {
static void setYTEnumValue(Enum<?> ytBehavior) { static void setYTEnumValue(Enum<?> ytBehavior) {
for (ShortsLoopBehavior rvBehavior : values()) { for (ShortsLoopBehavior rvBehavior : values()) {
if (ytBehavior.name().endsWith(rvBehavior.name())) { String ytName = ytBehavior.name();
rvBehavior.ytEnumValue = ytBehavior; if (ytName.endsWith(rvBehavior.name())) {
if (rvBehavior.ytEnumValue != null) {
Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytBehavior.name()); Logger.printException(() -> "Conflicting behavior names: " + rvBehavior
+ " ytBehavior: " + ytName);
} else {
rvBehavior.ytEnumValue = ytBehavior;
Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytName);
}
return; return;
} }
} }
@ -77,25 +84,39 @@ public class ShortsRepeatStatePatch {
/** /**
* Injection point. * Injection point.
*/ */
public static Enum<?> changeShortsRepeatBehavior(Enum<?> original) { @Nullable
public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) {
try { try {
final ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER && ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
isAppInBackgroundPiPMode() isAppInBackgroundPiPMode()
? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get() ? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get()
: Settings.CHANGE_SHORTS_REPEAT_STATE.get(); : Settings.CHANGE_SHORTS_REPEAT_STATE.get();
Enum<?> overrideBehavior = behavior.ytEnumValue;
if (behavior != ShortsLoopBehavior.UNKNOWN && behavior.ytEnumValue != null) { if (behavior != ShortsLoopBehavior.UNKNOWN && overrideBehavior != null) {
Logger.printDebug(() -> behavior.ytEnumValue == original Logger.printDebug(() -> {
? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue String name = original == null ? "unknown (null)" : original.name();
: "Behavior setting is same as original. Using original: " + 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) { } catch (Exception ex) {
Logger.printException(() -> "changeShortsRepeatState failure", ex); Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
} }
return original; 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 java.lang.ref.WeakReference;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
@ -59,4 +60,20 @@ public class SwipeControlsPatch {
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE; 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; package app.revanced.extension.youtube.patches.utils;
import app.revanced.extension.shared.utils.Logger; 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.PlayerType;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PlaybackSpeedWhilePlayingPatch { public class PlaybackSpeedWhilePlayingPatch {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
public static boolean playbackSpeedChanged(float playbackSpeed) { public static boolean playbackSpeedChanged(float playbackSpeed) {
PlayerType playerType = PlayerType.getCurrent(); if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED) {
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED && if (PlayerType.getCurrent().isMaximizedOrFullscreenOrPiP()
playerType.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; 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; 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; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class AV1CodecPatch { 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 final String VP9_CODEC = "video/x-vnd.on2.vp9";
private static long lastTimeResponse = 0;
/** /**
* Replace the SW AV01 codec to VP9 codec. * Replace the SW AV01 codec to VP9 codec.
@ -22,32 +15,4 @@ public class AV1CodecPatch {
public static String replaceCodec(String original) { public static String replaceCodec(String original) {
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : 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; return isCustomPlaybackSpeedEnabled() ? 0 : original;
} }
public static String[] getListEntries() { public static String[] getEntries() {
return isCustomPlaybackSpeedEnabled() return isCustomPlaybackSpeedEnabled()
? customSpeedEntries ? customSpeedEntries
: defaultSpeedEntries; : defaultSpeedEntries;
} }
public static String[] getListEntryValues() { public static String[] getEntryValues() {
return isCustomPlaybackSpeedEnabled() return isCustomPlaybackSpeedEnabled()
? customSpeedEntryValues ? customSpeedEntryValues
: defaultSpeedEntryValues; : defaultSpeedEntryValues;
} }
public static String[] getTrimmedListEntries() { public static String[] getTrimmedEntries() {
if (playbackSpeedEntries == null) { if (playbackSpeedEntries == null) {
final String[] playbackSpeedWithAutoEntries = getListEntries(); final String[] playbackSpeedWithAutoEntries = getEntries();
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length); playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
} }
return playbackSpeedEntries; return playbackSpeedEntries;
} }
public static String[] getTrimmedListEntryValues() { public static String[] getTrimmedEntryValues() {
if (playbackSpeedEntryValues == null) { if (playbackSpeedEntryValues == null) {
final String[] playbackSpeedWithAutoEntryValues = getListEntryValues(); final String[] playbackSpeedWithAutoEntryValues = getEntryValues();
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length); playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
} }

View File

@ -1,12 +1,14 @@
package app.revanced.extension.youtube.patches.video; package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str; 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.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils; 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.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.PatchStatus; import app.revanced.extension.youtube.patches.utils.PatchStatus;
@ -17,25 +19,57 @@ import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PlaybackSpeedPatch { 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 = private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC =
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get(); Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
private static final long TOAST_DELAY_MILLISECONDS = 750; private static final long TOAST_DELAY_MILLISECONDS = 750;
private static long lastTimeSpeedChanged; 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 boolean isLiveStream;
private static volatile String channelIdShorts = "";
private static volatile String videoIdShorts = "";
private static boolean isLiveStreamShorts;
/** /**
* Injection point. * Injection point.
*/ */
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
isLiveStream = newlyLoadedLiveStreamValue; if (isShortsActive()) {
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId); channelIdShorts = newlyLoadedChannelId;
videoIdShorts = newlyLoadedVideoId;
isLiveStreamShorts = newlyLoadedLiveStreamValue;
final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId); Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed); } 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. * Injection point.
*/ */
public static float getPlaybackSpeedInShorts(final float playbackSpeed) { public static float getPlaybackSpeed(float playbackSpeed) {
if (VideoInformation.lastPlayerResponseIsShort() && boolean isShorts = isShortsActive();
Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get() String currentChannelId = isShorts ? channelIdShorts : channelId;
) { String currentVideoId = isShorts ? videoIdShorts : videoId;
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null); boolean currentVideoIsLiveStream = isShorts ? isLiveStreamShorts : isLiveStream;
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed); 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) { public static void userSelectedPlaybackSpeed(float playbackSpeed) {
try { try {
if (PatchStatus.RememberPlaybackSpeed() && boolean isShorts = isShortsActive();
Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) { if (PatchStatus.RememberPlaybackSpeed()) {
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x BooleanSetting rememberPlaybackSpeedLastSelectedSetting = isShorts
// then the menu will allow increasing without bounds but the max speed is ? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED
// still capped to under 8.0x. : Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED;
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f); 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. if (rememberPlaybackSpeedLastSelectedSetting.get()) {
// Show exactly one toast after the user stops interacting with the speed menu. // With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
final long now = System.currentTimeMillis(); // then the menu will allow increasing without bounds but the max speed is
lastTimeSpeedChanged = now; // still capped to under 8.0x.
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
final float finalPlaybackSpeed = playbackSpeed; // Prevent toast spamming if using the 0.05x adjustments.
Utils.runOnMainThreadDelayed(() -> { // Show exactly one toast after the user stops interacting with the speed menu.
if (lastTimeSpeedChanged != now) { final long now = System.currentTimeMillis();
// The user made additional speed adjustments and this call is outdated. lastTimeSpeedChanged = now;
return;
}
if (Settings.DEFAULT_PLAYBACK_SPEED.get() == finalPlaybackSpeed) { final float finalPlaybackSpeed = playbackSpeed;
// User changed to a different speed and immediately changed back. Utils.runOnMainThreadDelayed(() -> {
// Or the user is going past 8.0x in the glitched out 0.05x menu. if (lastTimeSpeedChanged != now) {
return; // The user made additional speed adjustments and this call is outdated.
} return;
Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed); }
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()) { if (showToastSetting.get()) {
return; Utils.showToastShort(str(isShorts ? "revanced_remember_playback_speed_toast_shorts" : "revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
} }
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x"))); }, TOAST_DELAY_MILLISECONDS);
}, TOAST_DELAY_MILLISECONDS); }
} else if (!isShorts) {
lastSelectedPlaybackSpeed = playbackSpeed;
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex); Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex);
} }
} }
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) { private static boolean isMusic() {
return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId)) if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && !videoId.isEmpty()) {
? 1.0f
: Settings.DEFAULT_PLAYBACK_SPEED.get();
}
private static boolean isMusic(@Nullable String videoId) {
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) {
try { try {
MusicRequest request = MusicRequest.getRequestForVideoId(videoId); MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream()); final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.patches.video; package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str; 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.NonNull;
@ -14,8 +15,10 @@ import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class VideoQualityPatch { public class VideoQualityPatch {
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2; private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE; private static final IntegerSetting shortsQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS;
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI; 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 @NonNull
public static String videoId = ""; public static String videoId = "";
@ -35,12 +38,11 @@ public class VideoQualityPatch {
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) if (PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL &&
return; !videoId.equals(newlyLoadedVideoId)) {
if (videoId.equals(newlyLoadedVideoId)) videoId = newlyLoadedVideoId;
return; setVideoQuality(750);
videoId = newlyLoadedVideoId; }
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
} }
/** /**
@ -53,42 +55,62 @@ public class VideoQualityPatch {
); );
} }
private static void setVideoQuality(final long delayMillis) { private static void setVideoQuality(long delayMillis) {
final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE boolean isShorts = isShortsActive();
? mobileQualitySetting.get() IntegerSetting defaultQualitySetting = Utils.getNetworkType() == Utils.NetworkType.MOBILE
: wifiQualitySetting.get(); ? isShorts ? shortsQualityMobile : videoQualityMobile
: isShorts ? shortsQualityWifi : videoQualityWifi;
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY) int defaultQuality = defaultQualitySetting.get();
return;
Utils.runOnMainThreadDelayed(() -> { if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality); Utils.runOnMainThreadDelayed(() -> {
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal); final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
VideoInformation.overrideVideoQuality(qualityToUseFinal); Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
}, delayMillis VideoInformation.overrideVideoQuality(qualityToUseFinal);
); }, delayMillis
);
}
} }
private static void userSelectedVideoQuality(final int defaultQuality) { private static void userSelectedVideoQuality(final int defaultQuality) {
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
return; final Utils.NetworkType networkType = Utils.getNetworkType();
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY) String networkTypeMessage = networkType == Utils.NetworkType.MOBILE
return; ? 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) { defaultQualitySetting.save(defaultQuality);
case NONE -> {
Utils.showToastShort(str("revanced_remember_video_quality_none")); if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST.get()) {
return; 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 android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils 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" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE, GET_PLAYLIST_PAGE,
clientType clientType
) )
val requestBody = val requestBody =
PlayerRoutes.createApplicationRequestBody( createApplicationRequestBody(
clientType = clientType, clientType = clientType,
videoId = videoId, videoId = videoId,
playlistId = "RD$videoId" playlistId = "RD$videoId"
@ -168,12 +172,11 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_CATEGORY, GET_CATEGORY,
clientType clientType
) )
val requestBody = val requestBody = createWebInnertubeBody(clientType, videoId)
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)

View File

@ -115,7 +115,7 @@ public class ReturnYouTubeDislike {
private static final Rect middleSeparatorBounds; 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; public static final int leftSeparatorShapePaddingPixels;
private static final ShapeDrawable leftSeparatorShape; private static final ShapeDrawable leftSeparatorShape;
@ -131,7 +131,7 @@ public class ReturnYouTubeDislike {
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); 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 = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds); 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.Setting;
import app.revanced.extension.shared.settings.StringSetting; import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory; 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.DeArrowAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability; import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption; 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.player.MiniplayerPatch;
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType; import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior; 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.patches.utils.PatchStatus;
import app.revanced.extension.youtube.shared.PlaylistIdPrefix; import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
@ -148,7 +151,6 @@ public class Settings extends BaseSettings {
new ChangeStartPagePatch.ChangeStartPageTypeAvailability()); new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE); 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_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 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_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", 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 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 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 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)); 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_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 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 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); public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
// PreferenceScreen: General - Override buttons // 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_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); 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_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 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 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); 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 // 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 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 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)); 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_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_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 = 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_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 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); 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_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_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_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 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, 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, 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 // Experimental Flags
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true); 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_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 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_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_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 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)); 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); public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
// PreferenceScreen: Video // PreferenceScreen: Video - Codec
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);
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true); 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 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 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 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 = 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 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); 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 = 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 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 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 // PreferenceScreen: Miscellaneous
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); 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 = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400"); 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 = 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 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 = 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 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 = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684"); 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 = 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 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 = 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 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 = 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 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 = 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 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 = 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 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 = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF"); 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 // SB Setting not exported
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false); 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 { static {
// region Migration initialized // 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. // Categories were previously saved without a 'sb_' key prefix, so they need an additional adjustment.
Set<Setting<?>> sbCategories = new HashSet<>(Arrays.asList( Set<Setting<?>> sbCategories = new HashSet<>(Arrays.asList(
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR,

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference; package app.revanced.extension.youtube.settings.preference;
import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams; 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.showRestartDialog;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary; import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier; 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.isSDKAbove;
import static app.revanced.extension.shared.utils.Utils.showToastShort; 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;
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;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE; 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.PreferenceManager;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
import android.util.Pair;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
@ -55,12 +58,17 @@ import java.util.Set;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap; 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.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils; 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.shared.utils.Utils;
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch; 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.ExtendedUtils;
import app.revanced.extension.youtube.utils.ThemeUtils; import app.revanced.extension.youtube.utils.ThemeUtils;
@ -74,14 +82,19 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
@SuppressLint("SuspiciousIndentation") @SuppressLint("SuspiciousIndentation")
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try { try {
if (str == null) return; if (str == null) {
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str)); return;
}
if (setting == null) return; Setting<?> setting = Setting.getSettingFromPath(str);
if (setting == null) {
return;
}
Preference mPreference = findPreference(str); Preference mPreference = findPreference(str);
if (mPreference == null) {
if (mPreference == null) return; return;
}
if (mPreference instanceof SwitchPreference switchPreference) { if (mPreference instanceof SwitchPreference switchPreference) {
BooleanSetting boolSetting = (BooleanSetting) setting; BooleanSetting boolSetting = (BooleanSetting) setting;
@ -108,9 +121,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else { } else {
Setting.privateSetValueFromString(setting, listPreference.getValue()); Setting.privateSetValueFromString(setting, listPreference.getValue());
} }
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) { if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries()); listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues()); 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)) { if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting); updateListPreferenceSummary(listPreference, setting);
@ -122,18 +139,11 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
ReVancedSettingsPreference.initializeReVancedSettings(); ReVancedSettingsPreference.initializeReVancedSettings();
if (settingImportInProgress) { if (!settingImportInProgress && !showingUserDialogMessage) {
return;
}
if (!showingUserDialogMessage) {
final Context context = getActivity(); final Context context = getActivity();
if (setting.userDialogMessage != null if (setting.userDialogMessage != null && !prefIsSetToDefault(mPreference, setting)) {
&& mPreference instanceof SwitchPreference switchPreference showSettingUserDialogConfirmation(context, mPreference, setting);
&& setting.defaultValue instanceof Boolean defaultValue
&& switchPreference.isChecked() != defaultValue) {
showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
} else if (setting.rebootApp) { } else if (setting.rebootApp) {
showRestartDialog(context); 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(); Utils.verifyOnMainThread();
showingUserDialogMessage = true; final StringRef userDialogMessage = setting.userDialogMessage;
assert setting.userDialogMessage != null; if (context != null && userDialogMessage != null) {
new AlertDialog.Builder(context) showingUserDialogMessage = true;
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
.setMessage(setting.userDialogMessage.toString()) new AlertDialog.Builder(context)
.setPositiveButton(android.R.string.ok, (dialog, id) -> { .setTitle(str("revanced_extended_confirm_user_dialog_title"))
if (setting.rebootApp) { .setMessage(userDialogMessage.toString())
showRestartDialog(context); .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. })
}) .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
.setOnDismissListener(dialog -> showingUserDialogMessage = false) // Restore whatever the setting was before the change.
.setCancelable(false) if (setting instanceof BooleanSetting booleanSetting &&
.show(); 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; 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()) { for (PreferenceScreen mPreferenceScreen : preferenceScreenMap.values()) {
mPreferenceScreen.setOnPreferenceClickListener( mPreferenceScreen.setOnPreferenceClickListener(
preferenceScreen -> { preferenceScreen -> {
@ -205,11 +249,24 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
.findViewById(android.R.id.content) .findViewById(android.R.id.content)
.getParent(); .getParent();
// Fix required for Android 15 // Edge-to-edge is enforced if the following conditions are met:
if (isSDKAbove(35)) { // 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) -> { rootView.setOnApplyWindowInsetsListener((v, insets) -> {
Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars()); 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; return insets;
}); });
} }
@ -283,9 +340,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else if (preference instanceof EditTextPreference editTextPreference) { } else if (preference instanceof EditTextPreference editTextPreference) {
editTextPreference.setText(setting.get().toString()); editTextPreference.setText(setting.get().toString());
} else if (preference instanceof ListPreference listPreference) { } else if (preference instanceof ListPreference listPreference) {
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) { if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries()); listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues()); 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)) { if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting); updateListPreferenceSummary(listPreference, setting);
@ -298,6 +359,10 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity()); originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
copyPreferences(getPreferenceScreen(), originalPreferenceScreen); copyPreferences(getPreferenceScreen(), originalPreferenceScreen);
sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
sortPreferenceListMenu(Settings.SPOOF_STREAMING_DATA_LANGUAGE);
sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
} catch (Exception th) { } catch (Exception th) {
Logger.printException(() -> "Error during onCreate()", th); Logger.printException(() -> "Error during onCreate()", th);
} }
@ -312,9 +377,69 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
@Override @Override
public void onDestroy() { public void onDestroy() {
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
Utils.resetLocalizedContext();
super.onDestroy(); 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. * 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.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove; 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.Preference;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
@ -43,11 +42,11 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
enableDisablePreferences(); enableDisablePreferences();
AmbientModePreferenceLinks(); AmbientModePreferenceLinks();
ExternalDownloaderPreferenceLinks();
FullScreenPanelPreferenceLinks(); FullScreenPanelPreferenceLinks();
NavigationPreferenceLinks(); NavigationPreferenceLinks();
RYDPreferenceLinks(); RYDPreferenceLinks();
SeekBarPreferenceLinks(); SeekBarPreferenceLinks();
ShortsPreferenceLinks();
SpeedOverlayPreferenceLinks(); SpeedOverlayPreferenceLinks();
QuickActionsPreferenceLinks(); QuickActionsPreferenceLinks();
TabletLayoutLinks(); 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 * 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 * Enable/Disable Preference related to Speed overlay settings
*/ */

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference; package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str; 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.app.AlertDialog;
import android.content.Context; import android.content.Context;
@ -12,41 +13,51 @@ import android.text.InputType;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.GridLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView; import android.widget.TextView;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils; 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.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public class SegmentCategoryListPreference extends ListPreference { public class SegmentCategoryListPreference extends ListPreference {
private SegmentCategory mCategory; private SegmentCategory category;
private EditText mEditText; private TextView colorDotView;
private int mClickedDialogEntryIndex; private EditText colorEditText;
private EditText opacityEditText;
/**
* #RRGGBB
*/
private int categoryColor;
/**
* [0, 1]
*/
private float categoryOpacity;
private int selectedDialogEntryIndex;
private void init() { private void init() {
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey()); final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT; category = Objects.requireNonNull(segmentCategory);
mCategory = Objects.requireNonNull(segmentCategory);
// Edit: Using preferences to sync together multiple pieces // 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); setKey(segmentCategory.behaviorSetting.key);
setDefaultValue(segmentCategory.behaviorSetting.defaultValue); setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
setEntries(isHighlightCategory setEntries(isHighlightCategory
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce() ? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
: CategoryBehaviour.getBehaviorDescriptions()); : CategoryBehaviour.getBehaviorDescriptions());
setEntryValues(isHighlightCategory setEntryValues(isHighlightCategory
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce() ? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
: CategoryBehaviour.getBehaviorKeyValues()); : CategoryBehaviour.getBehaviorKeyValues());
updateTitle();
updateTitleFromCategory();
} }
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 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) { protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try { try {
Utils.setEditTextDialogTheme(builder); Utils.setEditTextDialogTheme(builder);
super.onPrepareDialogBuilder(builder);
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
Context context = builder.getContext(); Context context = builder.getContext();
TableLayout table = new TableLayout(context); GridLayout gridLayout = new GridLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL); gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout.
table.setPadding(70, 0, 150, 0); gridLayout.setColumnCount(3);
gridLayout.setRowCount(2);
TableRow row = new TableRow(context);
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); TextView colorTextLabel = new TextView(context);
colorTextLabel.setText(str("revanced_sb_color_dot_label")); colorTextLabel.setText(str("revanced_sb_color_dot_label"));
row.addView(colorTextLabel); colorTextLabel.setLayoutParams(gridParams);
gridLayout.addView(colorTextLabel);
TextView colorDotView = new TextView(context); gridParams = new GridLayout.LayoutParams();
colorDotView.setText(mCategory.getCategoryColorDot()); gridParams.rowSpec = GridLayout.spec(0); // First row.
colorDotView.setPadding(30, 0, 30, 0); gridParams.columnSpec = GridLayout.spec(1); // Second column.
row.addView(colorDotView); gridParams.setMargins(0, 0, 10, 0);
colorDotView = new TextView(context);
colorDotView.setLayoutParams(gridParams);
gridLayout.addView(colorDotView);
updateCategoryColorDot();
mEditText = new EditText(context); gridParams = new GridLayout.LayoutParams();
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); gridParams.rowSpec = GridLayout.spec(0); // First row.
mEditText.setText(mCategory.colorString()); gridParams.columnSpec = GridLayout.spec(2); // Third column.
mEditText.addTextChangedListener(new TextWatcher() { 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 @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { public void beforeTextChanged(CharSequence s, int start, int count, int after) {
} }
@ -104,44 +128,111 @@ public class SegmentCategoryListPreference extends ListPreference {
} }
@Override @Override
public void afterTextChanged(Editable s) { public void afterTextChanged(Editable edit) {
try { try {
String colorString = s.toString(); String colorString = edit.toString();
final int colorStringLength = colorString.length();
if (!colorString.startsWith("#")) { if (!colorString.startsWith("#")) {
s.insert(0, "#"); // recursively calls back into this method edit.insert(0, "#"); // Recursively calls back into this method.
return; return;
} }
if (colorString.length() > 7) {
s.delete(7, colorString.length()); final int maxColorStringLength = 7; // #RRGGBB
if (colorStringLength > maxColorStringLength) {
edit.delete(maxColorStringLength, colorStringLength);
return; return;
} }
final int color = Color.parseColor(colorString);
colorDotView.setText(SegmentCategory.getCategoryColorDot(color)); categoryColor = Color.parseColor(colorString);
updateCategoryColorDot();
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
// ignore // Ignore.
} }
} }
}); });
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f)); colorEditText.setLayoutParams(gridParams);
row.addView(mEditText); gridLayout.addView(colorEditText);
table.addView(row); gridParams = new GridLayout.LayoutParams();
builder.setView(table); gridParams.rowSpec = GridLayout.spec(1); // Second row.
builder.setTitle(mCategory.title.toString()); 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.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> { builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
try { try {
mCategory.resetColor(); category.resetColorAndOpacity();
updateTitle(); updateTitleFromCategory();
Utils.showToastShort(str("revanced_sb_color_reset")); Utils.showToastShort(str("revanced_sb_color_reset"));
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "setNeutralButton failure", ex); Logger.printException(() -> "setNeutralButton failure", ex);
} }
}); });
builder.setNegativeButton(android.R.string.cancel, null); 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) { } catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex); Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
} }
@ -150,31 +241,50 @@ public class SegmentCategoryListPreference extends ListPreference {
@Override @Override
protected void onDialogClosed(boolean positiveResult) { protected void onDialogClosed(boolean positiveResult) {
try { try {
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) { if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[mClickedDialogEntryIndex].toString(); String value = getEntryValues()[selectedDialogEntryIndex].toString();
if (callChangeListener(value)) { if (callChangeListener(value)) {
setValue(value); setValue(value);
mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value))); category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
SegmentCategory.updateEnabledCategories(); SegmentCategory.updateEnabledCategories();
} }
String colorString = mEditText.getText().toString();
try { try {
if (!colorString.equals(mCategory.colorString())) { String colorString = colorEditText.getText().toString();
mCategory.setColor(colorString); if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) {
category.setColor(colorString);
category.setOpacity(categoryOpacity);
Utils.showToastShort(str("revanced_sb_color_changed")); Utils.showToastShort(str("revanced_sb_color_changed"));
} }
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_sb_color_invalid")); Utils.showToastShort(str("revanced_sb_color_invalid"));
} }
updateTitle();
updateTitleFromCategory();
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex); Logger.printException(() -> "onDialogClosed failure", ex);
} }
} }
private void updateTitle() { private void applyOpacityToCategoryColor() {
setTitle(mCategory.getTitleWithColorDot()); categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
setEnabled(Settings.SB_ENABLED.get()); }
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 = new PreferenceCategory(mActivity);
statsCategory.setLayoutResource(preferencesCategoryLayout); statsCategory.setLayoutResource(preferencesCategoryLayout);
statsCategory.setTitle(str("revanced_sb_stats")); statsCategory.setTitle(str("revanced_sb_stats"));
statsCategory.setEnabled(Settings.SB_ENABLED.get());
mPreferenceScreen.addPreference(statsCategory); mPreferenceScreen.addPreference(statsCategory);
fetchAndDisplayStats(); fetchAndDisplayStats();
@ -261,7 +262,6 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
final String key = category.keyValue; final String key = category.keyValue;
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) { if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot()); segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
} }
} }
} catch (Exception ex) { } catch (Exception ex) {

View File

@ -58,6 +58,10 @@ public final class RootView {
return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get(); return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get();
} }
public static boolean isShortsActive() {
return ShortsPlayerState.getCurrent().isOpen();
}
/** /**
* Get current BrowseId. * Get current BrowseId.
* Rest of the implementation added by patch. * Rest of the implementation added by patch.

View File

@ -48,4 +48,12 @@ enum class ShortsPlayerState {
fun isClosed(): Boolean { fun isClosed(): Boolean {
return this == CLOSED 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) { for (SegmentCategory category : categories) {
JSONObject categoryObject = new JSONObject(); JSONObject categoryObject = new JSONObject();
String categoryKey = category.keyValue; String categoryKey = category.keyValue;
categoryObject.put("color", category.colorString()); categoryObject.put("color", category.getColorString());
barTypesObject.put(categoryKey, categoryObject); barTypesObject.put(categoryKey, categoryObject);
if (category.behaviour != CategoryBehaviour.IGNORE) { if (category.behaviour != CategoryBehaviour.IGNORE) {

View File

@ -6,7 +6,12 @@ import android.annotation.TargetApi;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; 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 android.widget.EditText;
import androidx.annotation.NonNull; 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. * Not thread safe. All fields/methods must be accessed from the main thread.
*
* @noinspection deprecation
*/ */
public class SponsorBlockUtils { 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 String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss";
private static final Pattern manualEditTimePattern private static final Pattern manualEditTimePattern
@ -162,28 +165,34 @@ public class SponsorBlockUtils {
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT) SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category ? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
: SegmentVote.values(); : 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]; SegmentVote voteOption = voteOptions[i];
String title = voteOption.title.toString(); CharSequence title = voteOption.title.toString();
if (Settings.SB_USER_IS_VIP.get() && segment.isLocked && voteOption.shouldHighlight) { if (userIsVip && segment.isLocked && voteOption.highlightIfVipAndVideoIsLocked) {
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title)); SpannableString coloredTitle = new SpannableString(title);
} else { coloredTitle.setSpan(new ForegroundColorSpan(LOCKED_COLOR),
items[i] = title; 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
title = coloredTitle;
} }
items[i] = title;
} }
new AlertDialog.Builder(context) new AlertDialog.Builder(context).setItems(items, (dialog1, which1) -> {
.setItems(items, (dialog1, which1) -> { SegmentVote voteOption = voteOptions[which1];
SegmentVote voteOption = voteOptions[which1]; switch (voteOption) {
switch (voteOption) { case UPVOTE:
case UPVOTE, DOWNVOTE -> case DOWNVOTE:
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption); SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
case CATEGORY_CHANGE -> onNewCategorySelect(segment, context); break;
} case CATEGORY_CHANGE:
}) onNewCategorySelect(segment, context);
.show(); break;
}
}).show();
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "segmentVoteClickListener failure", ex); Logger.printException(() -> "segmentVoteClickListener failure", ex);
} }
@ -287,22 +296,33 @@ public class SponsorBlockUtils {
if (segment.category == SegmentCategory.UNSUBMITTED) { if (segment.category == SegmentCategory.UNSUBMITTED) {
continue; continue;
} }
StringBuilder htmlBuilder = new StringBuilder();
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br>", SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
segment.category.color, segment.category.title));
htmlBuilder.append(formatSegmentTime(segment.start)); spannableBuilder.append(segment.category.getTitleWithColorDot());
if (segment.category != SegmentCategory.HIGHLIGHT) { spannableBuilder.append('\n');
htmlBuilder.append(" to ").append(formatSegmentTime(segment.end));
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 if (i + 1 != numberOfSegments) {
htmlBuilder.append("<br>"); // prevents trailing new line after last segment
titles[i] = Html.fromHtml(htmlBuilder.toString()); 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) new AlertDialog.Builder(context).setItems(titles, segmentVoteClickListener).show();
.setItems(titles, segmentVoteClickListener)
.show();
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onVotingClicked failure", 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.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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR; 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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR; 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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR; 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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR; 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;
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_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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR; 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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR; 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;
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_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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR; 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;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR; 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.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.text.Html; import android.text.Spannable;
import android.text.Spanned; import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -34,45 +45,46 @@ import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.StringSetting; import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringRef; import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "StaticFieldLeak"}) @SuppressWarnings("StaticFieldLeak")
public enum SegmentCategory { public enum SegmentCategory {
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"), 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"), 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"), 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. * 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"), 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"), 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_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"), 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"), 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"), 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_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"), 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"), 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"), 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"), 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 skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight"); private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
@ -111,12 +123,10 @@ public enum SegmentCategory {
mValuesMap.put(value.keyValue, value); mValuesMap.put(value.keyValue, value);
} }
@NonNull
public static SegmentCategory[] categoriesWithoutUnsubmitted() { public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted; return categoriesWithoutUnsubmitted;
} }
@NonNull
public static SegmentCategory[] categoriesWithoutHighlights() { public static SegmentCategory[] categoriesWithoutHighlights() {
return 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() { public static void updateEnabledCategories() {
Utils.verifyOnMainThread(); Utils.verifyOnMainThread();
@ -154,30 +164,32 @@ public enum SegmentCategory {
updateEnabledCategories(); updateEnabledCategories();
} }
@NonNull public static int applyOpacityToColor(int color, float opacity) {
public final String keyValue; if (opacity < 0 || opacity > 1.0f) {
@NonNull throw new IllegalArgumentException("Invalid opacity: " + opacity);
public final StringSetting behaviorSetting; }
@NonNull final int opacityInt = (int) (255 * opacity);
private final StringSetting colorSetting; 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; public final StringRef title;
/** /**
* Skip button text, if the skip occurs in the first quarter of the video * Skip button text, if the skip occurs in the first quarter of the video
*/ */
@NonNull
public final StringRef skipButtonTextBeginning; public final StringRef skipButtonTextBeginning;
/** /**
* Skip button text, if the skip occurs in the middle half of the video * Skip button text, if the skip occurs in the middle half of the video
*/ */
@NonNull
public final StringRef skipButtonTextMiddle; public final StringRef skipButtonTextMiddle;
/** /**
* Skip button text, if the skip occurs in the last quarter of the video * Skip button text, if the skip occurs in the last quarter of the video
*/ */
@NonNull
public final StringRef skipButtonTextEnd; public final StringRef skipButtonTextEnd;
/** /**
* Skipped segment toast, if the skip occurred in the first quarter of the video * Skipped segment toast, if the skip occurred in the first quarter of the video
@ -198,10 +210,7 @@ public enum SegmentCategory {
@NonNull @NonNull
public final Paint paint; public final Paint paint;
/** private int color;
* Value must be changed using {@link #setColor(String)}.
*/
public int color;
/** /**
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}. * Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
@ -213,17 +222,20 @@ public enum SegmentCategory {
SegmentCategory(String keyValue, StringRef title, SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonText, StringRef skipButtonText,
StringRef skippedToastText, StringRef skippedToastText,
StringSetting behavior, StringSetting color) { StringSetting behavior,
StringSetting color, FloatSetting opacity) {
this(keyValue, title, this(keyValue, title,
skipButtonText, skipButtonText, skipButtonText, skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText, skippedToastText, skippedToastText, skippedToastText,
behavior, color); behavior,
color, opacity);
} }
SegmentCategory(String keyValue, StringRef title, SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd, StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd, StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
StringSetting behavior, StringSetting color) { StringSetting behavior,
StringSetting color, FloatSetting opacity) {
this.keyValue = Objects.requireNonNull(keyValue); this.keyValue = Objects.requireNonNull(keyValue);
this.title = Objects.requireNonNull(title); this.title = Objects.requireNonNull(title);
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning); this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
@ -234,6 +246,7 @@ public enum SegmentCategory {
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd); this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviorSetting = Objects.requireNonNull(behavior); this.behaviorSetting = Objects.requireNonNull(behavior);
this.colorSetting = Objects.requireNonNull(color); this.colorSetting = Objects.requireNonNull(color);
this.opacitySetting = Objects.requireNonNull(opacity);
this.paint = new Paint(); this.paint = new Paint();
loadFromSettings(); loadFromSettings();
} }
@ -250,11 +263,14 @@ public enum SegmentCategory {
this.behaviour = savedBehavior; this.behaviour = savedBehavior;
String colorString = colorSetting.get(); String colorString = colorSetting.get();
final float opacity = opacitySetting.get();
try { try {
setColor(colorString); setColor(colorString);
setOpacity(opacity);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "Invalid color: " + colorString, ex); Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
colorSetting.resetToDefault(); colorSetting.resetToDefault();
opacitySetting.resetToDefault();
loadFromSettings(); loadFromSettings();
} }
} }
@ -264,45 +280,77 @@ public enum SegmentCategory {
this.behaviorSetting.save(behaviour.reVancedKeyValue); this.behaviorSetting.save(behaviour.reVancedKeyValue);
} }
/** private void updateColor() {
* @return HTML color format string color = applyOpacityToColor(color, opacitySetting.get());
*/
@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;
paint.setColor(color); 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); setColor(colorSetting.defaultValue);
setOpacity(opacitySetting.defaultValue);
} }
@NonNull /**
private static String getCategoryColorDotHTML(int color) { * @param colorString Segment color with #RRGGBB format.
color &= 0xFFFFFF; */
return String.format("<font color=\"#%06X\">⬤</font>", color); public void setColor(String colorString) throws IllegalArgumentException {
color = Color.parseColor(colorString);
colorSetting.save(colorString);
updateColor();
} }
@NonNull /**
public static Spanned getCategoryColorDot(int color) { * @return Integer color of #RRGGBB format.
return Html.fromHtml(getCategoryColorDotHTML(color)); */
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); return getCategoryColorDot(color);
} }
@NonNull public SpannableString getTitleWithColorDot(int categoryColor) {
public Spanned getTitleWithColorDot() { return getCategoryColorDotSpan(" " + title, categoryColor);
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title); }
public SpannableString getTitleWithColorDot() {
return getTitleWithColorDot(color);
} }
/** /**
@ -310,7 +358,6 @@ public enum SegmentCategory {
* @param videoLength length of the video * @param videoLength length of the video
* @return the skip button text * @return the skip button text
*/ */
@NonNull
StringRef getSkipButtonText(long segmentStartTime, long videoLength) { StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) { if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
return (this == SegmentCategory.HIGHLIGHT) return (this == SegmentCategory.HIGHLIGHT)
@ -319,7 +366,7 @@ public enum SegmentCategory {
} }
if (videoLength == 0) { 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; final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) { if (position < 0.25f) {
@ -335,10 +382,9 @@ public enum SegmentCategory {
* @param videoLength length of the video * @param videoLength length of the video
* @return 'skipped segment' toast message * @return 'skipped segment' toast message
*/ */
@NonNull
StringRef getSkippedToastText(long segmentStartTime, long videoLength) { StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) { 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; final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) { if (position < 0.25f) {

View File

@ -24,12 +24,15 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
@NonNull @NonNull
public final StringRef title; public final StringRef title;
public final int apiVoteType; 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.title = title;
this.apiVoteType = apiVoteType; 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 * get the background color for text on the overlay, as a color int
*/ */
val overlayTextBackgroundColor: 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 * get the foreground color for text on the overlay, as a color int
@ -133,6 +133,59 @@ class SwipeControlsConfigurationProvider(
val overlayForegroundColor: Int val overlayForegroundColor: Int
get() = Color.WHITE 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 // endregion
// region behaviour // region behaviour

View File

@ -24,7 +24,7 @@ import java.lang.ref.WeakReference
* The main controller for volume and brightness swipe controls. * The main controller for volume and brightness swipe controls.
* note that the superclass is overwritten to the superclass of the MainActivity at patch time * 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() { class SwipeControlsHostActivity : Activity() {
/** /**

View File

@ -1,14 +1,18 @@
package app.revanced.extension.youtube.swipecontrols.views package app.revanced.extension.youtube.swipecontrols.views
import android.annotation.SuppressLint
import android.content.Context 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.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import app.revanced.extension.shared.utils.ResourceUtils.ResourceType 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.SwipeControlsConfigurationProvider
import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay
import app.revanced.extension.youtube.swipecontrols.misc.applyDimension import app.revanced.extension.youtube.swipecontrols.misc.applyDimension
import kotlin.math.min
import kotlin.math.round import kotlin.math.round
/** /**
@ -33,36 +38,82 @@ class SwipeControlsOverlayLayout(
*/ */
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context)) constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
private val feedbackTextView: TextView
private val autoBrightnessIcon: Drawable 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 manualBrightnessIcon: Drawable
private val mutedVolumeIcon: Drawable private val mutedVolumeIcon: Drawable
private val lowVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_low")
private val normalVolumeIcon: Drawable 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 { private val circularProgressView: CircularProgressView = CircularProgressView(
return resources.getDrawable( 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), getIdentifier(name, ResourceType.DRAWABLE, context),
context.theme context.theme,
).apply { )
setTint(config.overlayForegroundColor)
setBounds( if (width != null && height != null) {
drawable.setTint(config.overlayForegroundColor)
drawable.setBounds(
0, 0,
0, 0,
width, width,
height, height,
) )
} else {
drawable.setTint(config.overlayTextColor)
} }
return drawable
} }
init { 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 // init views
val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
feedbackTextView = TextView(context).apply { feedbackTextView = TextView(context).apply {
layoutParams = LayoutParams( layoutParams = LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
).apply { ).apply {
addRule(CENTER_IN_PARENT, TRUE) addRule(CENTER_IN_PARENT, TRUE)
setPadding( setPadding(
@ -81,19 +132,36 @@ class SwipeControlsOverlayLayout(
compoundDrawablePadding = compoundIconPadding compoundDrawablePadding = compoundIconPadding
visibility = GONE visibility = GONE
} }
addView(feedbackTextView)
// get icons scaled, assuming square icons if (isAlternativeUI) {
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt() addView(circularProgressView)
autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight) addView(horizontalProgressView)
manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight) autoBrightnessIcon = getDrawable("revanced_ic_sc_brightness_auto")
normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight) 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 feedbackHideHandler = Handler(Looper.getMainLooper())
private val feedbackHideCallback = Runnable { 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) { override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
showFeedbackView( if (isAlternativeUI) {
"$newVolume", val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon, 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) { override fun onBrightnessChanged(brightness: Double) {
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) { if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
showFeedbackView( if (isAlternativeUI) {
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"), showFeedbackView(
autoBrightnessIcon, 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) { } 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 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 androidx.annotation.NonNull;
import java.util.Map;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.FloatSetting; import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.IntegerSetting; 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; import app.revanced.extension.youtube.settings.Settings;
public class ExtendedUtils extends PackageUtils { public class ExtendedUtils extends PackageUtils {
private static boolean isVersionOrGreater(String version) {
return getAppVersionName().compareTo(version) >= 0;
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final boolean IS_19_17_OR_GREATER = getAppVersionName().compareTo("19.17.00") >= 0; public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0; public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0; public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0; public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
public static final boolean IS_19_28_OR_GREATER = getAppVersionName().compareTo("19.28.00") >= 0; public static final boolean IS_19_28_OR_GREATER = isVersionOrGreater("19.28.00");
public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0; public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0; 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) { public static int validateValue(IntegerSetting settings, int min, int max, String message) {
int value = settings.get(); int value = settings.get();
@ -114,4 +139,88 @@ public class ExtendedUtils extends PackageUtils {
} }
return additionalSettingsEnabled; 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(); return builder.toString();
} }
private static String getVideoScheme(String videoId, boolean isShorts) { public static String getVideoScheme(String videoId, boolean isShorts) {
return String.format( return String.format(
Locale.ENGLISH, Locale.ENGLISH,
isShorts ? VIDEO_SCHEME_INTENT_FORMAT : VIDEO_SCHEME_LINK_FORMAT, isShorts ? VIDEO_SCHEME_INTENT_FORMAT : VIDEO_SCHEME_LINK_FORMAT,
@ -128,6 +128,22 @@ public class VideoUtils extends IntentUtils {
launchView(getChannelUrl(channelId), getContext().getPackageName()); 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() { public static void openVideo() {
openVideo(VideoInformation.getVideoId()); openVideo(VideoInformation.getVideoId());
} }
@ -177,8 +193,8 @@ public class VideoUtils extends IntentUtils {
} }
public static void showPlaybackSpeedDialog(@NonNull Context context) { public static void showPlaybackSpeedDialog(@NonNull Context context) {
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedListEntries(); final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedEntries();
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedListEntryValues(); final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedEntryValues();
final float playbackSpeed = VideoInformation.getPlaybackSpeed(); final float playbackSpeed = VideoInformation.getPlaybackSpeed();
final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed)); final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed));
@ -186,6 +202,7 @@ public class VideoUtils extends IntentUtils {
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> { .setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> {
final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f"); final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f");
VideoInformation.setPlaybackSpeed(selectedPlaybackSpeed);
VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed); VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed);
userSelectedPlaybackSpeed(selectedPlaybackSpeed); userSelectedPlaybackSpeed(selectedPlaybackSpeed);
mDialog.dismiss(); mDialog.dismiss();

View File

@ -1,7 +1,9 @@
package com.google.android.apps.youtube.app.settings.videoquality; package com.google.android.apps.youtube.app.settings.videoquality;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
@ -25,8 +27,8 @@ import app.revanced.extension.youtube.utils.ThemeUtils;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class VideoQualitySettingsActivity extends Activity { public class VideoQualitySettingsActivity extends Activity {
private static final String rvxSettingsLabel = ResourceUtils.getString("revanced_extended_settings_title"); private static String rvxSettingsLabel;
private static final String searchLabel = ResourceUtils.getString("revanced_extended_settings_search_title"); private static String searchLabel;
private static WeakReference<SearchView> searchViewRef = new WeakReference<>(null); private static WeakReference<SearchView> searchViewRef = new WeakReference<>(null);
private static WeakReference<ImageView> closeButtonRef = new WeakReference<>(null); private static WeakReference<ImageView> closeButtonRef = new WeakReference<>(null);
private ReVancedPreferenceFragment fragment; private ReVancedPreferenceFragment fragment;
@ -71,6 +73,10 @@ public class VideoQualitySettingsActivity extends Activity {
return; return;
} }
// Set label
rvxSettingsLabel = getString("revanced_extended_settings_title");
searchLabel = getString("revanced_extended_settings_search_title");
// Set toolbar // Set toolbar
setToolbar(); 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) { private void filterPreferences(String query) {
if (fragment == null) return; if (fragment == null) return;
fragment.filterPreferences(query); fragment.filterPreferences(query);

View File

@ -4,5 +4,5 @@ org.gradle.parallel = true
android.useAndroidX = true android.useAndroidX = true
kotlin.code.style = official kotlin.code.style = official
kotlin.jvm.target.validation.mode = IGNORE 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" gson = "2.12.1"
agp = "8.2.2" agp = "8.2.2"
annotation = "1.9.1" annotation = "1.9.1"
collections4 = "4.5.0-M3"
lang3 = "3.17.0" lang3 = "3.17.0"
preference = "1.2.1" preference = "1.2.1"
[libraries] [libraries]
gson = { module = "com.google.code.gson:gson", version.ref = "gson" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } 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" } lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" }
preference = { module = "androidx.preference:preference", version.ref = "preference" } 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 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 interface class app/revanced/patches/all/misc/transformation/IMethodCall {
public abstract fun getDefinedClassName ()Ljava/lang/String; public abstract fun getDefinedClassName ()Ljava/lang/String;
public abstract fun getMethodName ()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 getBottomSheetRecyclerView ()J
public static final fun getButtonContainer ()J public static final fun getButtonContainer ()J
public static final fun getButtonIconPaddingMedium ()J public static final fun getButtonIconPaddingMedium ()J
public static final fun getChannelHandle ()J
public static final fun getChipCloud ()J public static final fun getChipCloud ()J
public static final fun getColorGrey ()J public static final fun getColorGrey ()J
public static final fun getDarkBackground ()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 getPrivacyTosFooter ()J
public static final fun getQualityAuto ()J public static final fun getQualityAuto ()J
public static final fun getRemixGenericButtonSize ()J public static final fun getRemixGenericButtonSize ()J
public static final fun getSearchButton ()J
public static final fun getSlidingDialogAnimation ()J public static final fun getSlidingDialogAnimation ()J
public static final fun getTapBloomView ()J public static final fun getTapBloomView ()J
public static final fun getText1 ()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 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 final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt {
public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun is_2024_26_or_greater ()Z 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 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 getResourceId (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 (Ljava/lang/String;Ljava/lang/String;)J
public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; 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 { 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 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 final class app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatchKt {
public static final fun getShortsActionButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; 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 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 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 { 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 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 final class app/revanced/patches/youtube/utils/playservice/VersionCheckPatchKt {
public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch; public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun is_18_31_or_greater ()Z 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_39_or_greater ()Z
public static final fun is_18_42_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_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_02_or_greater ()Z
public static final fun is_19_04_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_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_15_or_greater ()Z
public static final fun is_19_16_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_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_23_or_greater ()Z
public static final fun is_19_25_or_greater ()Z public static final fun is_19_25_or_greater ()Z
public static final fun is_19_26_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_44_or_greater ()Z
public static final fun is_19_46_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_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_02_or_greater ()Z
public static final fun is_20_03_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_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_10_or_greater ()Z
public static final fun is_20_12_or_greater ()Z
} }
public final class app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatchKt { 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 getRelatedChipCloudMargin ()J
public static final fun getRightComment ()J public static final fun getRightComment ()J
public static final fun getScrimOverlay ()J public static final fun getScrimOverlay ()J
public static final fun getScrubbing ()J
public static final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J public static final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J
public static final fun getSeekUndoEduOverlayStub ()J public static final fun getSeekUndoEduOverlayStub ()J
public static final fun getSettingsFragment ()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 getTotalTime ()J
public static final fun getTouchArea ()J public static final fun getTouchArea ()J
public static final fun getVarispeedUnavailableTitle ()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 getVideoQualityBottomSheet ()J
public static final fun getVideoQualityUnavailableAnnouncement ()J public static final fun getVideoQualityUnavailableAnnouncement ()J
public static final fun getVideoZoomSnapIndicator ()J public static final fun getVideoZoomSnapIndicator ()J
public static final fun getVoiceSearch ()J public static final fun getVoiceSearch ()J
public static final fun getYouTubeControlsOverlaySubtitleButton ()J public static final fun getYouTubeControlsOverlaySubtitleButton ()J
public static final fun getYouTubeLogo ()J public static final fun getYouTubeLogo ()J
public static final fun getYtCallToAction ()J
public static final fun getYtFillBell ()J public static final fun getYtFillBell ()J
public static final fun getYtOutlineLibrary ()J
public static final fun getYtOutlineMoonZ ()J public static final fun getYtOutlineMoonZ ()J
public static final fun getYtOutlinePictureInPictureWhite ()J public static final fun getYtOutlinePictureInPictureWhite ()J
public static final fun getYtOutlineVideoCamera ()J public static final fun getYtOutlineVideoCamera ()J

View File

@ -152,7 +152,10 @@ private enum class MethodCall(
RegisterNetworkCallback1( RegisterNetworkCallback1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"registerNetworkCallback", "registerNetworkCallback",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;"
),
"V", "V",
), ),
RegisterNetworkCallback2( RegisterNetworkCallback2(
@ -174,13 +177,20 @@ private enum class MethodCall(
RequestNetwork1( RequestNetwork1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"requestNetwork", "requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;"
),
"V", "V",
), ),
RequestNetwork2( RequestNetwork2(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"requestNetwork", "requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"), arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;",
"I"
),
"V", "V",
), ),
RequestNetwork3( 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.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE 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.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR
import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS 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.resourceid.sharedResourceIdPatch
import app.revanced.patches.music.utils.settings.CategoryType import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction 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.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 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@Suppress("unused") @Suppress("unused")
@ -84,17 +88,50 @@ val accountComponentsPatch = bytecodePatch(
} }
// account switcher // account switcher
namesInactiveAccountThumbnailSizeFingerprint.matchOrThrow().let { val textViewField = with(
it.method.apply { channelHandleFingerprint
val targetIndex = it.patternMatch!!.startIndex .methodOrThrow(namesInactiveAccountThumbnailSizeFingerprint)
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA ) {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(channelHandle)
getInstruction(
indexOfFirstInstructionOrThrow(literalIndex) {
opcode == Opcode.IPUT_OBJECT &&
getReference<FieldReference>()?.type == "Landroid/widget/TextView;"
},
).getReference<FieldReference>()
}
addInstructions( namesInactiveAccountThumbnailSizeFingerprint.methodOrThrow().apply {
targetIndex, """ var hook = false
invoke-static {v$targetRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
move-result v$targetRegister 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 package app.revanced.patches.music.account.components
import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility 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.menuEntry
import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize
import app.revanced.patches.music.utils.resourceid.tosFooter import app.revanced.patches.music.utils.resourceid.tosFooter
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint( internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
name = "accountSwitcherAccessibilityLabelFingerprint", name = "accountSwitcherAccessibilityLabelFingerprint",
@ -14,6 +14,12 @@ internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
literals = listOf(accountSwitcherAccessibility) literals = listOf(accountSwitcherAccessibility)
) )
internal val channelHandleFingerprint = legacyFingerprint(
name = "channelHandleFingerprint",
returnType = "V",
literals = listOf(channelHandle),
)
internal val menuEntryFingerprint = legacyFingerprint( internal val menuEntryFingerprint = legacyFingerprint(
name = "menuEntryFingerprint", name = "menuEntryFingerprint",
returnType = "V", returnType = "V",
@ -24,19 +30,6 @@ internal val namesInactiveAccountThumbnailSizeFingerprint = legacyFingerprint(
name = "namesInactiveAccountThumbnailSizeFingerprint", name = "namesInactiveAccountThumbnailSizeFingerprint",
returnType = "V", returnType = "V",
parameters = listOf("L", "Ljava/lang/Object;"), 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) literals = listOf(namesInactiveAccountThumbnailSize)
) )

View File

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

View File

@ -97,8 +97,6 @@ internal val showDialogCommandFingerprint = legacyFingerprint(
name = "showDialogCommandFingerprint", name = "showDialogCommandFingerprint",
returnType = "V", returnType = "V",
opcodes = listOf( opcodes = listOf(
Opcode.IF_EQ,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL, Opcode.INVOKE_VIRTUAL,
Opcode.IGET, // get dialog code 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.musicTasteBuilderShelf
import app.revanced.patches.music.utils.resourceid.offlineSettingsMenuItem import app.revanced.patches.music.utils.resourceid.offlineSettingsMenuItem
import app.revanced.patches.music.utils.resourceid.playerOverlayChip 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.toolTipContentView
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
import app.revanced.util.fingerprint.legacyFingerprint 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( internal val searchBarFingerprint = legacyFingerprint(
name = "searchBarFingerprint", name = "searchBarFingerprint",
returnType = "V", 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.playservice.versionCheckPatch
import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf
import app.revanced.patches.music.utils.resourceid.playerOverlayChip 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.sharedResourceIdPatch
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
import app.revanced.patches.music.utils.settings.CategoryType import app.revanced.patches.music.utils.settings.CategoryType
@ -198,6 +199,23 @@ val layoutComponentsPatch = bytecodePatch(
// endregion // 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 // region patch for hide sound search button
if (is_6_48_or_greater) { if (is_6_48_or_greater) {
@ -353,6 +371,11 @@ val layoutComponentsPatch = bytecodePatch(
"revanced_hide_samples_shelf", "revanced_hide_samples_shelf",
"false" "false"
) )
addSwitchPreference(
CategoryType.GENERAL,
"revanced_hide_search_button",
"false"
)
if (is_6_48_or_greater) { if (is_6_48_or_greater) {
addSwitchPreference( addSwitchPreference(
CategoryType.GENERAL, CategoryType.GENERAL,

View File

@ -32,25 +32,32 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
) )
execute { execute {
if (!is_6_43_or_greater || is_7_25_or_greater) { if (!is_6_43_or_greater) {
return@execute return@execute
} }
if (is_7_17_or_greater) { var defaultVersionString = "6.42.55"
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
name == "SpoofAppVersionDefaultString" if (is_7_17_or_greater && !is_7_25_or_greater) {
}.replaceInstruction( defaultVersionString = "7.16.53"
0, defaultValue = "true"
"const-string v0, \"7.16.53\""
)
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) { findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
name == "SpoofAppVersionDefaultBoolean" name == "SpoofAppVersionDefaultBoolean"
}.replaceInstruction( }.replaceInstruction(
0, 0,
"const/4 v0, 0x1" "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( YOUTUBE_MUSIC_PACKAGE_NAME(
"6.51.53", "6.51.53",
"7.16.53", "7.16.53",
"7.25.53",
"8.05.51",
"8.10.52",
), ),
) )
@ -73,13 +83,19 @@ val spoofAppVersionPatch = resourcePatch(
) )
execute { execute {
if (!is_6_43_or_greater || is_7_25_or_greater) { if (!is_6_43_or_greater) {
printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.43.53 ~ 7.24.51.") printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.51.53 or later.")
return@execute 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") appendAppVersion("7.16.53")
} }
if (is_7_25_or_greater) {
appendAppVersion("7.17.52")
}
addSwitchPreference( addSwitchPreference(
CategoryType.GENERAL, CategoryType.GENERAL,

View File

@ -206,7 +206,8 @@ val changeHeaderPatch = resourcePatch(
printWarn(warnings) 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) { if (is_7_27_or_greater && isLegacyLogoExists) {
document("res/layout/signin_fragment.xml").use { document -> document("res/layout/signin_fragment.xml").use { document ->
document.doRecursively node@{ node -> document.doRecursively node@{ node ->

View File

@ -24,7 +24,7 @@ import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode 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 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val EXTENSION_METHOD_DESCRIPTOR = private const val EXTENSION_METHOD_DESCRIPTOR =
@ -41,7 +41,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51", "8.12.53",
), ),
) )
@ -57,7 +57,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
return@execute return@execute
} else if (!is_7_20_or_greater) { } else if (!is_7_20_or_greater) {
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall( cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
45635386L, CAIRO_SPLASH_ANIMATION_FEATURE_FLAG,
EXTENSION_METHOD_DESCRIPTOR EXTENSION_METHOD_DESCRIPTOR
) )
} else { } else {
@ -69,18 +69,13 @@ val cairoSplashAnimationPatch = bytecodePatch(
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "setContentView" getReference<MethodReference>()?.name == "setContentView"
} + 1 } + 1
val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) { val freeIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST)
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.name == "findViewById" &&
reference.definingClass != "Landroid/view/View;"
}
val freeRegister = val freeRegister =
getInstruction<FiveRegisterInstruction>(viewStubFindViewByIdIndex).registerD getInstruction<OneRegisterInstruction>(freeIndex).registerA
val jumpIndex = indexOfFirstInstructionReversedOrThrow( val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
viewStubFindViewByIdIndex, opcode == Opcode.INVOKE_VIRTUAL &&
Opcode.IGET_OBJECT getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
) } + 1
addInstructionsWithLabels( addInstructionsWithLabels(
insertIndex, """ 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.fingerprint.legacyFingerprint
import app.revanced.util.indexOfFirstLiteralInstruction import app.revanced.util.indexOfFirstLiteralInstruction
internal const val CAIRO_SPLASH_ANIMATION_FEATURE_FLAG = 45635386L
/** /**
* This fingerprint is compatible with YouTube Music v7.06.53+ * This fingerprint is compatible with YouTube Music v7.06.53+
*/ */
@ -20,7 +22,7 @@ internal val cairoSplashAnimationConfigFingerprint = legacyFingerprint(
if (is_7_20_or_greater) { if (is_7_20_or_greater) {
method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0 method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0
} else { } 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 package app.revanced.patches.music.misc.watchhistory
import app.revanced.patcher.patch.bytecodePatch 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.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.patch.PatchList.WATCH_HISTORY 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.CategoryType
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
@Suppress("unused") @Suppress("unused")
val watchHistoryPatch = bytecodePatch( val watchHistoryPatch = bytecodePatch(

View File

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

View File

@ -54,7 +54,7 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
parameters = emptyList(), parameters = emptyList(),
customFingerprint = { method, _ -> customFingerprint = { method, _ ->
AccessFlags.FINAL.isSet(method.accessFlags) && AccessFlags.FINAL.isSet(method.accessFlags) &&
method.containsLiteralInstruction(1) && method.containsLiteralInstruction(1) &&
method.indexOfFirstInstruction { method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "booleanValue" getReference<MethodReference>()?.name == "booleanValue"

View File

@ -746,15 +746,22 @@ val playerComponentsPatch = bytecodePatch(
val freeRegister = val freeRegister =
getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
val getFieldIndex = bottomSheetBehaviorIndex - 2
val getFieldReference =
getInstruction<ReferenceInstruction>(getFieldIndex).reference
val getFieldInstruction = getInstruction<TwoRegisterInstruction>(getFieldIndex)
addInstructionsWithLabels( addInstructionsWithLabels(
bottomSheetBehaviorIndex - 2, getFieldIndex + 1,
""" """
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z
move-result v$freeRegister move-result v$freeRegister
if-nez v$freeRegister, :dismiss if-nez v$freeRegister, :dismiss
iget-object v${getFieldInstruction.registerA}, v${getFieldInstruction.registerB}, $getFieldReference
""", """,
ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1)) ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
) )
removeInstruction(getFieldIndex)
} ?: throw PatchException("Could not find targetMethod") } ?: 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.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. "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.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 package app.revanced.patches.music.utils.extension
import app.revanced.patches.music.utils.extension.hooks.applicationInitHook 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 import app.revanced.patches.shared.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch( val sharedExtensionPatch = sharedExtensionPatch(
applicationInitHook, 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.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch 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.createPlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH 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.indexOfManufacturerInstruction
import app.revanced.patches.shared.indexOfModelInstruction import app.revanced.patches.shared.indexOfModelInstruction
import app.revanced.patches.shared.indexOfReleaseInstruction import app.revanced.patches.shared.indexOfReleaseInstruction
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.util.findMethodOrThrow import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.matchOrThrow
@ -63,9 +63,11 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
@Suppress("unused") @Suppress("unused")
val spoofClientPatch = bytecodePatch( val spoofClientPatch = bytecodePatch(
SPOOF_CLIENT.title, // Removed from the patch list to avoid user confusion:
SPOOF_CLIENT.summary, // https://github.com/inotia00/ReVanced_Extended/issues/2832#issuecomment-2745941171
false, // SPOOF_CLIENT.title,
// SPOOF_CLIENT.summary,
// false,
) { ) {
compatibleWith(COMPATIBLE_PACKAGE) 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.LAYOUT
import app.revanced.patches.shared.mapping.ResourceType.STRING import app.revanced.patches.shared.mapping.ResourceType.STRING
import app.revanced.patches.shared.mapping.ResourceType.STYLE 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.resourceMappingPatch
import app.revanced.patches.shared.mapping.resourceMappings
var accountSwitcherAccessibility = -1L var accountSwitcherAccessibility = -1L
private set private set
@ -25,6 +24,8 @@ var buttonContainer = -1L
private set private set
var buttonIconPaddingMedium = -1L var buttonIconPaddingMedium = -1L
private set private set
var channelHandle = -1L
private set
var chipCloud = -1L var chipCloud = -1L
private set private set
var colorGrey = -1L var colorGrey = -1L
@ -83,6 +84,8 @@ var qualityAuto = -1L
private set private set
var remixGenericButtonSize = -1L var remixGenericButtonSize = -1L
private set private set
var searchButton = -1L
private set
var slidingDialogAnimation = -1L var slidingDialogAnimation = -1L
private set private set
var tapBloomView = -1L var tapBloomView = -1L
@ -124,213 +127,61 @@ internal val sharedResourceIdPatch = resourcePatch(
dependsOn(resourceMappingPatch) dependsOn(resourceMappingPatch)
execute { execute {
accountSwitcherAccessibility = resourceMappings[ accountSwitcherAccessibility = getResourceId(STRING, "account_switcher_accessibility_label")
STRING, actionBarLogo = getResourceId(DRAWABLE, "action_bar_logo")
"account_switcher_accessibility_label", actionBarLogoRingo2 = getResourceId(DRAWABLE, "action_bar_logo_ringo2")
] bottomSheetRecyclerView = getResourceId(LAYOUT, "bottom_sheet_recycler_view")
actionBarLogo = resourceMappings[ buttonContainer = getResourceId(ID, "button_container")
DRAWABLE, buttonIconPaddingMedium = getResourceId(DIMEN, "button_icon_padding_medium")
"action_bar_logo", channelHandle = getResourceId(ID, "channel_handle")
] chipCloud = getResourceId(LAYOUT, "chip_cloud")
actionBarLogoRingo2 = resourceMappings[ colorGrey = getResourceId(COLOR, "ytm_color_grey_12")
DRAWABLE, darkBackground = getResourceId(ID, "dark_background")
"action_bar_logo_ringo2", designBottomSheetDialog = getResourceId(LAYOUT, "design_bottom_sheet_dialog")
] elementsContainer = getResourceId(ID, "elements_container")
bottomSheetRecyclerView = resourceMappings[ endButtonsContainer = getResourceId(ID, "end_buttons_container")
LAYOUT, floatingLayout = getResourceId(ID, "floating_layout")
"bottom_sheet_recycler_view" historyMenuItem = getResourceId(ID, "history_menu_item")
] inlineTimeBarAdBreakMarkerColor =
buttonContainer = resourceMappings[ getResourceId(COLOR, "inline_time_bar_ad_break_marker_color")
ID, inlineTimeBarProgressColor = getResourceId(COLOR, "inline_time_bar_progress_color")
"button_container" interstitialsContainer = getResourceId(ID, "interstitials_container")
] isTablet = getResourceId(BOOL, "is_tablet")
buttonIconPaddingMedium = resourceMappings[ likeDislikeContainer = getResourceId(ID, "like_dislike_container")
DIMEN, mainActivityLaunchAnimation = getResourceId(LAYOUT, "main_activity_launch_animation")
"button_icon_padding_medium" menuEntry = getResourceId(LAYOUT, "menu_entry")
] miniPlayerDefaultText = getResourceId(STRING, "mini_player_default_text")
chipCloud = resourceMappings[ miniPlayerMdxPlaying = getResourceId(STRING, "mini_player_mdx_playing")
LAYOUT, miniPlayerPlayPauseReplayButton = getResourceId(ID, "mini_player_play_pause_replay_button")
"chip_cloud" miniPlayerViewPager = getResourceId(ID, "mini_player_view_pager")
] modernDialogBackground = getResourceId(DRAWABLE, "modern_dialog_background")
colorGrey = resourceMappings[ musicNotifierShelf = getResourceId(LAYOUT, "music_notifier_shelf")
COLOR, musicTasteBuilderShelf = getResourceId(LAYOUT, "music_tastebuilder_shelf")
"ytm_color_grey_12" namesInactiveAccountThumbnailSize =
] getResourceId(DIMEN, "names_inactive_account_thumbnail_size")
darkBackground = resourceMappings[ offlineSettingsMenuItem = getResourceId(ID, "offline_settings_menu_item")
ID, playerOverlayChip = getResourceId(ID, "player_overlay_chip")
"dark_background" playerViewPager = getResourceId(ID, "player_view_pager")
] privacyTosFooter = getResourceId(ID, "privacy_tos_footer")
designBottomSheetDialog = resourceMappings[ qualityAuto = getResourceId(STRING, "quality_auto")
LAYOUT, remixGenericButtonSize = getResourceId(DIMEN, "remix_generic_button_size")
"design_bottom_sheet_dialog" searchButton = getResourceId(LAYOUT, "search_button")
] slidingDialogAnimation = getResourceId(STYLE, "SlidingDialogAnimation")
elementsContainer = resourceMappings[ tapBloomView = getResourceId(ID, "tap_bloom_view")
ID, text1 = getResourceId(ID, "text1")
"elements_container" toolTipContentView = getResourceId(LAYOUT, "tooltip_content_view")
] topEnd = getResourceId(ID, "TOP_END")
endButtonsContainer = resourceMappings[ topStart = getResourceId(ID, "TOP_START")
ID, topBarMenuItemImageView = getResourceId(ID, "top_bar_menu_item_image_view")
"end_buttons_container" tosFooter = getResourceId(ID, "tos_footer")
] touchOutside = getResourceId(ID, "touch_outside")
floatingLayout = resourceMappings[ trimSilenceSwitch = getResourceId(ID, "trim_silence_switch")
ID, varispeedUnavailableTitle = getResourceId(STRING, "varispeed_unavailable_title")
"floating_layout" ytFillSamples = getResourceId(DRAWABLE, "yt_fill_samples_vd_theme_24")
] ytFillYouTubeMusic = getResourceId(DRAWABLE, "yt_fill_youtube_music_vd_theme_24")
historyMenuItem = resourceMappings[ ytOutlineSamples = getResourceId(DRAWABLE, "yt_outline_samples_vd_theme_24")
ID, ytOutlineYouTubeMusic = getResourceId(DRAWABLE, "yt_outline_youtube_music_vd_theme_24")
"history_menu_item" ytmLogo = getResourceId(DRAWABLE, "ytm_logo")
] ytmLogoRingo2 = getResourceId(DRAWABLE, "ytm_logo_ringo2")
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",
]
} }
} }

View File

@ -25,7 +25,8 @@ val videoTypeHookPatch = bytecodePatch(
videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply { videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply {
val getEnumIndex = indexOfGetEnumInstruction(this) 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) { val referenceIndex = indexOfFirstInstructionOrThrow(getEnumIndex) {
opcode == Opcode.SGET_OBJECT && opcode == Opcode.SGET_OBJECT &&
getReference<FieldReference>()?.type == enumClass getReference<FieldReference>()?.type == enumClass

View File

@ -71,7 +71,8 @@ val playerResponseMethodHookPatch = bytecodePatch(
val beforeVideoIdHooks = val beforeVideoIdHooks =
hooks.filterIsInstance<Hook.PlayerParameterBeforeVideoId>().asReversed() hooks.filterIsInstance<Hook.PlayerParameterBeforeVideoId>().asReversed()
val videoIdHooks = hooks.filterIsInstance<Hook.VideoId>().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() val afterVideoIdHooks = hooks.filterIsInstance<Hook.PlayerParameter>().asReversed()
// Add the hooks in this specific order as they insert instructions at the beginning of the method. // 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 = private const val EXTENSION_CLASS_DESCRIPTOR =
"$PATCHES_PATH/GeneralAdsPatch;" "$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") @Suppress("unused")
val adsPatch = bytecodePatch( val adsPatch = bytecodePatch(
HIDE_ADS.title, HIDE_ADS.title,
@ -94,11 +86,20 @@ val adsPatch = bytecodePatch(
if (is_2025_06_or_greater) { if (is_2025_06_or_greater) {
listOf( listOf(
commentAdCommentScreenAdViewFingerprint, commentAdCommentScreenAdViewFingerprint,
commentAdDetailListHeaderViewFingerprint commentAdDetailListHeaderViewFingerprint,
commentsViewModelFingerprint
).forEach { fingerprint -> ).forEach { fingerprint ->
fingerprint.methodOrThrow().hook() fingerprint.methodOrThrow().hook()
} }
} else { } 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 -> classes.forEach { classDef ->
classDef.methods.forEach { method -> classDef.methods.forEach { method ->
if (method.isCommentAdsMethod()) { if (method.isCommentAdsMethod()) {

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