Compare commits

...

105 Commits

Author SHA1 Message Date
inotia00
42cc8201f5 Merge branch 'dev' into revanced-extended 2025-04-03 10:23:04 +09:00
inotia00
15d0fafcf8 bump 5.6.2 2025-04-03 10:20:34 +09:00
inotia00
db10e410d3 feat(Translations): Update translation 2025-04-03 10:20:10 +09:00
inotia00
8f23d76f37 feat(Reddit): Change the latest supported version from 2025.12.0 to 2025.12.1 2025-04-03 10:19:36 +09:00
inotia00
7168e49121 fix(Reddit - Disable screenshot popup): No exception is thrown if no matching pattern is found 2025-04-03 10:18:02 +09:00
inotia00
2b52380294 bump 5.6.2-dev.2 2025-04-01 19:12:45 +09:00
inotia00
0cc693961a feat(Translations): Update translation 2025-04-01 19:11:59 +09:00
inotia00
bbf863c630 fix(YouTube - Description components): Expand video descriptions requires longer wait time 2025-04-01 19:11:45 +09:00
inotia00
783e366242 fix(YouTube - Hook download actions, Overlay buttons): Auth key is set before the Context is initialized 2025-04-01 19:09:19 +09:00
inotia00
4a19a960c5 fix(YouTube - Settings): The default value for the patch option is incorrect 2025-04-01 19:06:28 +09:00
inotia00
e55fd4eb74 chore: Remove debug instruction 2025-04-01 19:05:20 +09:00
inotia00
64af7fd8b6 fix(YouTube - Video playback): Overridden to default playback speed in onResume callback https://github.com/inotia00/ReVanced_Extended/issues/2896 2025-04-01 19:02:55 +09:00
inotia00
457dbfec6c feat(YouTube - Disable resuming Shorts on startup): Match with ReVanced 2025-04-01 19:01:18 +09:00
inotia00
22b98336d5 fix(YouTube - Custom branding icon): Splash animation background color is always white https://github.com/inotia00/ReVanced_Extended/issues/2892 2025-04-01 19:00:18 +09:00
inotia00
7e4a71b385 feat(GmsCore support): Add patch option Patch all manifest components 2025-04-01 18:59:47 +09:00
inotia00
bb5964ce98 fix(Reddit - Disable screenshot popup): Screenshot popup not being completely removed https://github.com/inotia00/ReVanced_Extended/issues/1810 2025-04-01 18:51:58 +09:00
inotia00
15db05c636 fix(Reddit - Remove subreddit dialog): Can't open post from search results 2025-04-01 18:49:47 +09:00
KobeW50
3f9edca15d
feat(YouTube - Settings): Place RVX settings at top of settings by default (#147)
Co-authored-by: Kobe <kobew5050@gmail.com>
2025-04-01 08:50:25 +09:00
inotia00
1eca8c854c bump 5.6.2-dev.1 2025-03-31 21:21:14 +09:00
inotia00
692b4f2c53 feat(Translations): Update translation 2025-03-31 21:19:33 +09:00
inotia00
5162dccecc feat(YouTube - Settings): Add App name, App version and Patched date to patch information 2025-03-31 21:16:29 +09:00
inotia00
56b713b0db fix build error 2025-03-31 21:13:36 +09:00
inotia00
f761bc45ec feat(YouTube - Hook download actions, Overlay buttons): Add Add to queue and reload video and Remove from queue and reload video settings, fixed a bug where the queue was unavailable under certain conditions 2025-03-31 21:12:59 +09:00
inotia00
bccd6dc5df fix(YouTube - Swipe controls): Patch fails on YouTube 19.43.41 when Player components patch is included 2025-03-31 21:09:11 +09:00
inotia00
09a8eb7114 fix(YouTube - Custom branding icon): Splash animation background color is always white https://github.com/inotia00/ReVanced_Extended/issues/2892 2025-03-31 21:05:06 +09:00
inotia00
b439ef3ee7 fix(Reddit - Remove subreddit dialog): New type of NSFW dialog not hidden https://github.com/inotia00/ReVanced_Extended/issues/2895 2025-03-31 21:01:10 +09:00
inotia00
7f85e802c2 fix(Reddit - Disable screenshot popup): Patch does not work in certain environments https://github.com/inotia00/ReVanced_Extended/issues/2891 2025-03-31 20:54:10 +09:00
inotia00
bb1498df76 Merge branch 'dev' into revanced-extended 2025-03-30 19:49:58 +09:00
inotia00
1c58c6a36e feat(YouTube): Remove support version 20.03.43
Not reflected in branch due to issue with source comparison tool
2025-03-30 19:49:43 +09:00
inotia00
26bf2f4b82 Merge branch 'dev' into revanced-extended 2025-03-30 19:33:08 +09:00
inotia00
a037b25c13 bump 5.6.1 2025-03-30 19:32:31 +09:00
inotia00
b72bb71e30 feat(YouTube): Remove support version 20.03.43 2025-03-30 19:27:16 +09:00
inotia00
28ff781786 chore: Lint code 2025-03-30 19:13:14 +09:00
inotia00
f249e88ce8 bump 5.6.1-dev.5 2025-03-30 18:24:50 +09:00
inotia00
036e3dad11 feat(Translations): Update translation 2025-03-30 18:24:10 +09:00
inotia00
9342e1f7d2 fix(Reddit - Disable screenshot popup): Restart dialog is missing 2025-03-30 18:23:49 +09:00
inotia00
f19dd54026 fix(YouTube - Hook download actions, Overlay buttons): Queue manager fails to identify brand account 2025-03-30 18:22:29 +09:00
inotia00
edccd61e6b fix(YouTube - Custom branding icon): Remove unnecessary hooks 2025-03-30 18:20:02 +09:00
inotia00
35e6c26823 fix(Extensions): Remove unnecessary Context hooks 2025-03-30 18:18:46 +09:00
inotia00
88d59d05b9 fix(YouTube - Shorts components): Custom actions do not override Shorts flyout menu in YouTube 19.05.36 2025-03-30 18:16:34 +09:00
inotia00
aac38dc8af fix(YouTube - Settings): RVX language no longer changes the YouTube app language 2025-03-30 18:12:07 +09:00
inotia00
1dd7eda606 chore(YouTube): Reflecting the changes in ReVanced 2025-03-30 18:08:49 +09:00
MondayNitro
05195caa5a
feat(YouTube - Custom branding icon): Update old splash animation background color (#146)
* Update revanced_extended_settings_key_icon.xml

* Update $avd_anim__0.xml

* Update strings.xml
2025-03-30 18:05:53 +09:00
inotia00
82158deaf2 chore(YouTube - Fullscreen components): Remove warning for Keep landscape mode setting 2025-03-30 18:01:01 +09:00
inotia00
21be41c2a9 fix(Reddit - Disable screenshot popup): Screenshot popup not being completely removed https://github.com/inotia00/ReVanced_Extended/issues/1810 2025-03-30 17:56:59 +09:00
inotia00
56d2f7c4ad bump 5.6.1-dev.4 2025-03-29 16:58:52 +09:00
inotia00
aca0591575 feat(Translations): Update translation 2025-03-29 16:58:04 +09:00
inotia00
274e10aabc fix: add missing dependency 2025-03-29 16:57:21 +09:00
inotia00
4330b7f6df fix(Reddit - Hide ads): Hide comment ads does not work https://github.com/inotia00/ReVanced_Extended/issues/2884 2025-03-29 16:56:21 +09:00
inotia00
4feff6b150 feat(Reddit): Restore support version 2025.05.1 2025-03-29 16:55:41 +09:00
inotia00
fbf19ee78b feat(YouTube): Change the latest supported version from 20.03.45 to 20.03.43 https://github.com/inotia00/ReVanced_Extended/issues/2885#issuecomment-2763077766 2025-03-29 16:54:24 +09:00
inotia00
e157e9447d fix(YouTube): Playback speed sometimes changes to 1.0x in Shorts (Unpatched YouTube bug) 2025-03-29 16:52:45 +09:00
inotia00
79f933dad4 fix(YouTube - Remove background playback restrictions): Media controls appear in the status bar when playing Shorts from the feed 2025-03-29 16:51:35 +09:00
inotia00
b8f3917b55 chore(YouTube): Integrate methods related to Shorts state into RootView 2025-03-29 16:46:28 +09:00
inotia00
758e8ac568 fix(YouTube - Video playback): Update descriptions 2025-03-28 20:29:24 +09:00
inotia00
b8b61fdf51 fix typo 2025-03-28 20:28:07 +09:00
inotia00
34e482b03e fix(YouTube - Video playback): Update descriptions 2025-03-28 20:26:12 +09:00
inotia00
481a310d06 fix typo 2025-03-28 20:25:24 +09:00
inotia00
6818df4507 bump 5.6.1-dev.3 2025-03-28 19:48:38 +09:00
inotia00
af26cd58a8 feat(Translations): Update translation 2025-03-28 19:48:00 +09:00
inotia00
3e8c748f48 feat(YouTube): Add support version 20.03.45 https://github.com/inotia00/ReVanced_Extended/issues/2717#issuecomment-2760796211 2025-03-28 19:47:08 +09:00
inotia00
f0b1155d20 fix(YouTube - Change form factor): No user dialog shown when changing settings 2025-03-28 19:36:39 +09:00
inotia00
bf9ba0b1ef refactor(YouTube - Video playback): Add support for Shorts..
- Add 'Speed dialog' setting to Shorts custom actions
- Add default quality settings and default playback speed settings for Shorts
- Remove deprecated settings - 'Reject software AV1 codec response', 'Enable Shorts default playback speed'
- Reorder video categories
2025-03-28 19:31:25 +09:00
inotia00
82ceb8aa76 fix(YouTube Music - Disable music video in album): The redirect wait time may be too short. 2025-03-28 19:21:17 +09:00
inotia00
4f911d9a55 fix(YouTube - Spoof streaming data): No toast message is shown even if fetch fails 2025-03-28 19:16:21 +09:00
inotia00
a235c84e19 feat(Reddit): Add support version 2025.12.0, drop support version 2025.05.1 2025-03-28 19:14:17 +09:00
inotia00
98d362ad93 feat(YouTube Music): Add support version 8.12.53, drop support version 8.10.52 2025-03-28 19:12:18 +09:00
inotia00
8169ccacc2 refactor: Use map instead of list to lookup resource ids 2025-03-28 19:09:31 +09:00
inotia00
f3abc04812 bump 5.6.1-dev.2 2025-03-26 21:33:40 +09:00
inotia00
5446847f5f feat(Translations): Update translation 2025-03-26 21:33:05 +09:00
inotia00
29ba8f7a7d feat(YouTube Music - Spoof app version): Add target version 7.17.52 2025-03-26 21:27:39 +09:00
inotia00
0639b559b1 fix(YouTube - Spoof streaming data): Add patch option Use iOS client and add user dialog for warning 2025-03-26 21:03:35 +09:00
inotia00
dcb72cc803 feat(YouTube Music - Custom branding icon): Update afn icons https://github.com/inotia00/ReVanced_Extended/issues/2866 2025-03-26 20:45:45 +09:00
inotia00
d4ad05d4ba fix(YouTube - Theme): Change method to fix dark theme in YouTube 19.32+ 2025-03-26 20:34:30 +09:00
inotia00
451a14a74d feat(YouTube - Swipe controls): Add Swipe overlay alternative UI setting (Closes https://github.com/inotia00/ReVanced_Extended/issues/2828) 2025-03-26 18:58:02 +09:00
inotia00
99fa969857 fix(YouTube - SponsorBlock): Dependencies for some settings are not set 2025-03-26 15:24:58 +09:00
inotia00
efead108f9 feat(YouTube - SponsorBlock): Add opacity setting to category segment colors 2025-03-26 15:11:06 +09:00
inotia00
9d37e31a24 chore: Lint code 2025-03-26 15:08:28 +09:00
inotia00
a764e3aea3 fix(YouTube - Shorts components): Pause option of Change Shorts repeat state not working (20.09+) 2025-03-26 15:05:39 +09:00
inotia00
0bfabbc384 fix(YouTube - Shorts components): Shorts player automatically goes to next Short (Closes https://github.com/inotia00/ReVanced_Extended/issues/2873) 2025-03-26 15:03:55 +09:00
inotia00
cf90f7b94e fix(YouTube - Hook download actions, Overlay buttons): Sometimes the AlertDialog does not show 2025-03-26 15:02:02 +09:00
inotia00
df41f035b6 fix(YouTube - Theme): Change method to fix dark theme in YouTube 19.32+ 2025-03-26 15:00:26 +09:00
inotia00
5c743c299a feat(YouTube Music): Change the latest supported version from 8.10.51 to 8.10.52 2025-03-26 14:57:32 +09:00
inotia00
6ac6fcf953 feat(YouTube - Hide layout components): Change default value of Disable translucent status bar setting and move it to experimental flag 2025-03-26 14:55:48 +09:00
inotia00
123082b676 refactor(InnterTube): Move classes to the appropriate path 2025-03-26 12:31:03 +09:00
inotia00
ae69aca189 Merge branch 'dev' into revanced-extended 2025-03-16 13:07:53 +09:00
inotia00
8d84304737 Merge branch 'dev' into revanced-extended 2025-03-07 09:50:18 +09:00
inotia00
1926becdfc Merge branch 'dev' into revanced-extended 2025-02-13 11:21:24 +09:00
inotia00
9146d9283b Merge branch 'dev' into revanced-extended 2025-01-22 17:23:55 +09:00
inotia00
8db38af44f Merge branch 'dev' into revanced-extended 2025-01-16 12:43:34 +09:00
inotia00
1aaf03df50 Merge branch 'dev' into revanced-extended 2025-01-07 14:16:07 +09:00
inotia00
ee0a7b5443 Merge branch 'dev' into revanced-extended 2024-12-22 19:46:21 +09:00
inotia00
a9408b0f51 Merge branch 'dev' into revanced-extended 2024-12-22 18:25:46 +09:00
inotia00
03c0826fab Merge branch 'dev' into revanced-extended 2024-12-22 16:54:00 +09:00
inotia00
0384907324 Merge branch 'dev' into revanced-extended 2024-12-21 15:10:47 +09:00
inotia00
2b0f12cb9e Merge branch 'dev' into revanced-extended 2024-12-09 22:34:28 +09:00
inotia00
5088daa434 Merge branch 'dev' into revanced-extended 2024-12-09 01:40:58 +09:00
inotia00
b5fe136898 Merge branch 'dev' into revanced-extended 2024-12-08 18:48:19 +09:00
inotia00
807494f8a7 Merge branch 'dev' into revanced-extended 2024-11-08 22:42:17 +09:00
inotia00
d8ae741853 Merge branch 'dev' into revanced-extended 2024-10-21 20:29:13 +09:00
inotia00
934c33a0f9 Merge branch 'dev' into revanced-extended 2024-10-13 02:34:39 +09:00
inotia00
b725e54aee Merge branch 'dev' into revanced-extended 2024-09-29 00:26:24 +09:00
inotia00
c57de97576 Merge branch 'dev' into revanced-extended 2024-09-07 09:32:24 +09:00
inotia00
2e3274152b Merge branch 'dev' into revanced-extended 2024-08-12 12:02:15 +09:00
KobeW50
6499b7d3f0
ci: workflow to ping Discord users when patches are released (#72)
* init: Workflow to notify discord users of releases

* Rename workflow

* chore (Background playback): Shorten description

* Revert "chore (Background playback): Shorten description"

This reverts commit 10661b870f0c9c670c5d522f9b2ca7cc82d32772.

* Change message contents
2024-08-11 13:11:10 +09:00
287 changed files with 8453 additions and 4248 deletions

115
README.md
View File

@ -85,48 +85,48 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.10.51 |
| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.10.51 |
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.10.51 |
| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.10.51 |
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.10.51 |
| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.10.51 |
| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.10.51 |
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.10.51 |
| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.10.51 |
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.10.51 |
| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.10.51 |
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.10.51 |
| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.10.51 |
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.10.51 |
| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.10.51 |
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.10.51 |
| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.10.51 |
| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.10.51 |
| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.10.51 |
| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.10.51 |
| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.10.51 |
| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.10.51 |
| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.10.51 |
| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.10.51 |
| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.10.51 |
| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.10.51 |
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.10.51 |
| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.10.51 |
| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.10.51 |
| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.10.51 |
| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.10.51 |
| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.10.51 |
| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.10.51 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.10.51 |
| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.10.51 |
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.10.51 |
| `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 7.16.53 |
| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.10.51 |
| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.10.51 |
| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.10.51 |
| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.10.51 |
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.10.51 |
| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.12.53 |
| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.12.53 |
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.12.53 |
| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.12.53 |
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.12.53 |
| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.12.53 |
| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.12.53 |
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.12.53 |
| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.12.53 |
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.12.53 |
| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.12.53 |
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.12.53 |
| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.12.53 |
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.12.53 |
| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.12.53 |
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.12.53 |
| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.12.53 |
| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.12.53 |
| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.12.53 |
| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.12.53 |
| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.12.53 |
| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.12.53 |
| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.12.53 |
| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.12.53 |
| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.12.53 |
| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.12.53 |
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.12.53 |
| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.12.53 |
| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.12.53 |
| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.12.53 |
| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.12.53 |
| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.12.53 |
| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.12.53 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.12.53 |
| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.12.53 |
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.12.53 |
| `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 8.10.52 |
| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.12.53 |
| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.12.53 |
| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.12.53 |
| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.12.53 |
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.12.53 |
</details>
### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage)
@ -134,19 +134,19 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 |
| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 |
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.05.1 |
| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.05.1 |
| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.05.1 |
| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.05.1 |
| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.05.1 |
| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.05.1 |
| `Open links externally` | Adds an option to always open links in your browser instead of in the in-app-browser. | 2024.17.0 ~ 2025.05.1 |
| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.05.1 |
| `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | 2024.17.0 ~ 2025.05.1 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.05.1 |
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.05.1 |
| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.12.1 |
| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.12.1 |
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.12.1 |
| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.12.1 |
| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.12.1 |
| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.12.1 |
| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.12.1 |
| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.12.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.1 |
| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.12.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.1 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.12.1 |
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.12.1 |
</details>
@ -187,7 +187,7 @@ Example:
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -199,7 +199,8 @@ Example:
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []

View File

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

View File

@ -7,15 +7,12 @@ import androidx.annotation.NonNull;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.music.utils.VideoUtils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class AlbumMusicVideoPatch {
@ -40,7 +37,7 @@ public class AlbumMusicVideoPatch {
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false);
private static volatile boolean isVideoLaunched = false;
@NonNull
private static volatile String playerResponseVideoId = "";
@ -100,14 +97,6 @@ public class AlbumMusicVideoPatch {
if (request == null) {
return;
}
// This hook is always called off the main thread,
// but this can later be called for the same video id from the main thread.
// This is not a concern, since the fetch will always be finished
// and never block the main thread.
// But if debugging, then still verify this is the situation.
if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
Logger.printException(() -> "Error: Blocking main thread");
}
String songId = request.getStream();
if (songId.isEmpty()) {
Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
@ -149,17 +138,16 @@ public class AlbumMusicVideoPatch {
private static void openMusic(@NonNull String songId) {
try {
isVideoLaunched.compareAndSet(false, true);
// The newly opened video is not a music video.
// To prevent fetch requests from being sent, set the video id to the newly opened video
VideoUtils.runOnMainThreadDelayed(() -> {
isVideoLaunched = true;
playerResponseVideoId = songId;
currentVideoId = songId;
VideoUtils.openInYouTubeMusic(songId);
}, 1000);
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched = false, 3000);
}, 1500);
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 2500);
} catch (Exception ex) {
Logger.printException(() -> "openMusic failure", ex);
}
@ -191,7 +179,7 @@ public class AlbumMusicVideoPatch {
* Injection point.
*/
public static boolean hideSnackBar() {
return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched.get();
return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched;
}
}

View File

@ -9,7 +9,6 @@ import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

View File

@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.misc.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.settings.AppLanguage
import app.revanced.extension.shared.utils.Logger
@ -136,10 +138,11 @@ class PlaylistRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_PLAYLIST_PAGE,
clientType
)
/**
* For some reason, the tracks in Top Songs have the playlistId of the album:
* [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835)
@ -152,7 +155,7 @@ class PlaylistRequest private constructor(
* So we can work around this by setting the language to English when sending the request.
*/
val requestBody =
PlayerRoutes.createApplicationRequestBody(
createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId,

View File

@ -2,7 +2,10 @@ package app.revanced.extension.reddit.patches;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.Dialog;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import androidx.annotation.NonNull;
@ -34,6 +37,35 @@ public class RemoveSubRedditDialogPatch {
return Settings.REMOVE_NSFW_DIALOG.get() || hasBeenVisited;
}
public static void dismissNSFWDialog(Object customDialog) {
if (Settings.REMOVE_NSFW_DIALOG.get() &&
customDialog instanceof Dialog dialog) {
Window window = dialog.getWindow();
if (window != null) {
WindowManager.LayoutParams params = window.getAttributes();
params.height = 0;
params.width = 0;
// Change the size of dialog to 0.
window.setAttributes(params);
// Disable dialog's background dim.
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
// Hide DecorView.
View decorView = window.getDecorView();
decorView.setVisibility(View.GONE);
// Dismiss dialog.
dialog.dismiss();
}
}
}
public static boolean removeNSFWDialog() {
return Settings.REMOVE_NSFW_DIALOG.get();
}
public static boolean spoofLoggedInStatus(boolean isLoggedIn) {
return !Settings.REMOVE_NOTIFICATION_DIALOG.get() && isLoggedIn;
}

View File

@ -5,7 +5,7 @@ import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused")
public class ScreenshotPopupPatch {
public static boolean disableScreenshotPopup() {
return Settings.DISABLE_SCREENSHOT_POPUP.get();
public static Boolean disableScreenshotPopup(Boolean original) {
return Settings.DISABLE_SCREENSHOT_POPUP.get() ? Boolean.FALSE : original;
}
}

View File

@ -13,7 +13,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true);
// Layout
public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE);
public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE, true);
public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true);
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true);
public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true);

View File

@ -1,6 +1,7 @@
package app.revanced.extension.shared.patches.client
package app.revanced.extension.shared.innertube.client
import android.os.Build
import app.revanced.extension.shared.patches.PatchStatus
import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.PackageUtils
import org.apache.commons.lang3.ArrayUtils
@ -212,8 +213,15 @@ object YouTubeAppClient {
return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get()
}
private fun useIOS(): Boolean {
return PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get()
}
fun availableClientTypes(preferredClient: ClientType): Array<ClientType> {
val availableClientTypes = ClientType.CLIENT_ORDER_TO_USE
val availableClientTypes = if (useIOS())
ClientType.CLIENT_ORDER_TO_USE_IOS
else
ClientType.CLIENT_ORDER_TO_USE
if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
@ -278,10 +286,6 @@ object YouTubeAppClient {
* If true, 'Authorization' must be included.
*/
val requireAuth: Boolean = false,
/**
* Whether a poToken is required to get playback for more than 1 minute.
*/
val requirePoToken: Boolean = false,
/**
* Client name for innertube body.
*/
@ -363,7 +367,7 @@ object YouTubeAppClient {
else
"iOS TV"
),
IOS(
IOS_DEPRECATED(
id = 5,
deviceMake = DEVICE_MAKE_IOS,
deviceModel = DEVICE_MODEL_IOS,
@ -372,7 +376,6 @@ object YouTubeAppClient {
userAgent = USER_AGENT_IOS,
clientVersion = CLIENT_VERSION_IOS,
supportsCookies = false,
requirePoToken = true,
clientName = "IOS",
friendlyName = if (forceAVC())
"iOS Force AVC"
@ -388,6 +391,15 @@ object YouTubeAppClient {
IOS_UNPLUGGED,
ANDROID_VR,
)
val CLIENT_ORDER_TO_USE_IOS: Array<ClientType> = arrayOf(
ANDROID_VR_NO_AUTH,
ANDROID_UNPLUGGED,
ANDROID_CREATOR,
IOS_UNPLUGGED,
IOS_DEPRECATED,
ANDROID_VR,
)
}
}
}

View File

@ -1,10 +1,10 @@
package app.revanced.extension.shared.patches.client;
package app.revanced.extension.shared.innertube.client;
import android.os.Build;
import java.util.Locale;
public class MusicAppClient {
public class YouTubeMusicAppClient {
// Response to the '/next' request is 'Please update to continue using the app':
// https://github.com/inotia00/ReVanced_Extended/issues/2743
@ -46,7 +46,7 @@ public class MusicAppClient {
private static final String DEVICE_MAKE_IOS_MUSIC = "Apple";
private static final String OS_NAME_IOS_MUSIC = "iOS";
private MusicAppClient() {
private YouTubeMusicAppClient() {
}
private static String androidUserAgent(String clientVersion) {

View File

@ -1,14 +1,10 @@
package app.revanced.extension.shared.patches.client
package app.revanced.extension.shared.innertube.client
/**
* Used to fetch video information.
*/
@Suppress("unused")
object YouTubeWebClient {
/**
* This user agent does not require a PoToken in [ClientType.MWEB]
* https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259
*/
private const val USER_AGENT_SAFARI =
"Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)"
@ -26,11 +22,11 @@ object YouTubeWebClient {
* Client version.
*/
@JvmField
val clientVersion: String
val clientVersion: String,
) {
MWEB(
id = 2,
clientVersion = "2.20241202.07.00"
clientVersion = "2.20241202.07.00",
),
WEB_REMIX(
id = 29,

View File

@ -1,9 +1,8 @@
package app.revanced.extension.shared.patches.spoof.requests
package app.revanced.extension.shared.innertube.requests
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient
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
import app.revanced.extension.shared.requests.Route.CompiledRoute
import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger
@ -21,96 +20,17 @@ import java.util.Locale
import java.util.TimeZone
@Suppress("deprecation")
object PlayerRoutes {
@JvmField
val CREATE_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"playlist/create" +
"?prettyPrint=false" +
"&fields=playlistId"
).compile()
@JvmField
val DELETE_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"playlist/delete" +
"?prettyPrint=false"
).compile()
@JvmField
val EDIT_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"browse/edit_playlist" +
"?prettyPrint=false" +
"&fields=status," +
"playlistEditResults"
).compile()
@JvmField
val GET_PLAYLISTS: CompiledRoute = Route(
Route.Method.POST,
"playlist/get_add_to_playlist" +
"?prettyPrint=false" +
"&fields=contents.addToPlaylistRenderer.playlists.playlistAddToOptionRenderer"
).compile()
@JvmField
val GET_CATEGORY: CompiledRoute = Route(
Route.Method.POST,
"player" +
"?prettyPrint=false" +
"&fields=microformat.playerMicroformatRenderer.category"
).compile()
@JvmField
val GET_SET_VIDEO_ID: CompiledRoute = Route(
Route.Method.POST,
"next" +
"?prettyPrint=false" +
"&fields=contents.singleColumnWatchNextResults." +
"playlist.playlist.contents.playlistPanelVideoRenderer." +
"playlistSetVideoId"
).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()
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
*/
@ -230,16 +150,10 @@ object PlayerRoutes {
client.put("osName", clientType.osName)
client.put("osVersion", clientType.osVersion)
client.put("androidSdkVersion", clientType.androidSdkVersion)
if (clientType.gmscoreVersionCode != null) {
client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
}
client.put(
"hl",
LOCALE_LANGUAGE
)
client.put("hl", LOCALE_LANGUAGE)
client.put("gl", LOCALE_COUNTRY)
client.put("timeZone", TIME_ZONE_ID)
client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES")
client.put("utcOffsetMinutes", UTC_OFFSET_MINUTES.toString())
val context = JSONObject()
context.put("client", client)
@ -269,7 +183,7 @@ object PlayerRoutes {
videoIds.put(0, videoId)
innerTubeBody.put("videoIds", videoIds)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create create/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -284,7 +198,7 @@ object PlayerRoutes {
try {
innerTubeBody.put("playlistId", playlistId)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create delete/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -314,7 +228,7 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create edit/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -330,7 +244,7 @@ object PlayerRoutes {
innerTubeBody.put("playlistId", playlistId)
innerTubeBody.put("excludeWatchLater", false)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create get/playlists innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -354,44 +268,62 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create save/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
}
@JvmStatic
fun getPlayerResponseConnectionFromRoute(
fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute,
clientType: YouTubeAppClient.ClientType
): HttpURLConnection {
return getPlayerResponseConnectionFromRoute(
route,
clientType.userAgent,
clientType.id.toString(),
clientType.clientVersion
)
}
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 getPlayerResponseConnectionFromRoute(
fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute,
clientType: YouTubeWebClient.ClientType
): HttpURLConnection {
return getPlayerResponseConnectionFromRoute(
route,
clientType.userAgent,
clientType.id.toString(),
clientType.clientVersion,
)
}
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 getPlayerResponseConnectionFromRoute(
fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute,
userAgent: String,
clientId: String,
clientVersion: 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)
@ -403,8 +335,29 @@ object PlayerRoutes {
connection.useCaches = false
connection.doOutput = true
connection.connectTimeout = CONNECTION_TIMEOUT_MILLISECONDS
connection.readTimeout = CONNECTION_TIMEOUT_MILLISECONDS
connection.connectTimeout = connectTimeout
connection.readTimeout = readTimeout
if (requestHeader != null) {
for (key in REQUEST_HEADER_KEYS) {
var value = requestHeader[key]
if (value != null) {
if (key == AUTHORIZATION_HEADER) {
if (!supportsCookies) {
continue
}
}
connection.setRequestProperty(key, value)
}
}
}
// Used to identify brand accounts
if (dataSyncId != null && dataSyncId.isNotEmpty()) {
connection.setRequestProperty("X-Goog-PageId", dataSyncId)
}
return connection
}

View File

@ -0,0 +1,108 @@
package app.revanced.extension.shared.innertube.requests
import app.revanced.extension.shared.requests.Route
import app.revanced.extension.shared.requests.Route.CompiledRoute
object InnerTubeRoutes {
@JvmField
val CREATE_PLAYLIST = compileRoute(
endpoint = "playlist/create",
fields = "playlistId",
)
@JvmField
val DELETE_PLAYLIST = compileRoute(
endpoint = "playlist/delete",
)
@JvmField
val EDIT_PLAYLIST = compileRoute(
endpoint = "browse/edit_playlist",
fields = "status," + "playlistEditResults",
)
@JvmField
val GET_CATEGORY = compileRoute(
endpoint = "player",
fields = "microformat.playerMicroformatRenderer.category",
)
@JvmField
val GET_PLAYLISTS = compileRoute(
endpoint = "playlist/get_add_to_playlist",
fields = "contents.addToPlaylistRenderer.playlists.playlistAddToOptionRenderer",
)
@JvmField
val GET_SET_VIDEO_ID = compileRoute(
endpoint = "next",
fields = "contents.singleColumnWatchNextResults." +
"playlist.playlist.contents.playlistPanelVideoRenderer." +
"playlistSetVideoId",
)
@JvmField
val GET_PLAYLIST_PAGE = compileRoute(
endpoint = "next",
fields = "contents.singleColumnWatchNextResults.playlist.playlist",
)
@JvmField
val GET_STREAMING_DATA = compileRoute(
endpoint = "player",
fields = "streamingData",
alt = "proto",
prettier = true,
)
@JvmField
val GET_VIDEO_ACTION_BUTTON = compileRoute(
endpoint = "next",
fields = "contents.singleColumnWatchNextResults." +
"results.results.contents.slimVideoMetadataSectionRenderer." +
"contents.elementRenderer.newElement.type.componentType." +
"model.videoActionBarModel.buttons.buttonViewModel"
)
@JvmField
val GET_VIDEO_DETAILS = compileRoute(
endpoint = "player",
fields = "videoDetails.channelId," +
"videoDetails.isLiveContent," +
"videoDetails.isUpcoming"
)
private fun compileRoute(
endpoint: String,
fields: String? = null,
alt: String? = null,
prettier: Boolean = false,
): CompiledRoute {
var query = Array<String>(4) { "&" }
var i = 0
query[i] = "?"
val sb = StringBuilder(endpoint)
if (prettier == false) {
sb.append(query[i++])
sb.append("prettyPrint=false")
}
if (fields != null) {
sb.append(query[i++])
sb.append("fields=")
sb.append(fields)
}
if (alt != null) {
sb.append(query[i++])
sb.append("alt=")
sb.append(alt)
}
return Route(
Route.Method.POST,
sb.toString()
).compile()
}
}

View File

@ -37,7 +37,7 @@ public class FullscreenAdsPatch {
* Therefore, make sure that the dialog contains the ads at the beginning of the Method
*
* @param bytes proto buffer array
* @param type dialog type (similar to {@link Enum#ordinal()})
* @param type dialog type (similar to {@link Enum#ordinal()})
*/
public static void checkDialog(byte[] bytes, int type) {
if (!HIDE_FULLSCREEN_ADS) {

View File

@ -11,4 +11,8 @@ public class PatchStatus {
// Replace this with true If the Spoof streaming data patch succeeds in YouTube.
return false;
}
public static boolean SpoofStreamingDataIOS() {
return false;
}
}

View File

@ -2,7 +2,6 @@ package app.revanced.extension.shared.patches;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
import androidx.annotation.NonNull;

View File

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

View File

@ -1,11 +1,11 @@
package app.revanced.extension.shared.patches.spoof;
import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("unused")
public class SpoofClientPatch extends BlockRequestPatch {
private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get();
private static final ClientType CLIENT_TYPE = BaseSettings.SPOOF_CLIENT_TYPE.get();
/**
* Injection point.

View File

@ -10,21 +10,21 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import app.revanced.extension.shared.patches.client.YouTubeAppClient.ClientType;
import app.revanced.extension.shared.innertube.client.YouTubeAppClient.ClientType;
import app.revanced.extension.shared.patches.PatchStatus;
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch {
private static final String PO_TOKEN =
BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get();
private static final String VISITOR_DATA =
BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get();
private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION =
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get();
private static final boolean SPOOF_STREAMING_DATA_TYPE_IOS =
PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get();
/**
* Any unreachable ip address. Used to intentionally fail requests.
@ -69,17 +69,27 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* Skip response encryption in OnesiePlayerRequest.
*/
public static boolean skipResponseEncryption(boolean original) {
if (SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
return false;
if (!SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
return original;
}
return false;
}
return original;
/**
* Injection point.
* Turns off a feature flag that interferes with video playback.
*/
public static boolean usePlaybackStartFeatureFlag(boolean original) {
if (!SPOOF_STREAMING_DATA) {
return original;
}
return false;
}
/**
* Injection point.
*/
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
public static void fetchStreams(String url, Map<String, String> requestHeader) {
if (SPOOF_STREAMING_DATA) {
String id = Utils.getVideoIdFromRequest(url);
if (id == null) {
@ -89,7 +99,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return;
}
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN);
StreamingDataRequest.fetchRequest(id, requestHeader);
}
}
@ -210,6 +220,18 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return videoFormat;
}
public static String[] getEntries() {
return SPOOF_STREAMING_DATA_TYPE_IOS
? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entries")
: ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entries");
}
public static String[] getEntryValues() {
return SPOOF_STREAMING_DATA_TYPE_IOS
? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entry_values")
: ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entry_values");
}
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {

View File

@ -1,14 +1,14 @@
package app.revanced.extension.shared.patches.spoof.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.StringRef.str
import app.revanced.extension.shared.utils.Utils
import org.apache.commons.lang3.StringUtils
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
@ -32,21 +32,19 @@ import java.util.concurrent.TimeoutException
* did use its own client streams.
*/
class StreamingDataRequest private constructor(
videoId: String, playerHeaders: Map<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
requestHeader: Map<String, String>,
) {
private val videoId: String
private val future: Future<ByteBuffer?>
init {
Objects.requireNonNull(playerHeaders)
Objects.requireNonNull(requestHeader)
this.videoId = videoId
this.future = Utils.submitOnBackgroundThread {
fetch(
videoId,
playerHeaders,
visitorId,
botGuardPoToken
requestHeader,
)
}
}
@ -86,33 +84,16 @@ class StreamingDataRequest private constructor(
companion object {
private const val AUTHORIZATION_HEADER = "Authorization"
private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id"
private val REQUEST_HEADER_KEYS = arrayOf(
AUTHORIZATION_HEADER, // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
VISITOR_ID_HEADER
)
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
private var lastSpoofedClientFriendlyName: String? = null
@GuardedBy("itself")
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
@ -126,22 +107,24 @@ class StreamingDataRequest private constructor(
@JvmStatic
val lastSpoofedClientName: String
get() = lastSpoofedClientType
?.friendlyName
?: "Unknown"
get() {
return if (lastSpoofedClientFriendlyName != null) {
lastSpoofedClientFriendlyName!!
} else {
"Unknown"
}
}
@JvmStatic
fun fetchRequest(
videoId: String, fetchHeaders: Map<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
fetchHeaders: Map<String, String>,
) {
// Always fetch, even if there is an existing request for the same video.
cache[videoId] =
StreamingDataRequest(
videoId,
fetchHeaders,
visitorId,
botGuardPoToken
fetchHeaders
)
}
@ -150,71 +133,40 @@ class StreamingDataRequest private constructor(
return cache[videoId]
}
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
private fun handleConnectionError(
toastMessage: String,
ex: Exception?,
showToast: Boolean = false,
) {
if (showToast) Utils.showToastShort(toastMessage)
Logger.printInfo({ toastMessage }, ex)
}
private fun send(
clientType: YouTubeAppClient.ClientType,
videoId: String,
playerHeaders: Map<String, String>,
visitorId: String,
botGuardPoToken: String
requestHeader: Map<String, String>,
): HttpURLConnection? {
Objects.requireNonNull(clientType)
Objects.requireNonNull(videoId)
Objects.requireNonNull(playerHeaders)
Objects.requireNonNull(requestHeader)
val startTime = System.currentTimeMillis()
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
try {
val connection =
getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
val usePoToken =
clientType.requirePoToken && !StringUtils.isAnyEmpty(botGuardPoToken, visitorId)
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
if (key == AUTHORIZATION_HEADER) {
if (!clientType.supportsCookies) {
Logger.printDebug { "Not including request header: $key" }
continue
}
}
if (key == VISITOR_ID_HEADER && usePoToken) {
val originalVisitorId: String = value
Logger.printDebug { "Original visitor id:\n$originalVisitorId" }
Logger.printDebug { "Replaced visitor id:\n$visitorId" }
value = visitorId
}
connection.setRequestProperty(key, value)
}
}
val requestBody: ByteArray
if (usePoToken) {
requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
botGuardPoToken = botGuardPoToken,
visitorId = visitorId,
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
getInnerTubeResponseConnectionFromRoute(
GET_STREAMING_DATA,
clientType,
requestHeader
)
Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
} else {
requestBody =
createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
)
}
val requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -243,15 +195,15 @@ class StreamingDataRequest private constructor(
}
private fun fetch(
videoId: String, playerHeaders: Map<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
requestHeader: Map<String, String>,
): ByteBuffer? {
lastSpoofedClientType = null
lastSpoofedClientFriendlyName = null
// Retry with different client if empty response body is received.
for (clientType in CLIENT_ORDER_TO_USE) {
if (clientType.requireAuth &&
playerHeaders[AUTHORIZATION_HEADER] == null
requestHeader[AUTHORIZATION_HEADER] == null
) {
Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
continue
@ -259,9 +211,7 @@ class StreamingDataRequest private constructor(
send(
clientType,
videoId,
playerHeaders,
visitorId,
botGuardPoToken
requestHeader,
)?.let { connection ->
try {
// gzip encoding doesn't response with content length (-1),
@ -271,14 +221,14 @@ class StreamingDataRequest private constructor(
} else {
BufferedInputStream(connection.inputStream).use { inputStream ->
ByteArrayOutputStream().use { stream ->
val buffer = ByteArray(2048)
val buffer = ByteArray(4096)
var bytesRead: Int
while ((inputStream.read(buffer)
.also { bytesRead = it }) >= 0
) {
stream.write(buffer, 0, bytesRead)
}
lastSpoofedClientType = clientType
lastSpoofedClientFriendlyName = clientType.friendlyName
return ByteBuffer.wrap(stream.toByteArray())
}
}
@ -289,7 +239,12 @@ class StreamingDataRequest private constructor(
}
}
handleConnectionError("Could not fetch any client streams", null)
handleConnectionError(str("revanced_spoof_streaming_data_failed_forbidden"), null, true)
handleConnectionError(
str("revanced_spoof_streaming_data_failed_forbidden_suggestion"),
null,
true
)
return null
}
}

View File

@ -3,10 +3,10 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import app.revanced.extension.shared.innertube.client.YouTubeAppClient;
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType;
import app.revanced.extension.shared.patches.client.MusicAppClient;
import app.revanced.extension.shared.patches.client.YouTubeAppClient;
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability;
/**
@ -31,7 +31,7 @@ public class BaseSettings {
* Some patches are in a shared path, so they are declared here.
*/
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
public static final EnumSetting<MusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true);
public static final EnumSetting<YouTubeMusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", YouTubeMusicAppClient.ClientType.IOS_MUSIC_6_21, true);
/**
* These settings are used by YouTube.
@ -43,11 +43,9 @@ public class BaseSettings {
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = new BooleanSetting("revanced_spoof_streaming_data_skip_response_encryption", TRUE, true);
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
public static final BooleanSetting SPOOF_STREAMING_DATA_TYPE_IOS = new BooleanSetting("revanced_spoof_streaming_data_type_ios", FALSE, true, "revanced_spoof_streaming_data_type_ios_user_dialog_message");
// Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_UNPLUGGED, true);
public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true);
/**
* These settings are used by YouTube and YouTube Music.

View File

@ -149,7 +149,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* Updates all Preferences values and their availability using the current values in {@link Setting}.
*/
protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true,true);
updatePreferenceScreen(getPreferenceScreen(), true, true);
}
/**
@ -246,7 +246,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/**
* Updates a UI Preference with the {@link Setting} that backs it.
*
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/

View File

@ -10,6 +10,8 @@ import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.settings.Setting;
@ -19,6 +21,12 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"})
public class ResettableEditTextPreference extends EditTextPreference {
/**
* Setting to reset.
*/
@Nullable
private Setting<?> setting;
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@ -35,6 +43,10 @@ public class ResettableEditTextPreference extends EditTextPreference {
super(context);
}
public void setSetting(@Nullable Setting<?> setting) {
this.setting = setting;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder);
@ -44,7 +56,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
if (title != null) {
builder.setTitle(getTitle());
}
final Setting<?> setting = Setting.getSettingFromPath(getKey());
if (setting == null) {
String key = getKey();
if (key != null) {
setting = Setting.getSettingFromPath(key);
}
}
if (setting != null) {
builder.setNeutralButton(str("revanced_extended_settings_reset"), null);
}
@ -65,8 +82,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
}
button.setOnClickListener(v -> {
try {
Setting<?> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey()));
String defaultStringValue = setting.defaultValue.toString();
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
EditText editText = getEditText();
editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text

View File

@ -60,6 +60,7 @@ public class Utils {
private static WeakReference<Activity> activityRef = new WeakReference<>(null);
@SuppressLint("StaticFieldLeak")
private static volatile Context context;
private static Locale contextLocale;
protected Utils() {
} // utility class
@ -308,34 +309,51 @@ public class Utils {
* @return Context with locale applied.
*/
public static Context getLocalizedContext(Context mContext) {
Activity mActivity = activityRef.get();
if (mActivity == null) {
return mContext;
}
if (mContext == null) {
return null;
try {
Activity mActivity = activityRef.get();
if (mActivity != null && mContext != null) {
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
// Locale of Application.
Locale applicationLocale = language == AppLanguage.DEFAULT
? mActivity.getResources().getConfiguration().locale
: language.getLocale();
// Locale of Context.
Locale contextLocale = mContext.getResources().getConfiguration().locale;
// If they are different, overrides the Locale of the Context and resource.
if (applicationLocale != contextLocale) {
Utils.contextLocale = contextLocale;
// If they are different, overrides the Locale of the Context and resource.
Locale.setDefault(applicationLocale);
Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
configuration.setLocale(applicationLocale);
return mContext.createConfigurationContext(configuration);
}
}
} catch (Exception ex) {
Logger.printException(() -> "getLocalizedContext failed", ex);
}
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
return mContext;
}
// Locale of Application.
Locale applicationLocale = language == AppLanguage.DEFAULT
? mActivity.getResources().getConfiguration().locale
: language.getLocale();
// Locale of Context.
Locale contextLocale = mContext.getResources().getConfiguration().locale;
// If they are identical, no need to override them.
if (applicationLocale == contextLocale) {
return mContext;
public static void resetLocalizedContext() {
try {
if (contextLocale != null) {
Locale.setDefault(contextLocale);
Context mContext = getContext();
if (mContext != null) {
Configuration config = mContext.getResources().getConfiguration();
config.setLocale(contextLocale);
setContext(mContext.createConfigurationContext(config));
}
}
} catch (Exception ex) {
Logger.printException(() -> "resetLocalizedContext failed", ex);
}
// If they are different, overrides the Locale of the Context and resource.
Locale.setDefault(applicationLocale);
Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
configuration.setLocale(applicationLocale);
return mContext.createConfigurationContext(configuration);
}
public static void setActivity(Activity mainActivity) {
@ -353,14 +371,6 @@ public class Utils {
// Must initially set context to check the app language.
context = appContext;
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
if (language != AppLanguage.DEFAULT) {
// Create a new context with the desired language.
Configuration config = appContext.getResources().getConfiguration();
config.setLocale(language.getLocale());
context = appContext.createConfigurationContext(config);
}
}
public static void setClipboard(@NonNull String text) {
@ -538,14 +548,6 @@ public class Utils {
return Build.VERSION.SDK_INT >= sdk;
}
public static int dpToPx(float dp) {
if (context == null) {
return (int) dp;
} else {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
}
public static int dpToPx(int dp) {
if (context == null) {
return dp;
@ -608,10 +610,10 @@ public class Utils {
* <br>
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
*<br>
* <br>
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
*<br>
* <br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/

View File

@ -6,7 +6,6 @@ import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")

View File

@ -6,7 +6,6 @@ import app.revanced.extension.shared.settings.Setting.Availability
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.youtube.settings.Settings
import org.apache.commons.lang3.StringUtils
import kotlin.Boolean
@Suppress("unused")
object ChangeStartPagePatch {
@ -44,7 +43,7 @@ object ChangeStartPagePatch {
}
appLaunched = true
Logger.printDebug{ "Changing browseId to $browseId" }
Logger.printDebug { "Changing browseId to $browseId" }
return browseId
}

View File

@ -39,7 +39,7 @@ public final class DownloadActionsPatch {
* <p>
* Appears to always be called from the main thread.
*/
public static boolean inAppVideoDownloadButtonOnClick(@Nullable Map<Object, Object> map,Object offlineVideoEndpointOuterClass,
public static boolean inAppVideoDownloadButtonOnClick(@Nullable Map<Object, Object> map, Object offlineVideoEndpointOuterClass,
@Nullable String videoId) {
try {
if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(videoId)) {

View File

@ -119,9 +119,9 @@ public class GeneralPatch {
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.
* @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)) {

View File

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

View File

@ -2,8 +2,10 @@ package app.revanced.extension.youtube.patches.general.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_DETAILS
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@ -86,12 +88,11 @@ class VideoDetailsRequest private constructor(
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_DETAILS,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_VIDEO_DETAILS,
clientType
)
val requestBody =
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
val requestBody = createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)

View File

@ -15,7 +15,16 @@ public class BackgroundPlaybackPatch {
*/
public static boolean isBackgroundPlaybackAllowed(boolean original) {
if (original) return true;
return ShortsPlayerState.getCurrent().isClosed();
return ShortsPlayerState.getCurrent().isClosed() &&
// 1. Shorts background playback is enabled.
// 2. Autoplay in feed is turned on.
// 3. Play Shorts from feed.
// 4. Media controls appear in status bar.
// (For unpatched YouTube with Premium accounts, media controls do not appear in the status bar)
//
// This is just a visual bug and does not affect Shorts background play in any way.
// To fix this, just check PlayerType.
PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL;
}
/**

View File

@ -1,5 +1,7 @@
package app.revanced.extension.youtube.patches.player;
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.REMIX;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
@ -8,8 +10,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.*;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@ -106,8 +106,8 @@ public class ActionButtonsPatch {
/**
* Injection point.
*
* @param list Type list of litho components
* @param identifier Identifier of litho components
* @param list Type list of litho components
* @param identifier Identifier of litho components
*/
public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
try {

View File

@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
@ -34,7 +33,6 @@ import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.EngagementPanel;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.RootView;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils;
@ -185,8 +183,8 @@ public class PlayerPatch {
// The type of descriptionView can be either ViewGroup or TextView. (A/B tests)
// If the type of descriptionView is TextView, longer delay is required.
final long delayMillis = descriptionView instanceof TextView
? 500
: 100;
? 750
: 200;
Utils.runOnMainThreadDelayed(() -> Utils.clickView(descriptionView), delayMillis);
}
@ -441,7 +439,7 @@ public class PlayerPatch {
if (isLiveChatOrPlaylistPanel) {
return true;
}
return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed();
return isAutoPopupPanel && !RootView.isShortsActive();
}
/**
@ -471,8 +469,8 @@ public class PlayerPatch {
* Used in YouTube 20.05.46+.
*/
public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) {
Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L);
}

View File

@ -47,7 +47,7 @@ public class SeekbarColorPatch {
/**
* Empty seekbar gradient, if hide seekbar in feed is enabled.
*/
private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = { 0x0, 0x0 };
private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = {0x0, 0x0};
/**
* Default YouTube seekbar color brightness.

View File

@ -1,8 +1,10 @@
package app.revanced.extension.youtube.patches.player.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_ACTION_BUTTON
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
class ActionButtonRequest private constructor(
private val videoId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders)
fetch(videoId, requestHeader)
}
val array: Array<ActionButton>
@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
})
@JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) {
fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
Objects.requireNonNull(videoId)
synchronized(cache) {
if (!cache.containsKey(videoId)) {
cache[videoId] = ActionButtonRequest(videoId, playerHeaders)
cache[videoId] = ActionButtonRequest(videoId, requestHeader)
}
}
}
@ -93,43 +87,28 @@ class ActionButtonRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(videoId: String, playerHeaders: Map<String, String>): JSONObject? {
private fun sendRequest(videoId: String, requestHeader: Map<String, String>): JSONObject? {
Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis()
// '/next' request does not require PoToken.
// '/next' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_ACTION_BUTTON,
clientType
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
// Since [THANKS] button and [CLIP] button are shown only with the logged in,
// Set the [Authorization] field to property to get the correct action buttons.
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val connection = getInnerTubeResponseConnectionFromRoute(
GET_VIDEO_ACTION_BUTTON,
clientType,
requestHeader,
)
val requestBody =
PlayerRoutes.createApplicationRequestBody(
clientType = clientType,
videoId = videoId
)
val requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
return emptyArray()
}
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Array<ActionButton> {
val json = sendRequest(videoId, playerHeaders)
private fun fetch(
videoId: String,
requestHeader: Map<String, String>
): Array<ActionButton> {
val json = sendRequest(videoId, requestHeader)
if (json != null) {
return parseResponse(json)
}

View File

@ -2,6 +2,7 @@ package app.revanced.extension.youtube.patches.shorts;
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible;
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
import android.content.Context;
@ -30,7 +31,6 @@ import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.VideoUtils;
@ -55,7 +55,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) {
return;
}
if (ShortsPlayerState.getCurrent().isClosed()) {
if (!isShortsActive()) {
return;
}
if (!isMoreButton(enumString)) {
@ -118,7 +118,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return;
}
if (ShortsPlayerState.getCurrent().isClosed()) {
if (!isShortsActive()) {
return;
}
if (bottomSheetMenuObject == null) {
@ -136,7 +136,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return;
}
if (ShortsPlayerState.getCurrent().isClosed()) {
if (!isShortsActive()) {
return;
}
for (CustomAction customAction : CustomAction.values()) {
@ -155,6 +155,34 @@ public final class CustomActionsPatch {
Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject);
}
/**
* Injection point.
*/
public static boolean onBottomSheetMenuItemClick(View view) {
try {
if (view instanceof ViewGroup viewGroup) {
TextView textView = Utils.getChildView(viewGroup, v -> v instanceof TextView);
if (textView != null) {
String menuTitle = textView.getText().toString();
for (CustomAction customAction : CustomAction.values()) {
if (customAction.getLabel().equals(menuTitle)) {
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
if (onLongClick != null) {
view.setOnLongClickListener(onLongClick);
}
customAction.getOnClickAction().run();
return true;
}
}
}
}
} catch (Exception ex) {
Logger.printException(() -> "onBottomSheetMenuItemClick failed");
}
return false;
}
/**
* Injection point.
*/
@ -164,7 +192,7 @@ public final class CustomActionsPatch {
}
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try {
if (ShortsPlayerState.getCurrent().isClosed()) {
if (!isShortsActive()) {
return;
}
contextRef = new WeakReference<>(recyclerView.getContext());
@ -179,8 +207,9 @@ public final class CustomActionsPatch {
if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) {
childCount = recyclerView.getChildCount();
if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) {
String menuTitle = textView.getText().toString();
for (CustomAction customAction : CustomAction.values()) {
if (customAction.getLabel().equals(textView.getText().toString())) {
if (customAction.getLabel().equals(menuTitle)) {
View.OnClickListener onClick = customAction.getOnClickListener();
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
recyclerViewRef = new WeakReference<>(recyclerView);
@ -296,6 +325,11 @@ public final class CustomActionsPatch {
true
)
),
SPEED_DIALOG(
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG,
"yt_outline_play_arrow_half_circle_black_24",
() -> VideoUtils.showPlaybackSpeedDialog(contextRef.get())
),
REPEAT_STATE(
Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE,
"yt_outline_arrow_repeat_1_black_24",

View File

@ -30,8 +30,8 @@ public class ShortsRepeatStatePatch {
END_SCREEN;
static void setYTEnumValue(Enum<?> ytBehavior) {
String ytName = ytBehavior.name();
for (ShortsLoopBehavior rvBehavior : values()) {
String ytName = ytBehavior.name();
if (ytName.endsWith(rvBehavior.name())) {
if (rvBehavior.ytEnumValue != null) {
Logger.printException(() -> "Conflicting behavior names: " + rvBehavior
@ -87,24 +87,24 @@ public class ShortsRepeatStatePatch {
@Nullable
public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) {
try {
if (original == null) {
Logger.printDebug(() -> "Original is null, returning null");
return null;
}
ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
isAppInBackgroundPiPMode()
? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get()
: Settings.CHANGE_SHORTS_REPEAT_STATE.get();
Enum<?> overrideBehavior = behavior.ytEnumValue;
if (overrideBehavior != null) {
Logger.printDebug(() -> overrideBehavior == original
? "Behavior setting is same as original. Using original: " + original.name()
: "Changing Shorts repeat behavior from: " + original.name() + " to: " + overrideBehavior.name()
);
if (behavior != ShortsLoopBehavior.UNKNOWN && overrideBehavior != null) {
Logger.printDebug(() -> {
String name = original == null ? "unknown (null)" : original.name();
return overrideBehavior == original
? "Behavior setting is same as original. Using original: " + name
: "Changing Shorts repeat behavior from: " + name + " to: " + overrideBehavior.name();
});
return overrideBehavior;
// For some reason, in YouTube 20.09+, 'UNKNOWN' functions as 'Pause'.
return ExtendedUtils.IS_20_09_OR_GREATER && behavior == ShortsLoopBehavior.END_SCREEN
? ShortsLoopBehavior.UNKNOWN.ytEnumValue
: overrideBehavior;
}
} catch (Exception ex) {
Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
@ -117,6 +117,6 @@ public class ShortsRepeatStatePatch {
* Injection point.
*/
public static boolean isAutoPlay(@Nullable Enum<?> original) {
return original != null && ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original;
return ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original;
}
}

View File

@ -4,6 +4,7 @@ import android.view.View;
import java.lang.ref.WeakReference;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "deprecation"})
@ -59,4 +60,20 @@ public class SwipeControlsPatch {
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE;
}
public static final class SwipeOverlayTextSizeAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
!Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
}
}
public static final class SwipeOverlayModernUIAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
}
}
}

View File

@ -9,6 +9,11 @@ public class PatchStatus {
return false;
}
// Modified by a patch. Do not touch.
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
return false;
}
public static boolean OldSplashAnimation() {
// Replace this with true if the Restore old splash animation (Custom branding icon) succeeds
return false;
@ -40,23 +45,22 @@ public class PatchStatus {
return false;
}
public static String SpoofAppVersionDefaultString() {
return "18.17.43";
}
public static boolean ToolBarComponents() {
// Replace this with true if the Toolbar components patch succeeds
return false;
}
public static long PatchedTime() {
return 0L;
}
public static String SpoofAppVersionDefaultString() {
return "18.17.43";
}
// Modified by a patch. Do not touch.
public static String RVXMusicPackageName() {
return "com.google.android.apps.youtube.music";
}
// Modified by a patch. Do not touch.
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
return false;
}
}

View File

@ -1,20 +1,24 @@
package app.revanced.extension.youtube.patches.utils;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.shared.EngagementPanel;
import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused")
public class PlaybackSpeedWhilePlayingPatch {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
public static boolean playbackSpeedChanged(float playbackSpeed) {
PlayerType playerType = PlayerType.getCurrent();
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED &&
playerType.isMaximizedOrFullscreenOrPiP()) {
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED) {
if (PlayerType.getCurrent().isMaximizedOrFullscreenOrPiP()
// Since RVX has a default playback speed setting for Shorts,
// Playback speed reset should also be prevented in Shorts.
|| ShortsPlayerState.getCurrent().isOpen() && EngagementPanel.isOpen()) {
Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request");
Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request: " + playerType.name());
return true;
return true;
}
}
return false;

View File

@ -1,5 +1,11 @@
package app.revanced.extension.youtube.patches.utils;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed;
import static app.revanced.extension.youtube.utils.VideoUtils.dismissPlayer;
import static app.revanced.extension.youtube.utils.VideoUtils.launchVideoExternalDownloader;
import static app.revanced.extension.youtube.utils.VideoUtils.openPlaylist;
import android.content.Context;
import android.view.KeyEvent;
import android.widget.LinearLayout;
@ -8,6 +14,8 @@ 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;
@ -26,62 +34,61 @@ 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.shared.PlayerType;
import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.AuthUtils;
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"
};
public class PlaylistPatch extends AuthUtils {
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 = "";
private static volatile boolean isIncognito = false;
private static volatile Map<String, String> requestHeader;
private static volatile String playlistId = "";
private static volatile String videoId = "";
private static final String checkFailedAuth =
ResourceUtils.getString("revanced_queue_manager_check_failed_auth");
private static final String checkFailedPlaylistId =
ResourceUtils.getString("revanced_queue_manager_check_failed_playlist_id");
private static final String checkFailedQueue =
ResourceUtils.getString("revanced_queue_manager_check_failed_queue");
private static final String checkFailedVideoId =
ResourceUtils.getString("revanced_queue_manager_check_failed_video_id");
private static final String checkFailedGeneric =
ResourceUtils.getString("revanced_queue_manager_check_failed_generic");
private static String checkFailedAuth;
private static String checkFailedPlaylistId;
private static String checkFailedQueue;
private static String checkFailedVideoId;
private static String checkFailedGeneric;
private static final String fetchFailedAdd =
ResourceUtils.getString("revanced_queue_manager_fetch_failed_add");
private static final String fetchFailedCreate =
ResourceUtils.getString("revanced_queue_manager_fetch_failed_create");
private static final String fetchFailedDelete =
ResourceUtils.getString("revanced_queue_manager_fetch_failed_delete");
private static final String fetchFailedRemove =
ResourceUtils.getString("revanced_queue_manager_fetch_failed_remove");
private static final String fetchFailedSave =
ResourceUtils.getString("revanced_queue_manager_fetch_failed_save");
private static String fetchFailedAdd;
private static String fetchFailedCreate;
private static String fetchFailedDelete;
private static String fetchFailedRemove;
private static String fetchFailedSave;
private static final String fetchSucceededAdd =
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_add");
private static final String fetchSucceededCreate =
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_create");
private static final String fetchSucceededDelete =
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_delete");
private static final String fetchSucceededRemove =
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_remove");
private static final String fetchSucceededSave =
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_save");
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<>();
@ -111,6 +118,7 @@ public class PlaylistPatch extends VideoUtils {
if (videoId != null) {
lastVideoIds.remove(videoId, setVideoId);
EditPlaylistRequest.clearVideoId(videoId);
Logger.printDebug(() -> "Video removed by YouTube flyout menu: " + videoId);
}
}
}
@ -119,33 +127,9 @@ public class PlaylistPatch extends VideoUtils {
/**
* Injection point.
*/
public static void setIncognitoStatus(boolean incognito) {
public static void setPivotBar(PivotBar view) {
if (QUEUE_MANAGER) {
isIncognito = incognito;
}
}
/**
* 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);
}
mContext = view.getContext();
}
}
@ -160,7 +144,7 @@ public class PlaylistPatch extends VideoUtils {
* Invoked by extension.
*/
public static void prepareDialogBuilder(@NonNull String currentVideoId) {
if (authorization.isEmpty() || isIncognito) {
if (authorization.isEmpty() || (dataSyncId.isEmpty() && isIncognito)) {
handleCheckError(checkFailedAuth);
return;
}
@ -169,9 +153,22 @@ public class PlaylistPatch extends VideoUtils {
} else {
videoId = currentVideoId;
synchronized (lastVideoIds) {
QueueManager[] customActionsEntries = playlistId.isEmpty() || lastVideoIds.get(currentVideoId) == null
? QueueManager.addToQueueEntries
: QueueManager.removeFromQueueEntries;
QueueManager[] customActionsEntries;
boolean canReload = PlayerType.getCurrent().isMaximizedOrFullscreen() &&
lastVideoIds.get(VideoInformation.getVideoId()) != null;
if (playlistId.isEmpty() || lastVideoIds.get(currentVideoId) == null) {
if (canReload) {
customActionsEntries = QueueManager.addToQueueWithReloadEntries;
} else {
customActionsEntries = QueueManager.addToQueueEntries;
}
} else {
if (canReload) {
customActionsEntries = QueueManager.removeFromQueueWithReloadEntries;
} else {
customActionsEntries = QueueManager.removeFromQueueEntries;
}
}
buildBottomSheetDialog(customActionsEntries);
}
@ -200,13 +197,14 @@ public class PlaylistPatch extends VideoUtils {
ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
}
private static void fetchQueue(boolean remove, boolean openPlaylist, boolean openVideo) {
private static void fetchQueue(boolean remove, boolean openPlaylist,
boolean openVideo, boolean reload) {
try {
String currentPlaylistId = playlistId;
String currentVideoId = videoId;
synchronized (lastVideoIds) {
if (currentPlaylistId.isEmpty()) { // Queue is empty, create new playlist.
CreatePlaylistRequest.fetchRequestIfNeeded(currentVideoId, requestHeader);
CreatePlaylistRequest.fetchRequestIfNeeded(currentVideoId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> {
CreatePlaylistRequest request = CreatePlaylistRequest.getRequestForVideoId(currentVideoId);
if (request != null) {
@ -220,7 +218,7 @@ public class PlaylistPatch extends VideoUtils {
showToast(fetchSucceededCreate);
Logger.printDebug(() -> "Queue successfully created, playlistId: " + createdPlaylistId + ", setVideoId: " + setVideoId);
if (openPlaylist) {
openQueue(currentVideoId, openVideo);
openQueue(currentVideoId, openVideo, reload);
}
return;
}
@ -230,7 +228,7 @@ public class PlaylistPatch extends VideoUtils {
}, 1000);
} else { // Queue is not empty, add or remove video.
String setVideoId = lastVideoIds.get(currentVideoId);
EditPlaylistRequest.fetchRequestIfNeeded(currentVideoId, currentPlaylistId, setVideoId, requestHeader);
EditPlaylistRequest.fetchRequestIfNeeded(currentVideoId, currentPlaylistId, setVideoId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> {
EditPlaylistRequest request = EditPlaylistRequest.getRequestForVideoId(currentVideoId);
@ -238,22 +236,24 @@ public class PlaylistPatch extends VideoUtils {
String fetchedSetVideoId = request.getResult();
Logger.printDebug(() -> "fetchedSetVideoId: " + fetchedSetVideoId);
if (remove) { // Remove from queue.
if (StringUtils.isEmpty(fetchedSetVideoId)) {
if ("".equals(fetchedSetVideoId)) {
lastVideoIds.remove(currentVideoId, setVideoId);
EditPlaylistRequest.clearVideoId(currentVideoId);
showToast(fetchSucceededRemove);
if (openPlaylist) {
openQueue(currentVideoId, openVideo);
openQueue(currentVideoId, openVideo, reload);
}
return;
}
showToast(fetchFailedRemove);
} else { // Add to queue.
if (StringUtils.isNotEmpty(fetchedSetVideoId)) {
if (fetchedSetVideoId != null && !fetchedSetVideoId.isEmpty()) {
lastVideoIds.putIfAbsent(currentVideoId, fetchedSetVideoId);
EditPlaylistRequest.clearVideoId(currentVideoId);
showToast(fetchSucceededAdd);
Logger.printDebug(() -> "Video successfully added, setVideoId: " + fetchedSetVideoId);
if (openPlaylist) {
openQueue(currentVideoId, openVideo);
openQueue(currentVideoId, openVideo, reload);
}
return;
}
@ -275,7 +275,7 @@ public class PlaylistPatch extends VideoUtils {
return;
}
try {
GetPlaylistsRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader);
GetPlaylistsRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> {
GetPlaylistsRequest request = GetPlaylistsRequest.getRequestForPlaylistId(currentPlaylistId);
if (request != null) {
@ -317,7 +317,7 @@ public class PlaylistPatch extends VideoUtils {
handleCheckError(checkFailedPlaylistId);
return;
}
SavePlaylistRequest.fetchRequestIfNeeded(playlistId, libraryId, requestHeader);
SavePlaylistRequest.fetchRequestIfNeeded(playlistId, libraryId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> {
SavePlaylistRequest request = SavePlaylistRequest.getRequestForLibraryId(libraryId);
@ -343,7 +343,7 @@ public class PlaylistPatch extends VideoUtils {
return;
}
try {
DeletePlaylistRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader);
DeletePlaylistRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> {
DeletePlaylistRequest request = DeletePlaylistRequest.getRequestForPlaylistId(currentPlaylistId);
if (request != null) {
@ -375,10 +375,10 @@ public class PlaylistPatch extends VideoUtils {
}
private static void openQueue() {
openQueue("", false);
openQueue("", false, false);
}
private static void openQueue(String currentVideoId, boolean openVideo) {
private static void openQueue(String currentVideoId, boolean openVideo, boolean reload) {
String currentPlaylistId = playlistId;
if (currentPlaylistId.isEmpty()) {
handleCheckError(checkFailedQueue);
@ -390,7 +390,15 @@ public class PlaylistPatch extends VideoUtils {
return;
}
// Open a video from a playlist
openPlaylist(currentPlaylistId, currentVideoId);
if (reload) {
// Since the Queue is not automatically synced, a 'reload' action has been added as a workaround.
// The 'reload' action simply closes the video and reopens it.
// It is important to close the video, otherwise the Queue will not be updated.
dismissPlayer();
openPlaylist(currentPlaylistId, VideoInformation.getVideoId(), true);
} else {
openPlaylist(currentPlaylistId, currentVideoId);
}
} else {
// Open a playlist
openPlaylist(currentPlaylistId);
@ -409,27 +417,37 @@ public class PlaylistPatch extends VideoUtils {
ADD_TO_QUEUE(
"revanced_queue_manager_add_to_queue",
"yt_outline_list_add_black_24",
() -> fetchQueue(false, false, false)
() -> fetchQueue(false, 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)
() -> fetchQueue(false, true, false, 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)
() -> fetchQueue(false, true, true, false)
),
ADD_TO_QUEUE_AND_RELOAD_VIDEO(
"revanced_queue_manager_add_to_queue_and_reload_video",
"yt_outline_arrow_circle_black_24",
() -> fetchQueue(false, true, true, true)
),
REMOVE_FROM_QUEUE(
"revanced_queue_manager_remove_from_queue",
"yt_outline_trash_can_black_24",
() -> fetchQueue(true, false, false)
() -> fetchQueue(true, false, 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)
() -> fetchQueue(true, true, false, false)
),
REMOVE_FROM_QUEUE_AND_RELOAD_VIDEO(
"revanced_queue_manager_remove_from_queue_and_reload_video",
"yt_outline_arrow_circle_black_24",
() -> fetchQueue(true, true, true, true)
),
OPEN_QUEUE(
"revanced_queue_manager_open_queue",
@ -477,6 +495,17 @@ public class PlaylistPatch extends VideoUtils {
SAVE_QUEUE,
};
public static final QueueManager[] addToQueueWithReloadEntries = {
ADD_TO_QUEUE,
ADD_TO_QUEUE_AND_OPEN_QUEUE,
ADD_TO_QUEUE_AND_PLAY_VIDEO,
ADD_TO_QUEUE_AND_RELOAD_VIDEO,
OPEN_QUEUE,
//REMOVE_QUEUE,
EXTERNAL_DOWNLOADER,
SAVE_QUEUE,
};
public static final QueueManager[] removeFromQueueEntries = {
REMOVE_FROM_QUEUE,
REMOVE_FROM_QUEUE_AND_OPEN_QUEUE,
@ -486,6 +515,16 @@ public class PlaylistPatch extends VideoUtils {
SAVE_QUEUE,
};
public static final QueueManager[] removeFromQueueWithReloadEntries = {
REMOVE_FROM_QUEUE,
REMOVE_FROM_QUEUE_AND_OPEN_QUEUE,
REMOVE_FROM_QUEUE_AND_RELOAD_VIDEO,
OPEN_QUEUE,
//REMOVE_QUEUE,
EXTERNAL_DOWNLOADER,
SAVE_QUEUE,
};
public static final QueueManager[] noVideoIdQueueEntries = {
OPEN_QUEUE,
//REMOVE_QUEUE,

View File

@ -1,12 +1,15 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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 app.revanced.extension.youtube.patches.utils.requests.CreatePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -20,10 +23,15 @@ import java.util.concurrent.TimeoutException
class CreatePlaylistRequest private constructor(
private val videoId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Pair<String, String>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders)
fetch(
videoId,
requestHeader,
dataSyncId,
)
}
val playlistId: Pair<String, String>?
@ -52,14 +60,6 @@ class CreatePlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -80,11 +80,19 @@ class CreatePlaylistRequest private constructor(
}
@JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) {
fun fetchRequestIfNeeded(
videoId: String,
requestHeader: Map<String, String>,
dataSyncId: String,
) {
Objects.requireNonNull(videoId)
synchronized(cache) {
if (!cache.containsKey(videoId)) {
cache[videoId] = CreatePlaylistRequest(videoId, playerHeaders)
cache[videoId] = CreatePlaylistRequest(
videoId,
requestHeader,
dataSyncId,
)
}
}
}
@ -100,40 +108,28 @@ class CreatePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendCreatePlaylistRequest(videoId: String, playerHeaders: Map<String, String>): JSONObject? {
private fun sendCreatePlaylistRequest(
videoId: String,
requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? {
Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis()
// 'playlist/create' request does not require PoToken.
// '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 = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.CREATE_PLAYLIST,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
CREATE_PLAYLIST,
clientType,
requestHeader,
dataSyncId,
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.createPlaylistRequestBody(
videoId = videoId
)
val requestBody = createPlaylistRequestBody(videoId = videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -159,36 +155,33 @@ class CreatePlaylistRequest private constructor(
return null
}
private fun sendSetVideoIdRequest(videoId: String, playlistId: String, playerHeaders: Map<String, String>): JSONObject? {
private fun sendSetVideoIdRequest(
videoId: String,
playlistId: String,
requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? {
Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis()
// 'playlist/create' request does not require PoToken.
// '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 = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_SET_VIDEO_ID,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
GET_SET_VIDEO_ID,
clientType,
requestHeader,
dataSyncId,
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId
)
val requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -240,8 +233,8 @@ class CreatePlaylistRequest private constructor(
if (secondaryContentsJsonObject is JSONObject) {
return secondaryContentsJsonObject
.getJSONObject("playlistPanelVideoRenderer")
.getString("playlistSetVideoId")
.getJSONObject("playlistPanelVideoRenderer")
.getString("playlistSetVideoId")
}
} catch (e: JSONException) {
val jsonForMessage = json.toString()
@ -254,12 +247,25 @@ class CreatePlaylistRequest private constructor(
return null
}
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Pair<String, String>? {
val createPlaylistJson = sendCreatePlaylistRequest(videoId, playerHeaders)
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, playerHeaders)
val setVideoIdJson = sendSetVideoIdRequest(
videoId,
playlistId,
requestHeader,
dataSyncId
)
if (setVideoIdJson != null) {
val setVideoId = parseSetVideoIdResponse(setVideoIdJson)
if (setVideoId != null) {

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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 app.revanced.extension.youtube.patches.utils.requests.DeletePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -20,12 +21,14 @@ import java.util.concurrent.TimeoutException
class DeletePlaylistRequest private constructor(
private val playlistId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch(
playlistId,
playerHeaders,
requestHeader,
dataSyncId,
)
}
@ -55,14 +58,6 @@ class DeletePlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -85,14 +80,16 @@ class DeletePlaylistRequest private constructor(
@JvmStatic
fun fetchRequestIfNeeded(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
) {
Objects.requireNonNull(playlistId)
synchronized(cache) {
if (!cache.containsKey(playlistId)) {
cache[playlistId] = DeletePlaylistRequest(
playlistId,
playerHeaders
requestHeader,
dataSyncId,
)
}
}
@ -109,40 +106,28 @@ class DeletePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? {
Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis()
// 'playlist/delete' request does not require PoToken.
// '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 = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.DELETE_PLAYLIST,
val connection = getInnerTubeResponseConnectionFromRoute(
DELETE_PLAYLIST,
clientType,
requestHeader,
dataSyncId
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.deletePlaylistRequestBody(playlistId)
val requestBody = deletePlaylistRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -184,9 +169,14 @@ class DeletePlaylistRequest private constructor(
private fun fetch(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): Boolean? {
val json = sendRequest(playlistId, playerHeaders)
val json = sendRequest(
playlistId,
requestHeader,
dataSyncId,
)
if (json != null) {
return parseResponse(json)
}

View File

@ -1,13 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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 app.revanced.extension.youtube.patches.utils.requests.EditPlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.apache.commons.lang3.StringUtils
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -23,14 +23,16 @@ class EditPlaylistRequest private constructor(
private val videoId: String,
private val playlistId: String,
private val setVideoId: String?,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) {
private val future: Future<String> = Utils.submitOnBackgroundThread {
fetch(
videoId,
playlistId,
setVideoId,
playerHeaders,
requestHeader,
dataSyncId,
)
}
@ -60,14 +62,6 @@ class EditPlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -99,7 +93,8 @@ class EditPlaylistRequest private constructor(
videoId: String,
playlistId: String,
setVideoId: String?,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
) {
Objects.requireNonNull(videoId)
synchronized(cache) {
@ -108,7 +103,8 @@ class EditPlaylistRequest private constructor(
videoId,
playlistId,
setVideoId,
playerHeaders
requestHeader,
dataSyncId,
)
}
}
@ -125,47 +121,34 @@ class EditPlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
videoId: String,
playlistId: String,
setVideoId: String?,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? {
Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis()
// 'browse/edit_playlist' request does not require PoToken.
// '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 = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
EDIT_PLAYLIST,
clientType,
requestHeader,
dataSyncId
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.editPlaylistRequestBody(
videoId = videoId,
playlistId = playlistId,
setVideoId = setVideoId,
)
val requestBody = editPlaylistRequestBody(
videoId = videoId,
playlistId = playlistId,
setVideoId = setVideoId
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -197,7 +180,8 @@ class EditPlaylistRequest private constructor(
if (remove) {
return ""
}
val playlistEditResultsJSONObject = json.getJSONArray("playlistEditResults").get(0)
val playlistEditResultsJSONObject =
json.getJSONArray("playlistEditResults").get(0)
if (playlistEditResultsJSONObject is JSONObject) {
return playlistEditResultsJSONObject
@ -220,11 +204,18 @@ class EditPlaylistRequest private constructor(
videoId: String,
playlistId: String,
setVideoId: String?,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): String? {
val json = sendRequest(videoId, playlistId, setVideoId, playerHeaders)
val json = sendRequest(
videoId,
playlistId,
setVideoId,
requestHeader,
dataSyncId,
)
if (json != null) {
return parseResponse(json, StringUtils.isNotEmpty(setVideoId))
return parseResponse(json, setVideoId != null && setVideoId.isNotEmpty())
}
return null

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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 app.revanced.extension.youtube.patches.utils.requests.GetPlaylistsRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -20,12 +21,14 @@ import java.util.concurrent.TimeoutException
class GetPlaylistsRequest private constructor(
private val playlistId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Array<Pair<String, String>>> = Utils.submitOnBackgroundThread {
fetch(
playlistId,
playerHeaders,
requestHeader,
dataSyncId,
)
}
@ -55,14 +58,6 @@ class GetPlaylistsRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -85,14 +80,16 @@ class GetPlaylistsRequest private constructor(
@JvmStatic
fun fetchRequestIfNeeded(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
) {
Objects.requireNonNull(playlistId)
synchronized(cache) {
if (!cache.containsKey(playlistId)) {
cache[playlistId] = GetPlaylistsRequest(
playlistId,
playerHeaders
requestHeader,
dataSyncId,
)
}
}
@ -109,40 +106,28 @@ class GetPlaylistsRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? {
Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis()
// 'playlist/get_add_to_playlist' request does not require PoToken.
// '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 = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLISTS,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
GET_PLAYLISTS,
clientType,
requestHeader,
dataSyncId
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.getPlaylistsRequestBody(playlistId)
val requestBody = getPlaylistsRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -223,9 +208,10 @@ class GetPlaylistsRequest private constructor(
private fun fetch(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): Array<Pair<String, String>>? {
val json = sendRequest(playlistId, playerHeaders)
val json = sendRequest(playlistId, requestHeader, dataSyncId)
if (json != null) {
return parseResponse(json)
}

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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 app.revanced.extension.youtube.patches.utils.requests.SavePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -21,13 +22,15 @@ import java.util.concurrent.TimeoutException
class SavePlaylistRequest private constructor(
private val playlistId: String,
private val libraryId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch(
playlistId,
libraryId,
playerHeaders,
requestHeader,
dataSyncId,
)
}
@ -57,14 +60,6 @@ class SavePlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -88,14 +83,16 @@ class SavePlaylistRequest private constructor(
fun fetchRequestIfNeeded(
playlistId: String,
libraryId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
) {
Objects.requireNonNull(playlistId)
synchronized(cache) {
cache[libraryId] = SavePlaylistRequest(
playlistId,
libraryId,
playerHeaders
requestHeader,
dataSyncId,
)
}
}
@ -111,43 +108,30 @@ class SavePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
playlistId: String,
libraryId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? {
Objects.requireNonNull(playlistId)
Objects.requireNonNull(libraryId)
val startTime = System.currentTimeMillis()
// 'browse/edit_playlist' request does not require PoToken.
// '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 = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
EDIT_PLAYLIST,
clientType,
requestHeader,
dataSyncId
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.savePlaylistRequestBody(libraryId, playlistId)
val requestBody = savePlaylistRequestBody(libraryId, playlistId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -190,9 +174,15 @@ class SavePlaylistRequest private constructor(
private fun fetch(
playlistId: String,
libraryId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>,
dataSyncId: String,
): Boolean? {
val json = sendRequest(playlistId, libraryId,playerHeaders)
val json = sendRequest(
playlistId,
libraryId,
requestHeader,
dataSyncId,
)
if (json != null) {
return parseResponse(json)
}

View File

@ -1,17 +1,10 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class AV1CodecPatch {
private static final int LITERAL_VALUE_AV01 = 1635135811;
private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123;
private static final String VP9_CODEC = "video/x-vnd.on2.vp9";
private static long lastTimeResponse = 0;
/**
* Replace the SW AV01 codec to VP9 codec.
@ -22,32 +15,4 @@ public class AV1CodecPatch {
public static String replaceCodec(String original) {
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original;
}
/**
* Replace the SW AV01 codec request with a Dolby Vision codec request.
* This request is invalid, so it falls back to codecs other than AV01.
* <p>
* Limitation: Fallback process causes about 15-20 seconds of buffering.
*
* @param literalValue literal value of the codec
*/
public static int rejectResponse(int literalValue) {
if (!Settings.REJECT_AV1_CODEC.get())
return literalValue;
Logger.printDebug(() -> "Response: " + literalValue);
if (literalValue != LITERAL_VALUE_AV01)
return literalValue;
final long currentTime = System.currentTimeMillis();
// Ignore the invoke within 20 seconds.
if (currentTime - lastTimeResponse > 20000) {
lastTimeResponse = currentTime;
Utils.showToastShort(str("revanced_reject_av1_codec_toast"));
}
return LITERAL_VALUE_DOLBY_VISION;
}
}

View File

@ -75,30 +75,30 @@ public class CustomPlaybackSpeedPatch {
return isCustomPlaybackSpeedEnabled() ? 0 : original;
}
public static String[] getListEntries() {
public static String[] getEntries() {
return isCustomPlaybackSpeedEnabled()
? customSpeedEntries
: defaultSpeedEntries;
}
public static String[] getListEntryValues() {
public static String[] getEntryValues() {
return isCustomPlaybackSpeedEnabled()
? customSpeedEntryValues
: defaultSpeedEntryValues;
}
public static String[] getTrimmedListEntries() {
public static String[] getTrimmedEntries() {
if (playbackSpeedEntries == null) {
final String[] playbackSpeedWithAutoEntries = getListEntries();
final String[] playbackSpeedWithAutoEntries = getEntries();
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
}
return playbackSpeedEntries;
}
public static String[] getTrimmedListEntryValues() {
public static String[] getTrimmedEntryValues() {
if (playbackSpeedEntryValues == null) {
final String[] playbackSpeedWithAutoEntryValues = getListEntryValues();
final String[] playbackSpeedWithAutoEntryValues = getEntryValues();
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
}

View File

@ -1,12 +1,18 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import java.util.LinkedHashMap;
import java.util.Map;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
@ -17,25 +23,70 @@ import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused")
public class PlaybackSpeedPatch {
private static final FloatSetting DEFAULT_PLAYBACK_SPEED =
Settings.DEFAULT_PLAYBACK_SPEED;
private static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS =
Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC =
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
private static final long TOAST_DELAY_MILLISECONDS = 750;
private static long lastTimeSpeedChanged;
private static boolean isLiveStream;
/**
* The last used playback speed.
* This value is used when the default playback speed is 'Auto'.
*/
private static float lastSelectedPlaybackSpeed = 1.0f;
private static float lastSelectedShortsPlaybackSpeed = 1.0f;
/**
* The last regular video id.
*/
private static String videoId = "";
@GuardedBy("itself")
private static final Map<String, Float> ignoredPlaybackSpeedVideoIds = new LinkedHashMap<>() {
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 3;
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
}
};
/**
* Injection point.
* This method is used to reset the playback speed to 1.0 when a general video is started, whether it is a live stream, music, or whitelist.
*/
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
isLiveStream = newlyLoadedLiveStreamValue;
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
if (isShortsActive()) {
return;
}
if (videoId.equals(newlyLoadedVideoId)) {
return;
}
videoId = newlyLoadedVideoId;
final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId);
Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed);
boolean isMusic = isMusic(newlyLoadedVideoId);
boolean isWhitelisted = Whitelist.isChannelWhitelistedPlaybackSpeed(newlyLoadedVideoId);
VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed);
if (newlyLoadedLiveStreamValue || isMusic || isWhitelisted) {
synchronized(ignoredPlaybackSpeedVideoIds) {
if (!ignoredPlaybackSpeedVideoIds.containsKey(newlyLoadedVideoId)) {
lastSelectedPlaybackSpeed = 1.0f;
ignoredPlaybackSpeedVideoIds.put(newlyLoadedVideoId, lastSelectedPlaybackSpeed);
VideoInformation.setPlaybackSpeed(lastSelectedPlaybackSpeed);
VideoInformation.overridePlaybackSpeed(lastSelectedPlaybackSpeed);
Logger.printDebug(() -> "changing playback speed to: 1.0, isLiveStream: " + newlyLoadedLiveStreamValue +
", isMusic: " + isMusic + ", isWhitelisted: " + isWhitelisted);
}
}
}
}
/**
@ -64,18 +115,32 @@ public class PlaybackSpeedPatch {
/**
* Injection point.
* This method is called every second for regular videos and Shorts.
*/
public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
if (VideoInformation.lastPlayerResponseIsShort() &&
Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()
) {
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
public static float getPlaybackSpeed(float playbackSpeed) {
boolean isShorts = isShortsActive();
float defaultPlaybackSpeed = isShorts ? DEFAULT_PLAYBACK_SPEED_SHORTS.get() : DEFAULT_PLAYBACK_SPEED.get();
if (defaultPlaybackSpeed < 0) { // If the default playback speed is 'Auto', it will be overridden to the last used playback speed.
float finalPlaybackSpeed = isShorts ? lastSelectedShortsPlaybackSpeed : lastSelectedPlaybackSpeed;
VideoInformation.overridePlaybackSpeed(finalPlaybackSpeed);
Logger.printDebug(() -> "changing playback speed to: " + finalPlaybackSpeed);
return finalPlaybackSpeed;
} else { // Otherwise the default playback speed is used.
synchronized (ignoredPlaybackSpeedVideoIds) {
if (isShorts) {
// For Shorts, the VideoInformation.overridePlaybackSpeed() method is not used, so manually save the playback speed in VideoInformation.
VideoInformation.setPlaybackSpeed(defaultPlaybackSpeed);
} else if (ignoredPlaybackSpeedVideoIds.containsKey(videoId)) {
// For general videos, check whether the default video playback speed should not be applied.
Logger.printDebug(() -> "changing playback speed to: 1.0");
return 1.0f;
}
}
Logger.printDebug(() -> "changing playback speed to: " + defaultPlaybackSpeed);
return defaultPlaybackSpeed;
}
return playbackSpeed;
}
/**
@ -86,51 +151,78 @@ public class PlaybackSpeedPatch {
*/
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
try {
if (PatchStatus.RememberPlaybackSpeed() &&
Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
// then the menu will allow increasing without bounds but the max speed is
// still capped to under 8.0x.
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
boolean isShorts = isShortsActive();
// Prevent toast spamming if using the 0.05x adjustments.
// Show exactly one toast after the user stops interacting with the speed menu.
final long now = System.currentTimeMillis();
lastTimeSpeedChanged = now;
// Saves the user-selected playback speed in the method.
if (isShorts) {
lastSelectedShortsPlaybackSpeed = playbackSpeed;
} else {
lastSelectedPlaybackSpeed = playbackSpeed;
// If the user has manually changed the playback speed, the whitelist has already been applied.
// If there is a videoId on the map, it will be removed.
synchronized (ignoredPlaybackSpeedVideoIds) {
ignoredPlaybackSpeedVideoIds.remove(videoId);
}
}
final float finalPlaybackSpeed = playbackSpeed;
Utils.runOnMainThreadDelayed(() -> {
if (lastTimeSpeedChanged != now) {
// The user made additional speed adjustments and this call is outdated.
return;
}
if (PatchStatus.RememberPlaybackSpeed()) {
BooleanSetting rememberPlaybackSpeedLastSelectedSetting = isShorts
? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED
: Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED;
FloatSetting playbackSpeedSetting = isShorts
? DEFAULT_PLAYBACK_SPEED_SHORTS
: DEFAULT_PLAYBACK_SPEED;
BooleanSetting showToastSetting = isShorts
? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED_TOAST
: Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST;
if (Settings.DEFAULT_PLAYBACK_SPEED.get() == finalPlaybackSpeed) {
// User changed to a different speed and immediately changed back.
// Or the user is going past 8.0x in the glitched out 0.05x menu.
return;
}
Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed);
if (rememberPlaybackSpeedLastSelectedSetting.get()) {
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
// then the menu will allow increasing without bounds but the max speed is
// still capped to under 8.0x.
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) {
return;
}
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
}, TOAST_DELAY_MILLISECONDS);
// Prevent toast spamming if using the 0.05x adjustments.
// Show exactly one toast after the user stops interacting with the speed menu.
final long now = System.currentTimeMillis();
lastTimeSpeedChanged = now;
final float finalPlaybackSpeed = playbackSpeed;
Utils.runOnMainThreadDelayed(() -> {
if (lastTimeSpeedChanged != now) {
// The user made additional speed adjustments and this call is outdated.
return;
}
if (playbackSpeedSetting.get() == finalPlaybackSpeed) {
// User changed to a different speed and immediately changed back.
// Or the user is going past 8.0x in the glitched out 0.05x menu.
return;
}
playbackSpeedSetting.save(finalPlaybackSpeed);
if (showToastSetting.get()) {
Utils.showToastShort(str(isShorts ? "revanced_remember_playback_speed_toast_shorts" : "revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
}
}, TOAST_DELAY_MILLISECONDS);
}
}
} catch (Exception ex) {
Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex);
}
}
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId))
? 1.0f
: Settings.DEFAULT_PLAYBACK_SPEED.get();
/**
* Injection point.
*/
public static void onDismiss() {
synchronized (ignoredPlaybackSpeedVideoIds) {
ignoredPlaybackSpeedVideoIds.remove(videoId);
videoId = "";
}
}
private static boolean isMusic(@Nullable String videoId) {
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) {
private static boolean isMusic(String videoId) {
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && !videoId.isEmpty()) {
try {
MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import androidx.annotation.NonNull;
@ -14,8 +15,10 @@ import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused")
public class VideoQualityPatch {
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
private static final IntegerSetting shortsQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS;
private static final IntegerSetting shortsQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI_SHORTS;
private static final IntegerSetting videoQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
private static final IntegerSetting videoQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
@NonNull
public static String videoId = "";
@ -35,12 +38,11 @@ public class VideoQualityPatch {
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
return;
if (videoId.equals(newlyLoadedVideoId))
return;
videoId = newlyLoadedVideoId;
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
if (PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL &&
!videoId.equals(newlyLoadedVideoId)) {
videoId = newlyLoadedVideoId;
setVideoQuality(750);
}
}
/**
@ -53,42 +55,62 @@ public class VideoQualityPatch {
);
}
private static void setVideoQuality(final long delayMillis) {
final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE
? mobileQualitySetting.get()
: wifiQualitySetting.get();
private static void setVideoQuality(long delayMillis) {
boolean isShorts = isShortsActive();
IntegerSetting defaultQualitySetting = Utils.getNetworkType() == Utils.NetworkType.MOBILE
? isShorts ? shortsQualityMobile : videoQualityMobile
: isShorts ? shortsQualityWifi : videoQualityWifi;
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
return;
int defaultQuality = defaultQualitySetting.get();
Utils.runOnMainThreadDelayed(() -> {
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
VideoInformation.overrideVideoQuality(qualityToUseFinal);
}, delayMillis
);
if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
Utils.runOnMainThreadDelayed(() -> {
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
VideoInformation.overrideVideoQuality(qualityToUseFinal);
}, delayMillis
);
}
}
private static void userSelectedVideoQuality(final int defaultQuality) {
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get())
return;
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
return;
if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
final Utils.NetworkType networkType = Utils.getNetworkType();
String networkTypeMessage = networkType == Utils.NetworkType.MOBILE
? str("revanced_remember_video_quality_mobile")
: str("revanced_remember_video_quality_wifi");
final Utils.NetworkType networkType = Utils.getNetworkType();
if (isShortsActive()) {
if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED.get()) {
IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
? shortsQualityMobile
: shortsQualityWifi;
switch (networkType) {
case NONE -> {
Utils.showToastShort(str("revanced_remember_video_quality_none"));
return;
defaultQualitySetting.save(defaultQuality);
if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST.get()) {
Utils.showToastShort(str(
"revanced_remember_video_quality_toast_shorts",
networkTypeMessage, (defaultQuality + "p")
));
}
}
} else {
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) {
IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
? videoQualityMobile
: videoQualityWifi;
defaultQualitySetting.save(defaultQuality);
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
Utils.showToastShort(str(
"revanced_remember_video_quality_toast",
networkTypeMessage, (defaultQuality + "p")
));
}
}
}
case MOBILE -> mobileQualitySetting.save(defaultQuality);
default -> wifiQualitySetting.save(defaultQuality);
}
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
return;
Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p"));
}
}

View File

@ -2,9 +2,13 @@ package app.revanced.extension.youtube.patches.video.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_CATEGORY
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@ -124,12 +128,12 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_PLAYLIST_PAGE,
clientType
)
val requestBody =
PlayerRoutes.createApplicationRequestBody(
createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = "RD$videoId"
@ -168,12 +172,11 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_CATEGORY,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_CATEGORY,
clientType
)
val requestBody =
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
val requestBody = createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)

View File

@ -115,7 +115,7 @@ public class ReturnYouTubeDislike {
private static final Rect middleSeparatorBounds;
/**
* Left separator horizontal padding for Rolling Number layout.
* Horizontal padding between the left and middle separator.
*/
public static final int leftSeparatorShapePaddingPixels;
private static final ShapeDrawable leftSeparatorShape;
@ -131,7 +131,7 @@ public class ReturnYouTubeDislike {
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, dp);
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.4f, dp);
leftSeparatorShape = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds);

View File

@ -42,6 +42,7 @@ import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.Fullscr
import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior;
import app.revanced.extension.youtube.patches.swipe.SwipeControlsPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
@ -150,7 +151,6 @@ public class Settings extends BaseSettings {
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", PatchStatus.SplashAnimation(), true);
public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", TRUE, true);
public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
@ -159,6 +159,7 @@ public class Settings extends BaseSettings {
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true);
public static final BooleanSetting DISABLE_LAYOUT_UPDATES = new BooleanSetting("revanced_disable_layout_updates", false, true, "revanced_disable_layout_updates_user_dialog_message");
public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", FALSE, true, "revanced_disable_translucent_status_bar_user_dialog_message");
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message");
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));
@ -497,12 +498,13 @@ public class Settings extends BaseSettings {
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG = new BooleanSetting("revanced_shorts_custom_actions_speed_dialog", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true);
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true,
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true,
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
// Experimental Flags
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true);
@ -522,9 +524,15 @@ public class Settings extends BaseSettings {
public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_OVERLAY_ALTERNATIVE_UI = new BooleanSetting("revanced_swipe_overlay_alternative_ui", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_SHOW_CIRCULAR_OVERLAY = new BooleanSetting("revanced_swipe_show_circular_overlay", FALSE, true,
new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
public static final BooleanSetting SWIPE_OVERLAY_MINIMAL_STYLE = new BooleanSetting("revanced_swipe_overlay_minimal_style", FALSE, true,
new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true,
new SwipeControlsPatch.SwipeOverlayTextSizeAvailability());
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
@ -543,30 +551,38 @@ public class Settings extends BaseSettings {
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
// PreferenceScreen: Video
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
// PreferenceScreen: Video - Codec
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
// PreferenceScreen: Video - Playback speed
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
public static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS = new FloatSetting("revanced_default_playback_speed_shorts", -2.0f);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED));
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
// PreferenceScreen: Video - Video quality
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS = new IntegerSetting("revanced_default_video_quality_mobile_shorts", -2, true);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI_SHORTS = new IntegerSetting("revanced_default_video_quality_wifi_shorts", -2, true);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED));
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
// Experimental Flags
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE);
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
public static final BooleanSetting REJECT_AV1_CODEC = new BooleanSetting("revanced_reject_av1_codec", FALSE, true);
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
// PreferenceScreen: Miscellaneous
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
@ -618,24 +634,34 @@ public class Settings extends BaseSettings {
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
public static final FloatSetting SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
public static final FloatSetting SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
public static final FloatSetting SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
public static final FloatSetting SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
public static final FloatSetting SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
public static final FloatSetting SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
public static final FloatSetting SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
public static final FloatSetting SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
public static final FloatSetting SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
public static final FloatSetting SB_CATEGORY_UNSUBMITTED_OPACITY = new FloatSetting("sb_unsubmitted_opacity", 1.0f);
// SB Setting not exported
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference;
import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams;
import static app.revanced.extension.shared.settings.BaseSettings.SPOOF_STREAMING_DATA_TYPE;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
@ -9,6 +10,7 @@ import static app.revanced.extension.shared.utils.Utils.getChildView;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.shared.utils.Utils.showToastShort;
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED;
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE;
@ -56,12 +58,14 @@ import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings;
@ -117,9 +121,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else {
Setting.privateSetValueFromString(setting, listPreference.getValue());
}
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
}
if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
}
if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting);
@ -134,11 +142,8 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
if (!settingImportInProgress && !showingUserDialogMessage) {
final Context context = getActivity();
if (setting.userDialogMessage != null &&
mPreference instanceof SwitchPreference switchPreference &&
setting.defaultValue instanceof Boolean defaultValue &&
switchPreference.isChecked() != defaultValue) {
showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
if (setting.userDialogMessage != null && !prefIsSetToDefault(mPreference, setting)) {
showSettingUserDialogConfirmation(context, mPreference, setting);
} else if (setting.rebootApp) {
showRestartDialog(context);
}
@ -148,25 +153,56 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
}
};
private void showSettingUserDialogConfirmation(Context context, SwitchPreference switchPreference, BooleanSetting setting) {
/**
* @return If the preference is currently set to the default value of the Setting.
*/
private boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
Object defaultValue = setting.defaultValue;
if (pref instanceof SwitchPreference switchPref) {
return switchPref.isChecked() == (Boolean) defaultValue;
}
String defaultValueString = defaultValue.toString();
if (pref instanceof EditTextPreference editPreference) {
return editPreference.getText().equals(defaultValueString);
}
if (pref instanceof ListPreference listPref) {
return listPref.getValue().equals(defaultValueString);
}
throw new IllegalStateException("Must override method to handle "
+ "preference type: " + pref.getClass());
}
private void showSettingUserDialogConfirmation(Context context, Preference pref, Setting<?> setting) {
Utils.verifyOnMainThread();
showingUserDialogMessage = true;
assert setting.userDialogMessage != null;
new AlertDialog.Builder(context)
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
.setMessage(setting.userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
switchPreference.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
})
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
.setCancelable(false)
.show();
final StringRef userDialogMessage = setting.userDialogMessage;
if (context != null && userDialogMessage != null) {
showingUserDialogMessage = true;
new AlertDialog.Builder(context)
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
.setMessage(userDialogMessage.toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
// Restore whatever the setting was before the change.
if (setting instanceof BooleanSetting booleanSetting &&
pref instanceof SwitchPreference switchPreference) {
switchPreference.setChecked(booleanSetting.defaultValue);
} else if (setting instanceof EnumSetting<?> enumSetting &&
pref instanceof ListPreference listPreference) {
listPreference.setValue(enumSetting.defaultValue.toString());
updateListPreferenceSummary(listPreference, setting);
}
})
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
.setCancelable(false)
.show();
}
}
static PreferenceManager mPreferenceManager;
@ -304,9 +340,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else if (preference instanceof EditTextPreference editTextPreference) {
editTextPreference.setText(setting.get().toString());
} else if (preference instanceof ListPreference listPreference) {
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
}
if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
}
if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting);
@ -337,6 +377,7 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
@Override
public void onDestroy() {
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
Utils.resetLocalizedContext();
super.onDestroy();
}

View File

@ -6,6 +6,8 @@ import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.preference.Preference;
import android.preference.SwitchPreference;
import java.util.Date;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus;
@ -44,8 +46,10 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
AmbientModePreferenceLinks();
FullScreenPanelPreferenceLinks();
NavigationPreferenceLinks();
PatchInformationPreferenceLinks();
RYDPreferenceLinks();
SeekBarPreferenceLinks();
ShortsPreferenceLinks();
SpeedOverlayPreferenceLinks();
QuickActionsPreferenceLinks();
TabletLayoutLinks();
@ -142,6 +146,26 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
);
}
/**
* Set patch information preference summary
*/
private static void PatchInformationPreferenceLinks() {
Preference appNamePreference = mPreferenceManager.findPreference("revanced_app_name");
if (appNamePreference != null) {
appNamePreference.setSummary(ExtendedUtils.getAppLabel());
}
Preference appVersionPreference = mPreferenceManager.findPreference("revanced_app_version");
if (appVersionPreference != null) {
appVersionPreference.setSummary(ExtendedUtils.getAppVersionName());
}
Preference patchedDatePreference = mPreferenceManager.findPreference("revanced_patched_date");
if (patchedDatePreference != null) {
long patchedTime = PatchStatus.PatchedTime();
Date date = new Date(patchedTime);
patchedDatePreference.setSummary(date.toLocaleString());
}
}
/**
* Enable/Disable Preference related to RYD settings
*/
@ -186,6 +210,19 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
);
}
/**
* Enable/Disable Preference related to Shorts settings
*/
private static void ShortsPreferenceLinks() {
if (!PatchStatus.RememberPlaybackSpeed()) {
enableDisablePreferences(
true,
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG
);
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG.save(false);
}
}
/**
* Enable/Disable Preference related to Speed overlay settings
*/

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
import android.app.AlertDialog;
import android.content.Context;
@ -12,41 +13,51 @@ import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.GridLayout;
import android.widget.TextView;
import java.util.Locale;
import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
@SuppressWarnings({"unused", "deprecation"})
public class SegmentCategoryListPreference extends ListPreference {
private SegmentCategory mCategory;
private EditText mEditText;
private int mClickedDialogEntryIndex;
private SegmentCategory category;
private TextView colorDotView;
private EditText colorEditText;
private EditText opacityEditText;
/**
* #RRGGBB
*/
private int categoryColor;
/**
* [0, 1]
*/
private float categoryOpacity;
private int selectedDialogEntryIndex;
private void init() {
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT;
mCategory = Objects.requireNonNull(segmentCategory);
category = Objects.requireNonNull(segmentCategory);
// Edit: Using preferences to sync together multiple pieces
// of code together is messy and should be rethought.
// of code is messy and should be rethought.
setKey(segmentCategory.behaviorSetting.key);
setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
setEntries(isHighlightCategory
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
: CategoryBehaviour.getBehaviorDescriptions());
setEntryValues(isHighlightCategory
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
: CategoryBehaviour.getBehaviorKeyValues());
updateTitle();
updateTitleFromCategory();
}
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@ -73,28 +84,41 @@ public class SegmentCategoryListPreference extends ListPreference {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
super.onPrepareDialogBuilder(builder);
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
Context context = builder.getContext();
TableLayout table = new TableLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(70, 0, 150, 0);
TableRow row = new TableRow(context);
GridLayout gridLayout = new GridLayout(context);
gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout.
gridLayout.setColumnCount(3);
gridLayout.setRowCount(2);
GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row.
gridParams.columnSpec = GridLayout.spec(0); // First column.
TextView colorTextLabel = new TextView(context);
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
row.addView(colorTextLabel);
colorTextLabel.setLayoutParams(gridParams);
gridLayout.addView(colorTextLabel);
TextView colorDotView = new TextView(context);
colorDotView.setText(mCategory.getCategoryColorDot());
colorDotView.setPadding(30, 0, 30, 0);
row.addView(colorDotView);
gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row.
gridParams.columnSpec = GridLayout.spec(1); // Second column.
gridParams.setMargins(0, 0, 10, 0);
colorDotView = new TextView(context);
colorDotView.setLayoutParams(gridParams);
gridLayout.addView(colorDotView);
updateCategoryColorDot();
mEditText = new EditText(context);
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
mEditText.setText(mCategory.colorString());
mEditText.addTextChangedListener(new TextWatcher() {
gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row.
gridParams.columnSpec = GridLayout.spec(2); // Third column.
colorEditText = new EditText(context);
colorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
colorEditText.setTextLocale(Locale.US);
colorEditText.setText(category.getColorString());
colorEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@ -104,44 +128,111 @@ public class SegmentCategoryListPreference extends ListPreference {
}
@Override
public void afterTextChanged(Editable s) {
public void afterTextChanged(Editable edit) {
try {
String colorString = s.toString();
String colorString = edit.toString();
final int colorStringLength = colorString.length();
if (!colorString.startsWith("#")) {
s.insert(0, "#"); // recursively calls back into this method
edit.insert(0, "#"); // Recursively calls back into this method.
return;
}
if (colorString.length() > 7) {
s.delete(7, colorString.length());
final int maxColorStringLength = 7; // #RRGGBB
if (colorStringLength > maxColorStringLength) {
edit.delete(maxColorStringLength, colorStringLength);
return;
}
final int color = Color.parseColor(colorString);
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
categoryColor = Color.parseColor(colorString);
updateCategoryColorDot();
} catch (IllegalArgumentException ex) {
// ignore
// Ignore.
}
}
});
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(mEditText);
colorEditText.setLayoutParams(gridParams);
gridLayout.addView(colorEditText);
table.addView(row);
builder.setView(table);
builder.setTitle(mCategory.title.toString());
gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(1); // Second row.
gridParams.columnSpec = GridLayout.spec(0, 1); // First and second column.
TextView opacityLabel = new TextView(context);
opacityLabel.setText(str("revanced_sb_color_opacity_label"));
opacityLabel.setLayoutParams(gridParams);
gridLayout.addView(opacityLabel);
gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(1); // Second row.
gridParams.columnSpec = GridLayout.spec(2); // Third column.
opacityEditText = new EditText(context);
opacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
opacityEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable edit) {
try {
String editString = edit.toString();
final int opacityStringLength = editString.length();
final int maxOpacityStringLength = 4; // [0.00, 1.00]
if (opacityStringLength > maxOpacityStringLength) {
edit.delete(maxOpacityStringLength, opacityStringLength);
return;
}
final float opacity = opacityStringLength == 0
? 0
: Float.parseFloat(editString);
if (opacity < 0) {
categoryOpacity = 0;
edit.replace(0, opacityStringLength, "0");
return;
} else if (opacity > 1.0f) {
categoryOpacity = 1;
edit.replace(0, opacityStringLength, "1.0");
return;
} else if (!editString.endsWith(".")) {
// Ignore "0." and "1." until the user finishes entering a valid number.
categoryOpacity = opacity;
}
updateCategoryColorDot();
} catch (NumberFormatException ex) {
// Should never happen.
Logger.printException(() -> "Could not parse opacity string", ex);
}
}
});
opacityEditText.setLayoutParams(gridParams);
gridLayout.addView(opacityEditText);
updateOpacityText();
builder.setView(gridLayout);
builder.setTitle(category.title.toString());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
try {
mCategory.resetColor();
updateTitle();
category.resetColorAndOpacity();
updateTitleFromCategory();
Utils.showToastShort(str("revanced_sb_color_reset"));
} catch (Exception ex) {
Logger.printException(() -> "setNeutralButton failure", ex);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
mClickedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
selectedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), selectedDialogEntryIndex,
(dialog, which) -> selectedDialogEntryIndex = which);
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
@ -150,31 +241,50 @@ public class SegmentCategoryListPreference extends ListPreference {
@Override
protected void onDialogClosed(boolean positiveResult) {
try {
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[selectedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
SegmentCategory.updateEnabledCategories();
}
String colorString = mEditText.getText().toString();
try {
if (!colorString.equals(mCategory.colorString())) {
mCategory.setColor(colorString);
String colorString = colorEditText.getText().toString();
if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) {
category.setColor(colorString);
category.setOpacity(categoryOpacity);
Utils.showToastShort(str("revanced_sb_color_changed"));
}
} catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_sb_color_invalid"));
}
updateTitle();
updateTitleFromCategory();
}
} catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex);
}
}
private void updateTitle() {
setTitle(mCategory.getTitleWithColorDot());
setEnabled(Settings.SB_ENABLED.get());
private void applyOpacityToCategoryColor() {
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
}
private void updateTitleFromCategory() {
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
applyOpacityToCategoryColor();
setTitle(category.getTitleWithColorDot(categoryColor));
}
private void updateCategoryColorDot() {
applyOpacityToCategoryColor();
colorDotView.setText(SegmentCategory.getCategoryColorDot(categoryColor));
}
private void updateOpacityText() {
opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
}
}

View File

@ -233,6 +233,7 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
statsCategory = new PreferenceCategory(mActivity);
statsCategory.setLayoutResource(preferencesCategoryLayout);
statsCategory.setTitle(str("revanced_sb_stats"));
statsCategory.setEnabled(Settings.SB_ENABLED.get());
mPreferenceScreen.addPreference(statsCategory);
fetchAndDisplayStats();
@ -261,7 +262,6 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
final String key = category.keyValue;
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
}
}
} catch (Exception ex) {

View File

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

View File

@ -48,4 +48,12 @@ enum class ShortsPlayerState {
fun isClosed(): Boolean {
return this == CLOSED
}
/**
* Check if the shorts player is [OPEN].
* Useful for checking if a shorts player is open.
*/
fun isOpen(): Boolean {
return this == OPEN
}
}

View File

@ -139,7 +139,7 @@ public class SponsorBlockSettings {
for (SegmentCategory category : categories) {
JSONObject categoryObject = new JSONObject();
String categoryKey = category.keyValue;
categoryObject.put("color", category.colorString());
categoryObject.put("color", category.getColorString());
barTypesObject.put(categoryKey, categoryObject);
if (category.behaviour != CategoryBehaviour.IGNORE) {

View File

@ -6,7 +6,12 @@ import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.Html;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.widget.EditText;
import androidx.annotation.NonNull;
@ -32,11 +37,9 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController
/**
* Not thread safe. All fields/methods must be accessed from the main thread.
*
* @noinspection deprecation
*/
public class SponsorBlockUtils {
private static final String LOCKED_COLOR = "#FFC83D";
private static final int LOCKED_COLOR = Color.parseColor("#FFC83D");
private static final String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss";
private static final Pattern manualEditTimePattern
@ -162,28 +165,34 @@ public class SponsorBlockUtils {
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
: SegmentVote.values();
CharSequence[] items = new CharSequence[voteOptions.length];
final int voteOptionsLength = voteOptions.length;
final boolean userIsVip = Settings.SB_USER_IS_VIP.get();
CharSequence[] items = new CharSequence[voteOptionsLength];
for (int i = 0; i < voteOptions.length; i++) {
for (int i = 0; i < voteOptionsLength; i++) {
SegmentVote voteOption = voteOptions[i];
String title = voteOption.title.toString();
if (Settings.SB_USER_IS_VIP.get() && segment.isLocked && voteOption.shouldHighlight) {
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title));
} else {
items[i] = title;
CharSequence title = voteOption.title.toString();
if (userIsVip && segment.isLocked && voteOption.highlightIfVipAndVideoIsLocked) {
SpannableString coloredTitle = new SpannableString(title);
coloredTitle.setSpan(new ForegroundColorSpan(LOCKED_COLOR),
0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
title = coloredTitle;
}
items[i] = title;
}
new AlertDialog.Builder(context)
.setItems(items, (dialog1, which1) -> {
SegmentVote voteOption = voteOptions[which1];
switch (voteOption) {
case UPVOTE, DOWNVOTE ->
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
case CATEGORY_CHANGE -> onNewCategorySelect(segment, context);
}
})
.show();
new AlertDialog.Builder(context).setItems(items, (dialog1, which1) -> {
SegmentVote voteOption = voteOptions[which1];
switch (voteOption) {
case UPVOTE:
case DOWNVOTE:
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
break;
case CATEGORY_CHANGE:
onNewCategorySelect(segment, context);
break;
}
}).show();
} catch (Exception ex) {
Logger.printException(() -> "segmentVoteClickListener failure", ex);
}
@ -287,22 +296,33 @@ public class SponsorBlockUtils {
if (segment.category == SegmentCategory.UNSUBMITTED) {
continue;
}
StringBuilder htmlBuilder = new StringBuilder();
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br>",
segment.category.color, segment.category.title));
htmlBuilder.append(formatSegmentTime(segment.start));
if (segment.category != SegmentCategory.HIGHLIGHT) {
htmlBuilder.append(" to ").append(formatSegmentTime(segment.end));
SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
spannableBuilder.append(segment.category.getTitleWithColorDot());
spannableBuilder.append('\n');
String startTime = formatSegmentTime(segment.start);
if (segment.category == SegmentCategory.HIGHLIGHT) {
spannableBuilder.append(startTime);
} else {
String toFromString = str("revanced_sb_vote_segment_time_to_from",
startTime, formatSegmentTime(segment.end));
spannableBuilder.append(toFromString);
}
htmlBuilder.append("</b>");
if (i + 1 != numberOfSegments) // prevents trailing new line after last segment
htmlBuilder.append("<br>");
titles[i] = Html.fromHtml(htmlBuilder.toString());
if (i + 1 != numberOfSegments) {
// prevents trailing new line after last segment
spannableBuilder.append('\n');
}
spannableBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
0, spannableBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
titles[i] = spannableBuilder;
}
new AlertDialog.Builder(context)
.setItems(titles, segmentVoteClickListener)
.show();
new AlertDialog.Builder(context).setItems(titles, segmentVoteClickListener).show();
} catch (Exception ex) {
Logger.printException(() -> "onVotingClicked failure", ex);
}

View File

@ -3,30 +3,41 @@ package app.revanced.extension.youtube.sponsorblock.objects;
import static app.revanced.extension.shared.utils.StringRef.sf;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_OPACITY;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Html;
import android.text.Spanned;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -34,45 +45,46 @@ import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "StaticFieldLeak"})
@SuppressWarnings("StaticFieldLeak")
public enum SegmentCategory {
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR),
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR, SB_CATEGORY_SPONSOR_OPACITY),
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR),
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR, SB_CATEGORY_SELF_PROMO_OPACITY),
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR),
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR, SB_CATEGORY_INTERACTION_OPACITY),
/**
* Unique category that is treated differently than the rest.
*/
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR),
SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR, SB_CATEGORY_HIGHLIGHT_OPACITY),
INTRO("intro", sf("revanced_sb_segments_intro"),
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR),
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR, SB_CATEGORY_INTRO_OPACITY),
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR),
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR, SB_CATEGORY_OUTRO_OPACITY),
PREVIEW("preview", sf("revanced_sb_segments_preview"),
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR),
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR, SB_CATEGORY_PREVIEW_OPACITY),
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR),
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR, SB_CATEGORY_FILLER_OPACITY),
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR),
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY),
UNSUBMITTED("unsubmitted", StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR),
;
SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR, SB_CATEGORY_UNSUBMITTED_OPACITY);
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
@ -111,12 +123,10 @@ public enum SegmentCategory {
mValuesMap.put(value.keyValue, value);
}
@NonNull
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted;
}
@NonNull
public static SegmentCategory[] categoriesWithoutHighlights() {
return categoriesWithoutHighlights;
}
@ -127,7 +137,7 @@ public enum SegmentCategory {
}
/**
* Must be called if behavior of any category is changed
* Must be called if behavior of any category is changed.
*/
public static void updateEnabledCategories() {
Utils.verifyOnMainThread();
@ -154,30 +164,32 @@ public enum SegmentCategory {
updateEnabledCategories();
}
@NonNull
public final String keyValue;
@NonNull
public final StringSetting behaviorSetting;
@NonNull
private final StringSetting colorSetting;
public static int applyOpacityToColor(int color, float opacity) {
if (opacity < 0 || opacity > 1.0f) {
throw new IllegalArgumentException("Invalid opacity: " + opacity);
}
final int opacityInt = (int) (255 * opacity);
return (color & 0x00FFFFFF) | (opacityInt << 24);
}
public final String keyValue;
public final StringSetting behaviorSetting; // TODO: Replace with EnumSetting.
private final StringSetting colorSetting;
private final FloatSetting opacitySetting;
@NonNull
public final StringRef title;
/**
* Skip button text, if the skip occurs in the first quarter of the video
*/
@NonNull
public final StringRef skipButtonTextBeginning;
/**
* Skip button text, if the skip occurs in the middle half of the video
*/
@NonNull
public final StringRef skipButtonTextMiddle;
/**
* Skip button text, if the skip occurs in the last quarter of the video
*/
@NonNull
public final StringRef skipButtonTextEnd;
/**
* Skipped segment toast, if the skip occurred in the first quarter of the video
@ -198,10 +210,7 @@ public enum SegmentCategory {
@NonNull
public final Paint paint;
/**
* Value must be changed using {@link #setColor(String)}.
*/
public int color;
private int color;
/**
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
@ -213,17 +222,20 @@ public enum SegmentCategory {
SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonText,
StringRef skippedToastText,
StringSetting behavior, StringSetting color) {
StringSetting behavior,
StringSetting color, FloatSetting opacity) {
this(keyValue, title,
skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText,
behavior, color);
behavior,
color, opacity);
}
SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
StringSetting behavior, StringSetting color) {
StringSetting behavior,
StringSetting color, FloatSetting opacity) {
this.keyValue = Objects.requireNonNull(keyValue);
this.title = Objects.requireNonNull(title);
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
@ -234,6 +246,7 @@ public enum SegmentCategory {
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviorSetting = Objects.requireNonNull(behavior);
this.colorSetting = Objects.requireNonNull(color);
this.opacitySetting = Objects.requireNonNull(opacity);
this.paint = new Paint();
loadFromSettings();
}
@ -250,11 +263,14 @@ public enum SegmentCategory {
this.behaviour = savedBehavior;
String colorString = colorSetting.get();
final float opacity = opacitySetting.get();
try {
setColor(colorString);
setOpacity(opacity);
} catch (Exception ex) {
Logger.printException(() -> "Invalid color: " + colorString, ex);
Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
colorSetting.resetToDefault();
opacitySetting.resetToDefault();
loadFromSettings();
}
}
@ -264,45 +280,77 @@ public enum SegmentCategory {
this.behaviorSetting.save(behaviour.reVancedKeyValue);
}
/**
* @return HTML color format string
*/
@NonNull
public String colorString() {
return String.format("#%06X", color);
}
public void setColor(@NonNull String colorString) throws IllegalArgumentException {
final int color = Color.parseColor(colorString) & 0xFFFFFF;
this.color = color;
private void updateColor() {
color = applyOpacityToColor(color, opacitySetting.get());
paint.setColor(color);
paint.setAlpha(255);
colorSetting.save(colorString); // Save after parsing.
}
public void resetColor() {
/**
* @param opacity Segment color opacity between [0, 1].
*/
public void setOpacity(float opacity) throws IllegalArgumentException {
if (opacity < 0 || opacity > 1) {
throw new IllegalArgumentException("Invalid opacity: " + opacity);
}
opacitySetting.save(opacity);
updateColor();
}
public float getOpacity() {
return opacitySetting.get();
}
public void resetColorAndOpacity() {
setColor(colorSetting.defaultValue);
setOpacity(opacitySetting.defaultValue);
}
@NonNull
private static String getCategoryColorDotHTML(int color) {
color &= 0xFFFFFF;
return String.format("<font color=\"#%06X\">⬤</font>", color);
/**
* @param colorString Segment color with #RRGGBB format.
*/
public void setColor(String colorString) throws IllegalArgumentException {
color = Color.parseColor(colorString);
colorSetting.save(colorString);
updateColor();
}
@NonNull
public static Spanned getCategoryColorDot(int color) {
return Html.fromHtml(getCategoryColorDotHTML(color));
/**
* @return Integer color of #RRGGBB format.
*/
public int getColorNoOpacity() {
return color & 0x00FFFFFF;
}
@NonNull
public Spanned getCategoryColorDot() {
/**
* @return Hex color string of #RRGGBB format with no opacity level.
*/
public String getColorString() {
return String.format(Locale.US, "#%06X", getColorNoOpacity());
}
private static SpannableString getCategoryColorDotSpan(String text, int color) {
SpannableString dotSpan = new SpannableString('⬤' + text);
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return dotSpan;
}
public static SpannableString getCategoryColorDot(int color) {
return getCategoryColorDotSpan("", color);
}
public SpannableString getCategoryColorDot() {
return getCategoryColorDot(color);
}
@NonNull
public Spanned getTitleWithColorDot() {
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title);
public SpannableString getTitleWithColorDot(int categoryColor) {
return getCategoryColorDotSpan(" " + title, categoryColor);
}
public SpannableString getTitleWithColorDot() {
return getTitleWithColorDot(color);
}
/**
@ -310,7 +358,6 @@ public enum SegmentCategory {
* @param videoLength length of the video
* @return the skip button text
*/
@NonNull
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
return (this == SegmentCategory.HIGHLIGHT)
@ -319,7 +366,7 @@ public enum SegmentCategory {
}
if (videoLength == 0) {
return skipButtonTextBeginning; // video is still loading. Assume it's the beginning
return skipButtonTextBeginning; // Video is still loading. Assume it's the beginning.
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
@ -335,10 +382,9 @@ public enum SegmentCategory {
* @param videoLength length of the video
* @return 'skipped segment' toast message
*/
@NonNull
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
return skippedToastBeginning; // video is still loading. Assume it's the beginning
return skippedToastBeginning; // Video is still loading. Assume it's the beginning.
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {

View File

@ -24,12 +24,15 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
@NonNull
public final StringRef title;
public final int apiVoteType;
public final boolean shouldHighlight;
/**
* If the option should be highlighted for VIP users.
*/
public final boolean highlightIfVipAndVideoIsLocked;
SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) {
SegmentVote(@NonNull StringRef title, int apiVoteType, boolean highlightIfVipAndVideoIsLocked) {
this.title = title;
this.apiVoteType = apiVoteType;
this.shouldHighlight = shouldHighlight;
this.highlightIfVipAndVideoIsLocked = highlightIfVipAndVideoIsLocked;
}
}

View File

@ -125,7 +125,7 @@ class SwipeControlsConfigurationProvider(
* get the background color for text on the overlay, as a color int
*/
val overlayTextBackgroundColor: Int
get() = Color.argb(Settings.SWIPE_OVERLAY_BACKGROUND_ALPHA.get(), 0, 0, 0)
get() = overlayBackgroundOpacity
/**
* get the foreground color for text on the overlay, as a color int
@ -133,6 +133,59 @@ class SwipeControlsConfigurationProvider(
val overlayForegroundColor: Int
get() = Color.WHITE
/**
* Gets the opacity value (0-100%) is converted to an alpha value (0-255) for transparency.
* If the opacity value is out of range, it resets to the default and displays a warning message.
*/
val overlayBackgroundOpacity: Int
get() {
var opacity = validateValue(
Settings.SWIPE_OVERLAY_BACKGROUND_OPACITY,
0,
100,
"revanced_swipe_overlay_background_opacity_invalid_toast"
)
opacity = opacity * 255 / 100
return Color.argb(opacity, 0, 0, 0)
}
/**
* The color of the progress overlay.
*/
val overlayProgressColor: Int
get() = 0xBFFFFFFF.toInt()
/**
* The color used for the background of the progress overlay fill.
*/
val overlayFillBackgroundPaint: Int
get() = 0x80D3D3D3.toInt()
/**
* The color used for the text and icons in the overlay.
*/
val overlayTextColor: Int
get() = Color.WHITE
/**
* A flag that determines whether to use the alternate UI.
*/
val isAlternativeUI: Boolean
get() = Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get()
/**
* A flag that determines if the overlay should only show the icon.
*/
val overlayShowOverlayMinimalStyle: Boolean
get() = isAlternativeUI && Settings.SWIPE_OVERLAY_MINIMAL_STYLE.get()
/**
* A flag that determines if the progress bar should be circular.
*/
val isCircularProgressBar: Boolean
get() = isAlternativeUI && Settings.SWIPE_SHOW_CIRCULAR_OVERLAY.get()
// endregion
// region behaviour

View File

@ -24,7 +24,7 @@ import java.lang.ref.WeakReference
* The main controller for volume and brightness swipe controls.
* note that the superclass is overwritten to the superclass of the MainActivity at patch time
*
* @smali Lapp/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity;
* @smali Lapp/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity;
*/
class SwipeControlsHostActivity : Activity() {
/**

View File

@ -1,14 +1,18 @@
package app.revanced.extension.youtube.swipecontrols.views
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.TextView
import app.revanced.extension.shared.utils.ResourceUtils.ResourceType
@ -17,6 +21,7 @@ import app.revanced.extension.shared.utils.StringRef.str
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider
import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay
import app.revanced.extension.youtube.swipecontrols.misc.applyDimension
import kotlin.math.min
import kotlin.math.round
/**
@ -33,36 +38,82 @@ class SwipeControlsOverlayLayout(
*/
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
private val feedbackTextView: TextView
private val autoBrightnessIcon: Drawable
private val lowBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_low")
private val mediumBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_medium")
private val highBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_high")
private val fullBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_full")
private val manualBrightnessIcon: Drawable
private val mutedVolumeIcon: Drawable
private val lowVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_low")
private val normalVolumeIcon: Drawable
private val feedbackTextView: TextView
private val fullVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_high")
private fun getDrawable(name: String, width: Int, height: Int): Drawable {
return resources.getDrawable(
private val circularProgressView: CircularProgressView = CircularProgressView(
context,
config.overlayBackgroundOpacity,
config.overlayShowOverlayMinimalStyle,
config.overlayProgressColor,
config.overlayFillBackgroundPaint,
config.overlayTextColor
).apply {
layoutParams = LayoutParams(300, 300).apply {
addRule(CENTER_IN_PARENT, TRUE)
}
visibility = GONE // Initially hidden
}
private val horizontalProgressView: HorizontalProgressView
private val isAlternativeUI: Boolean = config.isAlternativeUI
private fun getDrawable(name: String, width: Int? = null, height: Int? = null): Drawable {
val drawable = resources.getDrawable(
getIdentifier(name, ResourceType.DRAWABLE, context),
context.theme
).apply {
setTint(config.overlayForegroundColor)
setBounds(
context.theme,
)
if (width != null && height != null) {
drawable.setTint(config.overlayForegroundColor)
drawable.setBounds(
0,
0,
width,
height,
)
} else {
drawable.setTint(config.overlayTextColor)
}
return drawable
}
init {
// Initialize horizontal progress bar
val screenWidth = resources.displayMetrics.widthPixels
val layoutWidth = (screenWidth * 2 / 3).toInt() // 2/3 of screen width
horizontalProgressView = HorizontalProgressView(
context,
config.overlayBackgroundOpacity,
config.overlayShowOverlayMinimalStyle,
config.overlayProgressColor,
config.overlayFillBackgroundPaint,
config.overlayTextColor
).apply {
layoutParams = LayoutParams(layoutWidth, 100).apply {
addRule(CENTER_HORIZONTAL)
topMargin = 40 // Top margin
}
visibility = GONE // Initially hidden
}
// init views
val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
feedbackTextView = TextView(context).apply {
layoutParams = LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT,
).apply {
addRule(CENTER_IN_PARENT, TRUE)
setPadding(
@ -81,19 +132,36 @@ class SwipeControlsOverlayLayout(
compoundDrawablePadding = compoundIconPadding
visibility = GONE
}
addView(feedbackTextView)
// get icons scaled, assuming square icons
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight)
manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight)
normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight)
if (isAlternativeUI) {
addView(circularProgressView)
addView(horizontalProgressView)
autoBrightnessIcon = getDrawable("revanced_ic_sc_brightness_auto")
manualBrightnessIcon = getDrawable("revanced_ic_sc_brightness_manual")
mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute")
normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal")
} else {
addView(feedbackTextView)
// get icons scaled, assuming square icons
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
autoBrightnessIcon =
getDrawable("revanced_ic_sc_brightness_auto", iconHeight, iconHeight)
manualBrightnessIcon =
getDrawable("revanced_ic_sc_brightness_manual", iconHeight, iconHeight)
mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute", iconHeight, iconHeight)
normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal", iconHeight, iconHeight)
}
}
private val feedbackHideHandler = Handler(Looper.getMainLooper())
private val feedbackHideCallback = Runnable {
feedbackTextView.visibility = View.GONE
if (isAlternativeUI) {
circularProgressView.visibility = GONE
horizontalProgressView.visibility = GONE
} else {
feedbackTextView.visibility = GONE
}
}
/**
@ -117,21 +185,81 @@ class SwipeControlsOverlayLayout(
}
}
/**
* Displays the progress bar with the appropriate value, icon, and type (brightness or volume).
*/
private fun showFeedbackView(
value: String,
progress: Int,
max: Int,
icon: Drawable,
isBrightness: Boolean
) {
feedbackHideHandler.removeCallbacks(feedbackHideCallback)
feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis)
val viewToShow =
if (config.isCircularProgressBar) circularProgressView else horizontalProgressView
viewToShow.apply {
setProgress(progress, max, value, isBrightness)
this.icon = icon
visibility = VISIBLE
}
}
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
showFeedbackView(
"$newVolume",
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
)
if (isAlternativeUI) {
val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
val icon = when {
newVolume == 0 -> mutedVolumeIcon
volumePercentage < 33 -> lowVolumeIcon
volumePercentage < 66 -> normalVolumeIcon
else -> fullVolumeIcon
}
showFeedbackView("$newVolume", newVolume, maximumVolume, icon, isBrightness = false)
} else {
showFeedbackView(
"$newVolume",
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
)
}
}
override fun onBrightnessChanged(brightness: Double) {
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
showFeedbackView(
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
autoBrightnessIcon,
)
if (isAlternativeUI) {
showFeedbackView(
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
0,
100,
autoBrightnessIcon,
isBrightness = true,
)
} else {
showFeedbackView(
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
autoBrightnessIcon,
)
}
} else if (brightness >= 0) {
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
if (isAlternativeUI) {
val brightnessValue = round(brightness).toInt()
val icon = when {
brightnessValue < 25 -> lowBrightnessIcon
brightnessValue < 50 -> mediumBrightnessIcon
brightnessValue < 75 -> highBrightnessIcon
else -> fullBrightnessIcon
}
showFeedbackView(
"$brightnessValue%",
brightnessValue,
100,
icon,
isBrightness = true
)
} else {
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
}
}
}
@ -145,3 +273,255 @@ class SwipeControlsOverlayLayout(
}
}
}
/**
* Abstract base class for progress views.
*/
abstract class AbstractProgressView(
context: Context,
overlayBackgroundOpacity: Int,
protected val overlayShowOverlayMinimalStyle: Boolean,
overlayProgressColor: Int,
overlayFillBackgroundPaint: Int,
protected val overlayTextColor: Int,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// Combined paint creation function for both fill and stroke styles
private fun createPaint(
color: Int,
style: Paint.Style = Paint.Style.FILL,
strokeCap: Paint.Cap = Paint.Cap.BUTT,
strokeWidth: Float = 0f
) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.style = style
this.color = color
this.strokeCap = strokeCap
this.strokeWidth = strokeWidth
}
// Initialize paints
val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL)
val progressPaint = createPaint(
overlayProgressColor,
style = Paint.Style.STROKE,
strokeCap = Paint.Cap.ROUND,
strokeWidth = 20f
)
val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL)
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = overlayTextColor
textAlign = Paint.Align.CENTER
textSize = 40f // Can adjust based on need
}
protected var progress = 0
protected var maxProgress = 100
protected var displayText: String = "0"
protected var isBrightness = true
var icon: Drawable? = null
fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) {
progress = value
maxProgress = max
displayText = text
isBrightness = isBrightnessMode
invalidate()
}
override fun onDraw(canvas: Canvas) {
// Base class implementation can be empty
}
}
/**
* Custom view for rendering a circular progress indicator with icons and text.
*/
@SuppressLint("ViewConstructor")
class CircularProgressView(
context: Context,
overlayBackgroundOpacity: Int,
overlayShowOverlayMinimalStyle: Boolean,
overlayProgressColor: Int,
overlayFillBackgroundPaint: Int,
overlayTextColor: Int,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractProgressView(
context,
overlayBackgroundOpacity,
overlayShowOverlayMinimalStyle,
overlayProgressColor,
overlayFillBackgroundPaint,
overlayTextColor,
attrs,
defStyleAttr
) {
private val rectF = RectF()
init {
textPaint.textSize = 40f // Override default text size for circular view
progressPaint.strokeWidth = 20f
fillBackgroundPaint.strokeWidth = 20f
progressPaint.strokeCap = Paint.Cap.ROUND
fillBackgroundPaint.strokeCap = Paint.Cap.BUTT
progressPaint.style = Paint.Style.STROKE
fillBackgroundPaint.style = Paint.Style.STROKE
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = min(width, height).toFloat()
rectF.set(20f, 20f, size - 20f, size - 20f)
canvas.drawOval(rectF, fillBackgroundPaint) // Draw the outer ring.
canvas.drawCircle(
width / 2f,
height / 2f,
size / 3,
backgroundPaint
) // Draw the inner circle.
// Select the paint for drawing based on whether it's brightness or volume.
val sweepAngle = (progress.toFloat() / maxProgress) * 360
canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint) // Draw the progress arc.
// Draw the icon in the center.
icon?.let {
val iconSize = if (overlayShowOverlayMinimalStyle) 100 else 80
val iconX = (width - iconSize) / 2
val iconY = (height / 2) - if (overlayShowOverlayMinimalStyle) 50 else 80
it.setBounds(iconX, iconY, iconX + iconSize, iconY + iconSize)
it.draw(canvas)
}
// If not a minimal style mode, draw the text inside the ring.
if (!overlayShowOverlayMinimalStyle) {
canvas.drawText(displayText, width / 2f, height / 2f + 60f, textPaint)
}
}
}
/**
* Custom view for rendering a rectangular progress bar with icons and text.
*/
@SuppressLint("ViewConstructor")
class HorizontalProgressView(
context: Context,
overlayBackgroundOpacity: Int,
overlayShowOverlayMinimalStyle: Boolean,
overlayProgressColor: Int,
overlayFillBackgroundPaint: Int,
overlayTextColor: Int,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractProgressView(
context,
overlayBackgroundOpacity,
overlayShowOverlayMinimalStyle,
overlayProgressColor,
overlayFillBackgroundPaint,
overlayTextColor,
attrs,
defStyleAttr
) {
private val iconSize = 60f
private val padding = 40f
init {
textPaint.textSize = 36f // Override default text size for horizontal view
progressPaint.strokeWidth = 0f
progressPaint.strokeCap = Paint.Cap.BUTT
progressPaint.style = Paint.Style.FILL
fillBackgroundPaint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = width.toFloat()
val height = height.toFloat()
// Radius for rounded corners
val cornerRadius = min(width, height) / 2
// Calculate the total width for the elements
val minimalElementWidth = 5 * padding + iconSize
// Calculate the starting point (X) to center the elements
val minimalStartX = (width - minimalElementWidth) / 2
// Draw the background
if (!overlayShowOverlayMinimalStyle) {
canvas.drawRoundRect(0f, 0f, width, height, cornerRadius, cornerRadius, backgroundPaint)
} else {
canvas.drawRoundRect(
minimalStartX,
0f,
minimalStartX + minimalElementWidth,
height,
cornerRadius,
cornerRadius,
backgroundPaint
)
}
if (!overlayShowOverlayMinimalStyle) {
// Draw the fill background
val startX = 2 * padding + iconSize
val endX = width - 4 * padding
val fillWidth = endX - startX
canvas.drawRoundRect(
startX,
height / 2 - 5f,
endX,
height / 2 + 5f,
10f, 10f,
fillBackgroundPaint
)
// Draw the progress
val progressWidth = (progress.toFloat() / maxProgress) * fillWidth
canvas.drawRoundRect(
startX,
height / 2 - 5f,
startX + progressWidth,
height / 2 + 5f,
10f, 10f,
progressPaint
)
}
// Draw the icon
icon?.let {
val iconX = if (!overlayShowOverlayMinimalStyle) {
padding
} else {
padding + minimalStartX
}
val iconY = height / 2 - iconSize / 2
it.setBounds(
iconX.toInt(),
iconY.toInt(),
(iconX + iconSize).toInt(),
(iconY + iconSize).toInt()
)
it.draw(canvas)
}
// Draw the text on the right
val textX = if (!overlayShowOverlayMinimalStyle) {
width - 2 * padding
} else {
minimalStartX + minimalElementWidth - 2 * padding
}
val textY = height / 2 + textPaint.textSize / 3
// Draw the text
canvas.drawText(displayText, textX, textY, textPaint)
}
}

View File

@ -0,0 +1,40 @@
package app.revanced.extension.youtube.utils;
import java.util.Map;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class AuthUtils {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String[] REQUEST_HEADER_KEYS = {
AUTHORIZATION_HEADER,
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
};
public static volatile String authorization = "";
public static volatile String dataSyncId = "";
public static volatile boolean isIncognito = false;
public static volatile Map<String, String> requestHeader;
public static volatile String playlistId = "";
public static volatile String videoId = "";
public static void setRequestHeaders(String url, Map<String, String> requestHeaders) {
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.initializationException(AuthUtils.class, "setRequestHeaders failure", ex);
}
}
}

View File

@ -31,14 +31,20 @@ import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings;
public class ExtendedUtils extends PackageUtils {
private static boolean isVersionOrGreater(String version) {
return getAppVersionName().compareTo(version) >= 0;
}
@SuppressWarnings("unused")
public static final boolean IS_19_17_OR_GREATER = getAppVersionName().compareTo("19.17.00") >= 0;
public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0;
public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0;
public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0;
public static final boolean IS_19_28_OR_GREATER = getAppVersionName().compareTo("19.28.00") >= 0;
public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0;
public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0;
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
public static final boolean IS_19_28_OR_GREATER = isVersionOrGreater("19.28.00");
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
public static final boolean IS_20_09_OR_GREATER = isVersionOrGreater("20.09.00");
public static int validateValue(IntegerSetting settings, int min, int max, String message) {
int value = settings.get();

View File

@ -133,13 +133,25 @@ public class VideoUtils extends IntentUtils {
}
public static void openPlaylist(@NonNull String playlistId, @NonNull String videoId) {
openPlaylist(playlistId, videoId, false);
}
public static void openPlaylist(@NonNull String playlistId, @NonNull String videoId, boolean withTimestamp) {
final StringBuilder sb = new StringBuilder();
if (videoId.isEmpty()) {
sb.append(getPlaylistUrl(playlistId));
} else {
sb.append(getVideoScheme(videoId, false));
sb.append("&list=");
sb.append(VIDEO_URL);
sb.append(videoId);
sb.append("?list=");
sb.append(playlistId);
if (withTimestamp) {
final long currentVideoTimeInSeconds = VideoInformation.getVideoTimeInSeconds();
if (currentVideoTimeInSeconds > 0) {
sb.append("&t=");
sb.append(currentVideoTimeInSeconds);
}
}
}
launchView(sb.toString(), getContext().getPackageName());
}
@ -193,8 +205,8 @@ public class VideoUtils extends IntentUtils {
}
public static void showPlaybackSpeedDialog(@NonNull Context context) {
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedListEntries();
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedListEntryValues();
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedEntries();
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedEntryValues();
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed));
@ -202,6 +214,7 @@ public class VideoUtils extends IntentUtils {
new AlertDialog.Builder(context)
.setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> {
final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f");
VideoInformation.setPlaybackSpeed(selectedPlaybackSpeed);
VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed);
userSelectedPlaybackSpeed(selectedPlaybackSpeed);
mDialog.dismiss();
@ -268,6 +281,13 @@ public class VideoUtils extends IntentUtils {
return !isExternalDownloaderLaunched.get() && original;
}
/**
* Rest of the implementation added by patch.
*/
public static void dismissPlayer() {
Logger.printDebug(() -> "Dismiss player");
}
/**
* Rest of the implementation added by patch.
*/

View File

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

View File

@ -4,5 +4,5 @@ org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
kotlin.jvm.target.validation.mode = IGNORE
version = 5.6.1-dev.1
version = 5.6.2

View File

@ -55,7 +55,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -95,7 +95,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -133,7 +133,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -187,7 +187,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": [
@ -242,7 +243,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -284,7 +285,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -435,7 +436,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -483,7 +484,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": [
@ -550,7 +552,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -659,7 +661,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -692,7 +694,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -764,7 +766,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -786,7 +788,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -808,7 +810,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -849,7 +851,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -897,7 +899,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -975,7 +977,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1022,13 +1024,13 @@
"description": "Adds an option to disable the popup that appears when taking a screenshot.",
"use": true,
"dependencies": [
"Settings for Reddit",
"ResourcePatch"
"Settings for Reddit"
],
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -1069,7 +1071,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1109,7 +1111,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1167,7 +1169,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1195,7 +1197,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1260,7 +1262,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -1307,6 +1309,15 @@
"Clone": "com.rvx.android.apps.youtube.music",
"Default": "app.rvx.android.apps.youtube.music"
}
},
{
"key": "patchAllManifest",
"title": "Patch all manifest components",
"description": "Patch all permissions, intents and content provider authorities supported by GmsCore.",
"required": true,
"type": "kotlin.Boolean",
"default": true,
"values": null
}
]
},
@ -1371,6 +1382,15 @@
"Clone": "com.rvx.android.apps.youtube.music",
"Default": "app.rvx.android.apps.youtube.music"
}
},
{
"key": "patchAllManifest",
"title": "Patch all manifest components",
"description": "Patch all permissions, intents and content provider authorities supported by GmsCore.",
"required": true,
"type": "kotlin.Boolean",
"default": true,
"values": null
}
]
},
@ -1384,7 +1404,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -1442,7 +1463,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1468,7 +1489,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1517,7 +1538,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1532,7 +1553,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -1645,7 +1667,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1683,7 +1705,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -1705,7 +1728,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1768,7 +1791,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1783,7 +1806,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -1942,7 +1966,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -1980,7 +2004,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -1996,7 +2021,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -2110,7 +2136,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2151,7 +2177,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -2172,7 +2199,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2202,12 +2229,14 @@
"description": "Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically.",
"use": true,
"dependencies": [
"Settings for Reddit"
"Settings for Reddit",
"ResourcePatch"
],
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -2229,7 +2258,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2269,7 +2298,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2291,7 +2320,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2336,7 +2365,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2377,7 +2406,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2392,7 +2421,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": []
@ -2451,7 +2481,8 @@
"compatiblePackages": {
"com.reddit.frontpage": [
"2024.17.0",
"2025.05.1"
"2025.05.1",
"2025.12.1"
]
},
"options": [
@ -2496,7 +2527,7 @@
"description": "The settings menu name that the RVX settings menu should be above.",
"required": true,
"type": "kotlin.String",
"default": "@string/about_key",
"default": "@string/parent_tools_key",
"values": {
"Parent settings": "@string/parent_tools_key",
"General": "@string/general_key",
@ -2552,7 +2583,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -2708,7 +2739,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2765,7 +2796,10 @@
"compatiblePackages": {
"com.google.android.apps.youtube.music": [
"6.51.53",
"7.16.53"
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.52"
]
},
"options": []
@ -2809,7 +2843,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -2834,7 +2868,17 @@
"19.47.53"
]
},
"options": []
"options": [
{
"key": "useIOSClient",
"title": "Use iOS client",
"description": "Add setting to set iOS client (Deprecated) as default client.",
"required": false,
"type": "kotlin.Boolean",
"default": false,
"values": null
}
]
},
{
"name": "Swipe controls",
@ -3000,7 +3044,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -3051,7 +3095,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []
@ -3070,6 +3114,7 @@
"BytecodePatch",
"BytecodePatch",
"BytecodePatch",
"BytecodePatch",
"ResourcePatch"
],
"compatiblePackages": {
@ -3143,7 +3188,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": [
@ -3181,7 +3226,7 @@
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51"
"8.12.53"
]
},
"options": []

View File

@ -257,6 +257,7 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
public static final fun getBottomSheetRecyclerView ()J
public static final fun getButtonContainer ()J
public static final fun getButtonIconPaddingMedium ()J
public static final fun getChannelHandle ()J
public static final fun getChipCloud ()J
public static final fun getColorGrey ()J
public static final fun getDarkBackground ()J
@ -458,7 +459,7 @@ public final class app/revanced/patches/reddit/utils/extension/SharedExtensionPa
}
public final class app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatchKt {
public static final fun getScreenShotShareBanner ()J
public static final fun getNsfwDialogTitle ()J
}
public final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt {
@ -555,10 +556,9 @@ public final class app/revanced/patches/shared/mapping/ResourceElement {
}
public final class app/revanced/patches/shared/mapping/ResourceMappingPatchKt {
public static final fun get (Ljava/util/List;Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J
public static final fun get (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)J
public static final fun getResourceId (Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J
public static final fun getResourceId (Ljava/lang/String;Ljava/lang/String;)J
public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun getResourceMappings ()Ljava/util/List;
}
public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum {
@ -875,6 +875,7 @@ public final class app/revanced/patches/youtube/player/seekbar/SeekbarComponents
public final class app/revanced/patches/youtube/shorts/components/FingerprintsKt {
public static final fun indexOfAddLiveHeaderElementsContainerInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
public static final fun indexOfDismissInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
}
public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt {
@ -894,6 +895,10 @@ public final class app/revanced/patches/youtube/utils/FingerprintsKt {
public static final fun indexOfSpannedCharSequenceInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
}
public final class app/revanced/patches/youtube/utils/auth/AuthHookPatchKt {
public static final fun getAuthHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatchKt {
public static final fun getBottomSheetHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@ -906,6 +911,10 @@ public final class app/revanced/patches/youtube/utils/controlsoverlay/ControlsOv
public static final fun getControlsOverlayConfigPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/utils/dismiss/DismissPlayerHookPatchKt {
public static final fun getDismissPlayerHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/utils/engagement/EngagementPanelHookPatchKt {
public static final fun getEngagementPanelHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@ -1038,6 +1047,7 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
public static final fun is_19_04_or_greater ()Z
public static final fun is_19_05_or_greater ()Z
public static final fun is_19_09_or_greater ()Z
public static final fun is_19_11_or_greater ()Z
public static final fun is_19_15_or_greater ()Z
public static final fun is_19_16_or_greater ()Z
public static final fun is_19_17_or_greater ()Z
@ -1052,10 +1062,12 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
public static final fun is_19_34_or_greater ()Z
public static final fun is_19_36_or_greater ()Z
public static final fun is_19_41_or_greater ()Z
public static final fun is_19_42_or_greater ()Z
public static final fun is_19_43_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_49_or_greater ()Z
public static final fun is_19_50_or_greater ()Z
public static final fun is_20_02_or_greater ()Z
public static final fun is_20_03_or_greater ()Z
public static final fun is_20_05_or_greater ()Z

View File

@ -152,7 +152,10 @@ private enum class MethodCall(
RegisterNetworkCallback1(
"Landroid/net/ConnectivityManager;",
"registerNetworkCallback",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;"
),
"V",
),
RegisterNetworkCallback2(
@ -174,13 +177,20 @@ private enum class MethodCall(
RequestNetwork1(
"Landroid/net/ConnectivityManager;",
"requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;"
),
"V",
),
RequestNetwork2(
"Landroid/net/ConnectivityManager;",
"requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"),
arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;",
"I"
),
"V",
),
RequestNetwork3(

View File

@ -19,7 +19,8 @@ val edgeToEdgeDisplayPatch = resourcePatch(
// Instead, it checks compileSdkVersion and prints a warning.
try {
val manifestElement = document.getNode("manifest") as Element
val compileSdkVersion = Integer.parseInt(manifestElement.getAttribute("android:compileSdkVersion"))
val compileSdkVersion =
Integer.parseInt(manifestElement.getAttribute("android:compileSdkVersion"))
if (compileSdkVersion < 35) {
printWarn("This app may not be forcing edge to edge display (compileSdkVersion: $compileSdkVersion)")
}

View File

@ -4,23 +4,27 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR
import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS
import app.revanced.patches.music.utils.resourceid.channelHandle
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@Suppress("unused")
@ -84,17 +88,50 @@ val accountComponentsPatch = bytecodePatch(
}
// account switcher
namesInactiveAccountThumbnailSizeFingerprint.matchOrThrow().let {
it.method.apply {
val targetIndex = it.patternMatch!!.startIndex
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
val textViewField = with(
channelHandleFingerprint
.methodOrThrow(namesInactiveAccountThumbnailSizeFingerprint)
) {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(channelHandle)
getInstruction(
indexOfFirstInstructionOrThrow(literalIndex) {
opcode == Opcode.IPUT_OBJECT &&
getReference<FieldReference>()?.type == "Landroid/widget/TextView;"
},
).getReference<FieldReference>()
}
addInstructions(
targetIndex, """
invoke-static {v$targetRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
move-result v$targetRegister
"""
)
namesInactiveAccountThumbnailSizeFingerprint.methodOrThrow().apply {
var hook = false
implementation!!.instructions
.withIndex()
.filter { (_, instruction) ->
val reference =
(instruction as? ReferenceInstruction)?.reference
instruction.opcode == Opcode.IGET_OBJECT &&
reference is FieldReference &&
reference == textViewField
}
.map { (index, _) -> index }
.forEach { index ->
val insertIndex = index - 1
if (!hook && getInstruction(insertIndex).opcode == Opcode.IF_NEZ) {
val insertRegister =
getInstruction<OneRegisterInstruction>(insertIndex).registerA
addInstructions(
insertIndex, """
invoke-static {v$insertRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
move-result v$insertRegister
"""
)
hook = true
}
}
if (!hook) {
throw PatchException("Could not find TextUtils.isEmpty() index")
}
}

View File

@ -1,11 +1,11 @@
package app.revanced.patches.music.account.components
import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility
import app.revanced.patches.music.utils.resourceid.channelHandle
import app.revanced.patches.music.utils.resourceid.menuEntry
import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize
import app.revanced.patches.music.utils.resourceid.tosFooter
import app.revanced.util.fingerprint.legacyFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
name = "accountSwitcherAccessibilityLabelFingerprint",
@ -14,6 +14,12 @@ internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
literals = listOf(accountSwitcherAccessibility)
)
internal val channelHandleFingerprint = legacyFingerprint(
name = "channelHandleFingerprint",
returnType = "V",
literals = listOf(channelHandle),
)
internal val menuEntryFingerprint = legacyFingerprint(
name = "menuEntryFingerprint",
returnType = "V",
@ -24,19 +30,6 @@ internal val namesInactiveAccountThumbnailSizeFingerprint = legacyFingerprint(
name = "namesInactiveAccountThumbnailSizeFingerprint",
returnType = "V",
parameters = listOf("L", "Ljava/lang/Object;"),
opcodes = listOf(
Opcode.IF_NEZ,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.GOTO,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.IF_EQZ
),
literals = listOf(namesInactiveAccountThumbnailSize)
)

View File

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

View File

@ -97,8 +97,6 @@ internal val showDialogCommandFingerprint = legacyFingerprint(
name = "showDialogCommandFingerprint",
returnType = "V",
opcodes = listOf(
Opcode.IF_EQ,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IGET, // get dialog code
),

View File

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

View File

@ -206,7 +206,8 @@ val changeHeaderPatch = resourcePatch(
printWarn(warnings)
}
val isLegacyLogoExists = get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists()
val isLegacyLogoExists =
get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists()
if (is_7_27_or_greater && isLegacyLogoExists) {
document("res/layout/signin_fragment.xml").use { document ->
document.doRecursively node@{ node ->

View File

@ -24,7 +24,7 @@ import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val EXTENSION_METHOD_DESCRIPTOR =
@ -41,7 +41,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
"7.16.53",
"7.25.53",
"8.05.51",
"8.10.51",
"8.12.53",
),
)
@ -57,7 +57,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
return@execute
} else if (!is_7_20_or_greater) {
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
45635386L,
CAIRO_SPLASH_ANIMATION_FEATURE_FLAG,
EXTENSION_METHOD_DESCRIPTOR
)
} else {
@ -69,18 +69,13 @@ val cairoSplashAnimationPatch = bytecodePatch(
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "setContentView"
} + 1
val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) {
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.name == "findViewById" &&
reference.definingClass != "Landroid/view/View;"
}
val freeIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST)
val freeRegister =
getInstruction<FiveRegisterInstruction>(viewStubFindViewByIdIndex).registerD
val jumpIndex = indexOfFirstInstructionReversedOrThrow(
viewStubFindViewByIdIndex,
Opcode.IGET_OBJECT
)
getInstruction<OneRegisterInstruction>(freeIndex).registerA
val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
} + 1
addInstructionsWithLabels(
insertIndex, """

View File

@ -5,6 +5,8 @@ import app.revanced.patches.music.utils.resourceid.mainActivityLaunchAnimation
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.indexOfFirstLiteralInstruction
internal const val CAIRO_SPLASH_ANIMATION_FEATURE_FLAG = 45635386L
/**
* This fingerprint is compatible with YouTube Music v7.06.53+
*/
@ -20,7 +22,7 @@ internal val cairoSplashAnimationConfigFingerprint = legacyFingerprint(
if (is_7_20_or_greater) {
method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0
} else {
method.indexOfFirstLiteralInstruction(45635386) >= 0
method.indexOfFirstLiteralInstruction(CAIRO_SPLASH_ANIMATION_FEATURE_FLAG) >= 0
}
}
)

View File

@ -1,13 +1,13 @@
package app.revanced.patches.music.misc.watchhistory
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.patch.PatchList.WATCH_HISTORY
import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
@Suppress("unused")
val watchHistoryPatch = bytecodePatch(

View File

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

View File

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

View File

@ -746,15 +746,22 @@ val playerComponentsPatch = bytecodePatch(
val freeRegister =
getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
val getFieldIndex = bottomSheetBehaviorIndex - 2
val getFieldReference =
getInstruction<ReferenceInstruction>(getFieldIndex).reference
val getFieldInstruction = getInstruction<TwoRegisterInstruction>(getFieldIndex)
addInstructionsWithLabels(
bottomSheetBehaviorIndex - 2,
getFieldIndex + 1,
"""
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z
move-result v$freeRegister
if-nez v$freeRegister, :dismiss
iget-object v${getFieldInstruction.registerA}, v${getFieldInstruction.registerB}, $getFieldReference
""",
ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
)
removeInstruction(getFieldIndex)
} ?: throw PatchException("Could not find targetMethod")
}

View File

@ -16,7 +16,7 @@ internal object Constants {
"7.16.53", // This is the latest version that supports the 'Spoof app version' patch.
"7.25.53", // This is the last supported version for 2024.
"8.05.51", // This was the latest version supported by the previous RVX patch.
"8.10.51", // This is the latest version supported by the RVX patch.
"8.12.53", // This is the latest version supported by the RVX patch.
)
)
}

View File

@ -1,14 +1,8 @@
package app.revanced.patches.music.utils.extension
import app.revanced.patches.music.utils.extension.hooks.applicationInitHook
import app.revanced.patches.music.utils.extension.hooks.mainActivityBaseContextHook
import app.revanced.patches.shared.extension.hooks.cronetEngineContextHook
import app.revanced.patches.shared.extension.hooks.firebaseInitProviderContextHook
import app.revanced.patches.shared.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch(
applicationInitHook,
cronetEngineContextHook,
firebaseInitProviderContextHook,
mainActivityBaseContextHook,
)

View File

@ -1,32 +0,0 @@
package app.revanced.patches.music.utils.extension.hooks
import app.revanced.patches.shared.extension.extensionHook
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private var attachBaseContextIndex = -1
internal val mainActivityBaseContextHook = extensionHook(
insertIndexResolver = { method ->
attachBaseContextIndex = method.indexOfFirstInstructionOrThrow {
getReference<MethodReference>()?.name == "attachBaseContext"
}
attachBaseContextIndex + 1
},
contextRegisterResolver = { method ->
val overrideInstruction =
method.implementation!!.instructions.elementAt(attachBaseContextIndex)
as FiveRegisterInstruction
"v${overrideInstruction.registerD}"
},
) {
returns("V")
parameters("Landroid/content/Context;")
custom { method, classDef ->
classDef.type == "Lcom/google/android/apps/youtube/music/activities/MusicActivity;" &&
method.name == "attachBaseContext"
}
}

View File

@ -22,7 +22,6 @@ import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
@ -31,6 +30,7 @@ import app.revanced.patches.shared.indexOfBrandInstruction
import app.revanced.patches.shared.indexOfManufacturerInstruction
import app.revanced.patches.shared.indexOfModelInstruction
import app.revanced.patches.shared.indexOfReleaseInstruction
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow

View File

@ -9,9 +9,8 @@ import app.revanced.patches.shared.mapping.ResourceType.ID
import app.revanced.patches.shared.mapping.ResourceType.LAYOUT
import app.revanced.patches.shared.mapping.ResourceType.STRING
import app.revanced.patches.shared.mapping.ResourceType.STYLE
import app.revanced.patches.shared.mapping.get
import app.revanced.patches.shared.mapping.getResourceId
import app.revanced.patches.shared.mapping.resourceMappingPatch
import app.revanced.patches.shared.mapping.resourceMappings
var accountSwitcherAccessibility = -1L
private set
@ -25,6 +24,8 @@ var buttonContainer = -1L
private set
var buttonIconPaddingMedium = -1L
private set
var channelHandle = -1L
private set
var chipCloud = -1L
private set
var colorGrey = -1L
@ -126,217 +127,61 @@ internal val sharedResourceIdPatch = resourcePatch(
dependsOn(resourceMappingPatch)
execute {
accountSwitcherAccessibility = resourceMappings[
STRING,
"account_switcher_accessibility_label",
]
actionBarLogo = resourceMappings[
DRAWABLE,
"action_bar_logo",
]
actionBarLogoRingo2 = resourceMappings[
DRAWABLE,
"action_bar_logo_ringo2",
]
bottomSheetRecyclerView = resourceMappings[
LAYOUT,
"bottom_sheet_recycler_view"
]
buttonContainer = resourceMappings[
ID,
"button_container"
]
buttonIconPaddingMedium = resourceMappings[
DIMEN,
"button_icon_padding_medium"
]
chipCloud = resourceMappings[
LAYOUT,
"chip_cloud"
]
colorGrey = resourceMappings[
COLOR,
"ytm_color_grey_12"
]
darkBackground = resourceMappings[
ID,
"dark_background"
]
designBottomSheetDialog = resourceMappings[
LAYOUT,
"design_bottom_sheet_dialog"
]
elementsContainer = resourceMappings[
ID,
"elements_container"
]
endButtonsContainer = resourceMappings[
ID,
"end_buttons_container"
]
floatingLayout = resourceMappings[
ID,
"floating_layout"
]
historyMenuItem = resourceMappings[
ID,
"history_menu_item"
]
inlineTimeBarAdBreakMarkerColor = resourceMappings[
COLOR,
"inline_time_bar_ad_break_marker_color"
]
inlineTimeBarProgressColor = resourceMappings[
COLOR,
"inline_time_bar_progress_color"
]
interstitialsContainer = resourceMappings[
ID,
"interstitials_container"
]
isTablet = resourceMappings[
BOOL,
"is_tablet"
]
likeDislikeContainer = resourceMappings[
ID,
"like_dislike_container"
]
mainActivityLaunchAnimation = resourceMappings[
LAYOUT,
"main_activity_launch_animation"
]
menuEntry = resourceMappings[
LAYOUT,
"menu_entry"
]
miniPlayerDefaultText = resourceMappings[
STRING,
"mini_player_default_text"
]
miniPlayerMdxPlaying = resourceMappings[
STRING,
"mini_player_mdx_playing"
]
miniPlayerPlayPauseReplayButton = resourceMappings[
ID,
"mini_player_play_pause_replay_button"
]
miniPlayerViewPager = resourceMappings[
ID,
"mini_player_view_pager"
]
modernDialogBackground = resourceMappings[
DRAWABLE,
"modern_dialog_background"
]
musicNotifierShelf = resourceMappings[
LAYOUT,
"music_notifier_shelf"
]
musicTasteBuilderShelf = resourceMappings[
LAYOUT,
"music_tastebuilder_shelf"
]
namesInactiveAccountThumbnailSize = resourceMappings[
DIMEN,
"names_inactive_account_thumbnail_size"
]
offlineSettingsMenuItem = resourceMappings[
ID,
"offline_settings_menu_item"
]
playerOverlayChip = resourceMappings[
ID,
"player_overlay_chip"
]
playerViewPager = resourceMappings[
ID,
"player_view_pager"
]
privacyTosFooter = resourceMappings[
ID,
"privacy_tos_footer"
]
qualityAuto = resourceMappings[
STRING,
"quality_auto"
]
remixGenericButtonSize = resourceMappings[
DIMEN,
"remix_generic_button_size"
]
searchButton = resourceMappings[
LAYOUT,
"search_button"
]
slidingDialogAnimation = resourceMappings[
STYLE,
"SlidingDialogAnimation"
]
tapBloomView = resourceMappings[
ID,
"tap_bloom_view"
]
text1 = resourceMappings[
ID,
"text1"
]
toolTipContentView = resourceMappings[
LAYOUT,
"tooltip_content_view"
]
topEnd = resourceMappings[
ID,
"TOP_END"
]
topStart = resourceMappings[
ID,
"TOP_START"
]
topBarMenuItemImageView = resourceMappings[
ID,
"top_bar_menu_item_image_view"
]
tosFooter = resourceMappings[
ID,
"tos_footer"
]
touchOutside = resourceMappings[
ID,
"touch_outside"
]
trimSilenceSwitch = resourceMappings[
ID,
"trim_silence_switch"
]
varispeedUnavailableTitle = resourceMappings[
STRING,
"varispeed_unavailable_title"
]
ytFillSamples = resourceMappings[
DRAWABLE,
"yt_fill_samples_vd_theme_24",
]
ytFillYouTubeMusic = resourceMappings[
DRAWABLE,
"yt_fill_youtube_music_vd_theme_24",
]
ytOutlineSamples = resourceMappings[
DRAWABLE,
"yt_outline_samples_vd_theme_24",
]
ytOutlineYouTubeMusic = resourceMappings[
DRAWABLE,
"yt_outline_youtube_music_vd_theme_24",
]
ytmLogo = resourceMappings[
DRAWABLE,
"ytm_logo",
]
ytmLogoRingo2 = resourceMappings[
DRAWABLE,
"ytm_logo_ringo2",
]
accountSwitcherAccessibility = getResourceId(STRING, "account_switcher_accessibility_label")
actionBarLogo = getResourceId(DRAWABLE, "action_bar_logo")
actionBarLogoRingo2 = getResourceId(DRAWABLE, "action_bar_logo_ringo2")
bottomSheetRecyclerView = getResourceId(LAYOUT, "bottom_sheet_recycler_view")
buttonContainer = getResourceId(ID, "button_container")
buttonIconPaddingMedium = getResourceId(DIMEN, "button_icon_padding_medium")
channelHandle = getResourceId(ID, "channel_handle")
chipCloud = getResourceId(LAYOUT, "chip_cloud")
colorGrey = getResourceId(COLOR, "ytm_color_grey_12")
darkBackground = getResourceId(ID, "dark_background")
designBottomSheetDialog = getResourceId(LAYOUT, "design_bottom_sheet_dialog")
elementsContainer = getResourceId(ID, "elements_container")
endButtonsContainer = getResourceId(ID, "end_buttons_container")
floatingLayout = getResourceId(ID, "floating_layout")
historyMenuItem = getResourceId(ID, "history_menu_item")
inlineTimeBarAdBreakMarkerColor =
getResourceId(COLOR, "inline_time_bar_ad_break_marker_color")
inlineTimeBarProgressColor = getResourceId(COLOR, "inline_time_bar_progress_color")
interstitialsContainer = getResourceId(ID, "interstitials_container")
isTablet = getResourceId(BOOL, "is_tablet")
likeDislikeContainer = getResourceId(ID, "like_dislike_container")
mainActivityLaunchAnimation = getResourceId(LAYOUT, "main_activity_launch_animation")
menuEntry = getResourceId(LAYOUT, "menu_entry")
miniPlayerDefaultText = getResourceId(STRING, "mini_player_default_text")
miniPlayerMdxPlaying = getResourceId(STRING, "mini_player_mdx_playing")
miniPlayerPlayPauseReplayButton = getResourceId(ID, "mini_player_play_pause_replay_button")
miniPlayerViewPager = getResourceId(ID, "mini_player_view_pager")
modernDialogBackground = getResourceId(DRAWABLE, "modern_dialog_background")
musicNotifierShelf = getResourceId(LAYOUT, "music_notifier_shelf")
musicTasteBuilderShelf = getResourceId(LAYOUT, "music_tastebuilder_shelf")
namesInactiveAccountThumbnailSize =
getResourceId(DIMEN, "names_inactive_account_thumbnail_size")
offlineSettingsMenuItem = getResourceId(ID, "offline_settings_menu_item")
playerOverlayChip = getResourceId(ID, "player_overlay_chip")
playerViewPager = getResourceId(ID, "player_view_pager")
privacyTosFooter = getResourceId(ID, "privacy_tos_footer")
qualityAuto = getResourceId(STRING, "quality_auto")
remixGenericButtonSize = getResourceId(DIMEN, "remix_generic_button_size")
searchButton = getResourceId(LAYOUT, "search_button")
slidingDialogAnimation = getResourceId(STYLE, "SlidingDialogAnimation")
tapBloomView = getResourceId(ID, "tap_bloom_view")
text1 = getResourceId(ID, "text1")
toolTipContentView = getResourceId(LAYOUT, "tooltip_content_view")
topEnd = getResourceId(ID, "TOP_END")
topStart = getResourceId(ID, "TOP_START")
topBarMenuItemImageView = getResourceId(ID, "top_bar_menu_item_image_view")
tosFooter = getResourceId(ID, "tos_footer")
touchOutside = getResourceId(ID, "touch_outside")
trimSilenceSwitch = getResourceId(ID, "trim_silence_switch")
varispeedUnavailableTitle = getResourceId(STRING, "varispeed_unavailable_title")
ytFillSamples = getResourceId(DRAWABLE, "yt_fill_samples_vd_theme_24")
ytFillYouTubeMusic = getResourceId(DRAWABLE, "yt_fill_youtube_music_vd_theme_24")
ytOutlineSamples = getResourceId(DRAWABLE, "yt_outline_samples_vd_theme_24")
ytOutlineYouTubeMusic = getResourceId(DRAWABLE, "yt_outline_youtube_music_vd_theme_24")
ytmLogo = getResourceId(DRAWABLE, "ytm_logo")
ytmLogoRingo2 = getResourceId(DRAWABLE, "ytm_logo_ringo2")
}
}

View File

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

View File

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

View File

@ -27,14 +27,6 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR =
"$PATCHES_PATH/GeneralAdsPatch;"
private val isCommentAdsMethod: Method.() -> Boolean = {
parameterTypes.size == 1 &&
parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
returnType == "V" &&
indexOfFirstStringInstruction("ad") >= 0
}
@Suppress("unused")
val adsPatch = bytecodePatch(
HIDE_ADS.title,
@ -94,11 +86,20 @@ val adsPatch = bytecodePatch(
if (is_2025_06_or_greater) {
listOf(
commentAdCommentScreenAdViewFingerprint,
commentAdDetailListHeaderViewFingerprint
commentAdDetailListHeaderViewFingerprint,
commentsViewModelFingerprint
).forEach { fingerprint ->
fingerprint.methodOrThrow().hook()
}
} else {
val isCommentAdsMethod: Method.() -> Boolean = {
parameterTypes.size == 1 &&
parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
returnType == "V" &&
indexOfFirstStringInstruction("ad") >= 0
}
classes.forEach { classDef ->
classDef.methods.forEach { method ->
if (method.isCommentAdsMethod()) {

View File

@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
internal val adPostFingerprint = legacyFingerprint(
name = "adPostFingerprint",
@ -49,6 +50,20 @@ internal val commentAdDetailListHeaderViewFingerprint = legacyFingerprint(
},
)
internal val commentsViewModelFingerprint = legacyFingerprint(
name = "commentsViewModelFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("L", "Z", "L", "I"),
customFingerprint = { method, classDef ->
classDef.superclass == "Lcom/reddit/screen/presentation/CompositionViewModel;" &&
method.indexOfFirstInstruction {
opcode == Opcode.NEW_INSTANCE &&
getReference<TypeReference>()?.type?.startsWith("Lcom/reddit/postdetail/comment/refactor/CommentsViewModel\$LoadAdsSeparately\$") == true
} >= 0
},
)
internal val newAdPostFingerprint = legacyFingerprint(
name = "newAdPostFingerprint",
returnType = "L",

View File

@ -56,7 +56,8 @@ val navigationButtonsPatch = bytecodePatch(
if (bottomNavScreenFingerprint.resolvable()) {
val bottomNavScreenMutableClass = with(bottomNavScreenFingerprint.methodOrThrow()) {
val startIndex = indexOfGetDimensionPixelSizeInstruction(this)
val targetIndex = indexOfFirstInstructionOrThrow(startIndex, Opcode.NEW_INSTANCE)
val targetIndex =
indexOfFirstInstructionOrThrow(startIndex, Opcode.NEW_INSTANCE)
val targetReference =
getInstruction<ReferenceInstruction>(targetIndex).reference.toString()
@ -65,7 +66,9 @@ val navigationButtonsPatch = bytecodePatch(
?: throw ClassNotFoundException("Failed to find class $targetReference")
}
bottomNavScreenOnGlobalLayoutFingerprint.second.matchOrNull(bottomNavScreenMutableClass)
bottomNavScreenOnGlobalLayoutFingerprint.second.matchOrNull(
bottomNavScreenMutableClass
)
?.let {
it.method.apply {
val startIndex = it.patternMatch!!.startIndex
@ -82,7 +85,8 @@ val navigationButtonsPatch = bytecodePatch(
// Legacy method.
bottomNavScreenHandlerFingerprint.methodOrThrow().apply {
val targetIndex = indexOfGetItemsInstruction(this) + 1
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
val targetRegister =
getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions(
targetIndex + 1, """

View File

@ -1,36 +0,0 @@
package app.revanced.patches.reddit.layout.screenshotpopup
import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
import com.android.tools.smali.dexlib2.Opcode
/**
* Reddit 2025.06.0 ~
*/
internal val screenshotTakenBannerFingerprint = legacyFingerprint(
name = "screenshotTakenBannerFingerprint",
returnType = "L",
opcodes = listOf(
Opcode.CONST_4,
Opcode.IF_NE,
),
customFingerprint = { method, classDef ->
method.containsLiteralInstruction(screenShotShareBanner) &&
classDef.type.startsWith("Lcom/reddit/sharing/screenshot/composables/") &&
method.name == "invoke"
}
)
/**
* ~ Reddit 2025.05.1
*/
internal val screenshotTakenBannerLegacyFingerprint = legacyFingerprint(
name = "screenshotTakenBannerLegacyFingerprint",
returnType = "V",
parameters = listOf("Landroidx/compose/runtime/", "I"),
customFingerprint = { method, classDef ->
classDef.type.endsWith("\$ScreenshotTakenBannerKt\$lambda-1\$1;") &&
method.name == "invoke"
}
)

View File

@ -1,26 +1,22 @@
package app.revanced.patches.reddit.layout.screenshotpopup
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH
import app.revanced.patches.reddit.utils.patch.PatchList.DISABLE_SCREENSHOT_POPUP
import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
import app.revanced.patches.reddit.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.reddit.utils.settings.is_2025_06_or_greater
import app.revanced.patches.reddit.utils.settings.settingsPatch
import app.revanced.patches.reddit.utils.settings.updatePatchStatus
import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import app.revanced.util.findMutableMethodOf
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
private const val EXTENSION_METHOD_DESCRIPTOR =
"$PATCHES_PATH/ScreenshotPopupPatch;->disableScreenshotPopup()Z"
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@Suppress("unused")
val screenshotPopupPatch = bytecodePatch(
@ -29,39 +25,67 @@ val screenshotPopupPatch = bytecodePatch(
) {
compatibleWith(COMPATIBLE_PACKAGE)
dependsOn(
settingsPatch,
sharedResourceIdPatch,
)
dependsOn(settingsPatch)
execute {
if (is_2025_06_or_greater) {
screenshotTakenBannerFingerprint.methodOrThrow().apply {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(screenShotShareBanner)
val insertIndex = indexOfFirstInstructionReversedOrThrow(literalIndex, Opcode.CONST_4)
val insertRegister = getInstruction<OneRegisterInstruction>(insertIndex).registerA
val jumpIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.SGET_OBJECT)
fun indexOfShowBannerInstruction(method: Method) =
method.indexOfFirstInstruction {
val reference = getReference<FieldReference>()
opcode == Opcode.IGET_OBJECT &&
reference?.name?.contains("shouldShowBanner") == true &&
reference.definingClass.startsWith("Lcom/reddit/sharing/screenshot/") == true
}
addInstructionsWithLabels(
insertIndex, """
invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
move-result v$insertRegister
if-nez v$insertRegister, :hidden
""", ExternalLabel("hidden", getInstruction(jumpIndex))
)
fun indexOfSetValueInstruction(method: Method) =
method.indexOfFirstInstruction {
getReference<MethodReference>()?.name == "setValue"
}
} else {
screenshotTakenBannerLegacyFingerprint.methodOrThrow().apply {
addInstructionsWithLabels(
0, """
invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
move-result v0
if-eqz v0, :dismiss
return-void
""", ExternalLabel("dismiss", getInstruction(0))
)
fun indexOfBooleanInstruction(method: Method, startIndex: Int = 0) =
method.indexOfFirstInstruction(startIndex) {
val reference = getReference<FieldReference>()
opcode == Opcode.SGET_OBJECT &&
reference?.definingClass == "Ljava/lang/Boolean;" &&
reference.type == "Ljava/lang/Boolean;"
}
val isScreenShotMethod: Method.() -> Boolean = {
definingClass.startsWith("Lcom/reddit/sharing/screenshot/") &&
name == "invokeSuspend" &&
indexOfShowBannerInstruction(this) >= 0 &&
indexOfBooleanInstruction(this) >= 0 &&
indexOfSetValueInstruction(this) >= 0
}
var hookCount = 0
classes.forEach { classDef ->
classDef.methods.forEach { method ->
if (method.isScreenShotMethod()) {
proxy(classDef)
.mutableClass
.findMutableMethodOf(method)
.apply {
val showBannerIndex = indexOfShowBannerInstruction(this)
val booleanIndex = indexOfBooleanInstruction(this, showBannerIndex)
val booleanRegister =
getInstruction<OneRegisterInstruction>(booleanIndex).registerA
addInstructions(
booleanIndex + 1, """
invoke-static {v$booleanRegister}, $PATCHES_PATH/ScreenshotPopupPatch;->disableScreenshotPopup(Ljava/lang/Boolean;)Ljava/lang/Boolean;
move-result-object v$booleanRegister
"""
)
hookCount++
}
}
}
}
if (hookCount == 0) {
throw PatchException("Failed to find hook method")
}
updatePatchStatus(

View File

@ -1,5 +1,6 @@
package app.revanced.patches.reddit.layout.subredditdialog
import app.revanced.patches.reddit.utils.resourceid.nsfwDialogTitle
import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
@ -71,6 +72,14 @@ fun indexOfHasBeenVisitedInstruction(method: Method) =
reference.returnType == "Z"
}
internal val nsfwAlertBuilderFingerprint = legacyFingerprint(
name = "nsfwAlertBuilderFingerprint",
literals = listOf(nsfwDialogTitle),
customFingerprint = { method, _ ->
method.definingClass.startsWith("Lcom/reddit/screen/nsfw")
}
)
internal val redditAlertDialogsFingerprint = legacyFingerprint(
name = "redditAlertDialogsFingerprint",
returnType = "V",

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