mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-25 18:34:07 +01:00
Compare commits
122 Commits
v5.30.0-de
...
v5.32.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e4113555b | ||
|
|
125855540b | ||
|
|
a8eee825e6 | ||
|
|
63859f0ef9 | ||
|
|
1c9000dbda | ||
|
|
8ec857a175 | ||
|
|
f56c7868f5 | ||
|
|
cfd77800d6 | ||
|
|
707deaef0b | ||
|
|
9ddb3ac39d | ||
|
|
a7d3b7c287 | ||
|
|
30bac0397e | ||
|
|
c5fc187a35 | ||
|
|
f46dbcd084 | ||
|
|
2136573cb6 | ||
|
|
86ec08993c | ||
|
|
44da5a71c5 | ||
|
|
e4e81b89ea | ||
|
|
165df659a1 | ||
|
|
bb87afe0f6 | ||
|
|
ac5fb17937 | ||
|
|
e88356b3c5 | ||
|
|
dead9c2d94 | ||
|
|
ca640b2839 | ||
|
|
c972267cd8 | ||
|
|
d0d2c13d16 | ||
|
|
e7b4ab53cf | ||
|
|
f994264d9c | ||
|
|
eb61c1f5d1 | ||
|
|
e578347277 | ||
|
|
294b2dce2e | ||
|
|
aa37105ea3 | ||
|
|
eb57a2697b | ||
|
|
19bc5b63c5 | ||
|
|
2b93ff6cfc | ||
|
|
cc6984e919 | ||
|
|
8bf575e778 | ||
|
|
2e625ee1a2 | ||
|
|
6bcba48ee7 | ||
|
|
c3034edc43 | ||
|
|
82255a09d3 | ||
|
|
594dce13cd | ||
|
|
479e205808 | ||
|
|
3d1b7e8101 | ||
|
|
e951184b7a | ||
|
|
d088b1e7ed | ||
|
|
a38f635514 | ||
|
|
b3e6c215cc | ||
|
|
c9cc3d5c41 | ||
|
|
536e64565c | ||
|
|
65cbf3c1eb | ||
|
|
61c1a7a75a | ||
|
|
1e39db06b8 | ||
|
|
e019f83232 | ||
|
|
3b57a5f8c0 | ||
|
|
eafe3dfc45 | ||
|
|
d56d8d990c | ||
|
|
37a8682901 | ||
|
|
11ba7d4e3e | ||
|
|
6833d37c26 | ||
|
|
e6f72bcb7d | ||
|
|
e8a227c082 | ||
|
|
0472ec2830 | ||
|
|
6412a5cb1a | ||
|
|
cc548689ac | ||
|
|
a3d47e72e3 | ||
|
|
f37482443a | ||
|
|
cc4aef89d3 | ||
|
|
1c0a0eb4b5 | ||
|
|
b1d6c46763 | ||
|
|
42195b9f63 | ||
|
|
a4e08ea13d | ||
|
|
bd2a939a72 | ||
|
|
a89179ab79 | ||
|
|
b0129d383a | ||
|
|
23b6c42630 | ||
|
|
10f4464735 | ||
|
|
4e5addbba5 | ||
|
|
8d11ede927 | ||
|
|
83a3f4da00 | ||
|
|
caf3b69731 | ||
|
|
3135203b55 | ||
|
|
8d113a7c67 | ||
|
|
4e742075f3 | ||
|
|
04caa66662 | ||
|
|
dacc85f5e7 | ||
|
|
f9abec358a | ||
|
|
7e11514cc1 | ||
|
|
2e9c8df8f6 | ||
|
|
4c8cfc8800 | ||
|
|
0ba6fad33f | ||
|
|
3eac215e13 | ||
|
|
90a3262f68 | ||
|
|
f7f49b834e | ||
|
|
89ec5d5bc6 | ||
|
|
e3bc8be936 | ||
|
|
6c5c3f5a4d | ||
|
|
629bd0644b | ||
|
|
b4005079e3 | ||
|
|
a354c443ad | ||
|
|
d1313e3ea1 | ||
|
|
11338008c6 | ||
|
|
8b9e04475d | ||
|
|
d3c9dc6ed7 | ||
|
|
d7ed32571f | ||
|
|
d3935f03c0 | ||
|
|
b2e601f0f0 | ||
|
|
d3ec219a29 | ||
|
|
5ed07d4aaa | ||
|
|
209a3a3626 | ||
|
|
2b3419571f | ||
|
|
bbe504e616 | ||
|
|
6c32591f62 | ||
|
|
ad6da67281 | ||
|
|
14dc593eba | ||
|
|
e52ee41222 | ||
|
|
6ee94f8532 | ||
|
|
21688201af | ||
|
|
f08474369b | ||
|
|
ed617094ea | ||
|
|
9131c50f1b | ||
|
|
69600d08a4 |
2
.github/workflows/build_pull_request.yml
vendored
2
.github/workflows/build_pull_request.yml
vendored
@@ -13,8 +13,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
|
||||
1
.github/workflows/pull_strings.yml
vendored
1
.github/workflows/pull_strings.yml
vendored
@@ -17,7 +17,6 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
fetch-depth: 0
|
||||
clean: true
|
||||
|
||||
- name: Pull strings
|
||||
|
||||
2
.github/workflows/push_strings.yml
vendored
2
.github/workflows/push_strings.yml
vendored
@@ -15,8 +15,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Preprocess strings
|
||||
env:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -19,8 +19,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
|
||||
350
CHANGELOG.md
350
CHANGELOG.md
@@ -1,3 +1,353 @@
|
||||
# [5.32.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.4...v5.32.0-dev.5) (2025-07-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YT Music:** Support latest versions ([#5524](https://github.com/ReVanced/revanced-patches/issues/5524)) ([551dcf0](https://github.com/ReVanced/revanced-patches/commit/551dcf01ca9c489a779196b49c8744727d79d6bc))
|
||||
|
||||
# [5.32.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.3...v5.32.0-dev.4) (2025-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Messenger - Hide inbox ads:** Support the latest app version ([2959c02](https://github.com/ReVanced/revanced-patches/commit/2959c0214dfa703ee623ef1f89bded7f78c9d252))
|
||||
|
||||
# [5.32.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.2...v5.32.0-dev.3) (2025-07-24)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - External downloads:** Improve the selection of the external downloader package ([#5504](https://github.com/ReVanced/revanced-patches/issues/5504)) ([5de9aa9](https://github.com/ReVanced/revanced-patches/commit/5de9aa9fad4f24186da045fb188f8718d6f63d7a))
|
||||
|
||||
# [5.32.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.1...v5.32.0-dev.2) (2025-07-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Fix "Hide ticket shelf" ([#5516](https://github.com/ReVanced/revanced-patches/issues/5516)) ([3b85c71](https://github.com/ReVanced/revanced-patches/commit/3b85c71433325fff49e01c77c7b9ff8ddd0a7068))
|
||||
|
||||
# [5.32.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.3-dev.1...v5.32.0-dev.1) (2025-07-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Prime Video:** Add `Playback speed` patch ([#5444](https://github.com/ReVanced/revanced-patches/issues/5444)) ([22cf313](https://github.com/ReVanced/revanced-patches/commit/22cf313a7b99b69e17b9d488c514802043a5dc10))
|
||||
|
||||
## [5.31.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.2...v5.31.3-dev.1) (2025-07-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - GmsCore support:** Fix search suggestions when logged out by using correct search provider ([#5483](https://github.com/ReVanced/revanced-patches/issues/5483)) ([e86fdc8](https://github.com/ReVanced/revanced-patches/commit/e86fdc86b161a6077960b85149e83bacbac664e7))
|
||||
|
||||
## [5.31.2](https://github.com/ReVanced/revanced-patches/compare/v5.31.1...v5.31.2) (2025-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Spoof client:** Fix login failing by spoofing login request in addition ([#5448](https://github.com/ReVanced/revanced-patches/issues/5448)) ([4e59ddc](https://github.com/ReVanced/revanced-patches/commit/4e59ddc62388d09f71b89593fc8b76933d9facea))
|
||||
* **YouTube - Disable double tap actions:** Remove old incompatible targets ([857053e](https://github.com/ReVanced/revanced-patches/commit/857053e29b72ded10a84b0ac693fa107705342d9))
|
||||
* **YouTube - Hide layout components:** Hide quick actions does not work ([#5423](https://github.com/ReVanced/revanced-patches/issues/5423)) ([9c66729](https://github.com/ReVanced/revanced-patches/commit/9c6672946d44001e106bdac9041e2d79ef3f6ab2))
|
||||
* **YouTube - Hide layout components:** Show correct custom header logo if 'Hide YouTube Doodles' is enabled ([#5431](https://github.com/ReVanced/revanced-patches/issues/5431)) ([20cc141](https://github.com/ReVanced/revanced-patches/commit/20cc141e61f75de1a1749247c4f4aed167dee8ea))
|
||||
* **YouTube - Settings:** Back button/gesture closes search instead of exiting ([#5418](https://github.com/ReVanced/revanced-patches/issues/5418)) ([134b278](https://github.com/ReVanced/revanced-patches/commit/134b278baa7b90d2c4b06200cabacabf55ebc055))
|
||||
|
||||
## [5.31.2-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.4...v5.31.2-dev.5) (2025-07-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Spoof client:** Fix login failing by spoofing login request in addition ([#5448](https://github.com/ReVanced/revanced-patches/issues/5448)) ([4e59ddc](https://github.com/ReVanced/revanced-patches/commit/4e59ddc62388d09f71b89593fc8b76933d9facea))
|
||||
|
||||
## [5.31.2-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.3...v5.31.2-dev.4) (2025-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Settings:** Back button/gesture closes search instead of exiting ([#5418](https://github.com/ReVanced/revanced-patches/issues/5418)) ([134b278](https://github.com/ReVanced/revanced-patches/commit/134b278baa7b90d2c4b06200cabacabf55ebc055))
|
||||
|
||||
## [5.31.2-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.2...v5.31.2-dev.3) (2025-07-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Disable double tap actions:** Remove old incompatible targets ([857053e](https://github.com/ReVanced/revanced-patches/commit/857053e29b72ded10a84b0ac693fa107705342d9))
|
||||
|
||||
## [5.31.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.1...v5.31.2-dev.2) (2025-07-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Show correct custom header logo if 'Hide YouTube Doodles' is enabled ([#5431](https://github.com/ReVanced/revanced-patches/issues/5431)) ([20cc141](https://github.com/ReVanced/revanced-patches/commit/20cc141e61f75de1a1749247c4f4aed167dee8ea))
|
||||
|
||||
## [5.31.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.1...v5.31.2-dev.1) (2025-07-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Hide quick actions does not work ([#5423](https://github.com/ReVanced/revanced-patches/issues/5423)) ([9c66729](https://github.com/ReVanced/revanced-patches/commit/9c6672946d44001e106bdac9041e2d79ef3f6ab2))
|
||||
|
||||
## [5.31.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.0...v5.31.1) (2025-07-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Unlock Premium:** Fix hiding context menu ads for latest version ([#5415](https://github.com/ReVanced/revanced-patches/issues/5415)) ([dcde393](https://github.com/ReVanced/revanced-patches/commit/dcde3935bde3172576d0f9f5ff9eb62ecfff7dfe))
|
||||
|
||||
## [5.31.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.0...v5.31.1-dev.1) (2025-07-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Unlock Premium:** Fix hiding context menu ads for latest version ([#5415](https://github.com/ReVanced/revanced-patches/issues/5415)) ([dcde393](https://github.com/ReVanced/revanced-patches/commit/dcde3935bde3172576d0f9f5ff9eb62ecfff7dfe))
|
||||
|
||||
# [5.31.0](https://github.com/ReVanced/revanced-patches/compare/v5.30.0...v5.31.0) (2025-07-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Bacon Reader - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5402](https://github.com/ReVanced/revanced-patches/issues/5402)) ([72459bb](https://github.com/ReVanced/revanced-patches/commit/72459bb2eaf4691e32822dfdd1db3240e2fe98dd))
|
||||
* Correctly name `Enable ROM signature spoofing` patch ([d85881a](https://github.com/ReVanced/revanced-patches/commit/d85881a6768232a999534677bebb248e640fe5ab))
|
||||
* Fix accidental changes ([e2ac841](https://github.com/ReVanced/revanced-patches/commit/e2ac8419756e3c7d62e2c0430a2918a3c1c63666))
|
||||
* Fix refactoring typo ([ec0ae42](https://github.com/ReVanced/revanced-patches/commit/ec0ae42496628cdeb2a639020fce94316b41b751))
|
||||
* Handle empty list of announcements ([de9d720](https://github.com/ReVanced/revanced-patches/commit/de9d7209f4e818a618a7fd9000013ae8ebd728f2))
|
||||
* **SoundCloud:** Constrain patches to last working app target ([e8ea89f](https://github.com/ReVanced/revanced-patches/commit/e8ea89fc1a3f0531a0af7529663f13328aca4fe7))
|
||||
* **Spotify - Unlock Premium:** Remove wrongfully hidden non ad browse sections ([#5403](https://github.com/ReVanced/revanced-patches/issues/5403)) ([8633544](https://github.com/ReVanced/revanced-patches/commit/8633544decc0814d7a548fbc5576b4bdd1d7eee0))
|
||||
* **Spotify:** Remove other ads type from the browse screen ([#5333](https://github.com/ReVanced/revanced-patches/issues/5333)) ([c68533a](https://github.com/ReVanced/revanced-patches/commit/c68533a33a399ca813380b5c9ccddce434ceadf8))
|
||||
* **Sync for Reddit - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5392](https://github.com/ReVanced/revanced-patches/issues/5392)) ([47e6b62](https://github.com/ReVanced/revanced-patches/commit/47e6b62f3d8b07960cfb2963f441222d3e67df92))
|
||||
* **YouTube - Hide ads:** Hide new type of general ad ([#5345](https://github.com/ReVanced/revanced-patches/issues/5345)) ([f23716b](https://github.com/ReVanced/revanced-patches/commit/f23716bc52c03d8d0271bfe38b19247e6de7021d))
|
||||
* **YouTube - Hide layout components:** Do not hide playlist sort button if 'Hide AI comments summary' is on ([5f3e48e](https://github.com/ReVanced/revanced-patches/commit/5f3e48ec5853f6439800ef58239291c34bcab5f6))
|
||||
* **YouTube - Playback speed:** Allow custom speeds with 0.01x precision ([#5360](https://github.com/ReVanced/revanced-patches/issues/5360)) ([0eecef0](https://github.com/ReVanced/revanced-patches/commit/0eecef00fc93d2a217944978e29dce82e3134e35))
|
||||
* **YouTube - Slide to seek:** Show tap and hold 2x speed overlay when active ([#5398](https://github.com/ReVanced/revanced-patches/issues/5398)) ([dbc9c5f](https://github.com/ReVanced/revanced-patches/commit/dbc9c5f00c1f5bbb95f8822667cc1ac3c613fa00))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Cricbuzz - Hide ads:** Hide Cricbuzz11 UI elements ([#5381](https://github.com/ReVanced/revanced-patches/issues/5381)) ([a42c98f](https://github.com/ReVanced/revanced-patches/commit/a42c98f8b51fd37d815fd38b75a2b7ccc4fb049b))
|
||||
* **Lightroom:** Constrain patches to last working version ([#5335](https://github.com/ReVanced/revanced-patches/issues/5335)) ([32ce70e](https://github.com/ReVanced/revanced-patches/commit/32ce70e994f354b9a569376bb89eb38b3190e6f9))
|
||||
* **Spotify - Spoof client:** Fix issues like songs skipping by spoofing to iOS ([#5388](https://github.com/ReVanced/revanced-patches/issues/5388)) ([e36d4c1](https://github.com/ReVanced/revanced-patches/commit/e36d4c1986b58815c7659e6ef44011166873f9c8))
|
||||
* **Spotify:** Remove support for old versions ([#5404](https://github.com/ReVanced/revanced-patches/issues/5404)) ([9d31238](https://github.com/ReVanced/revanced-patches/commit/9d31238803a45e957472760fc40c3862da2cf3f0))
|
||||
* **YouTube - Change header:** Add in-app setting to change the app header ([#5346](https://github.com/ReVanced/revanced-patches/issues/5346)) ([9ba45b6](https://github.com/ReVanced/revanced-patches/commit/9ba45b6680595d732b47e8fa54bee98b7c7af179))
|
||||
* **YouTube - Hide layout components:** Add `Hide channel links preview` and `Hide 'Visit Community' button` in channel page ([#5320](https://github.com/ReVanced/revanced-patches/issues/5320)) ([9d9cce3](https://github.com/ReVanced/revanced-patches/commit/9d9cce3ec5550b2fea88df745f1700bb2f17eb9e))
|
||||
* **YouTube:** Disable two-finger tap gesture for skipping chapters ([#5374](https://github.com/ReVanced/revanced-patches/issues/5374)) ([71db0a2](https://github.com/ReVanced/revanced-patches/commit/71db0a2661b5f76eb5048cdeed83f26fbfdf4fee))
|
||||
|
||||
# [5.31.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.16...v5.31.0-dev.17) (2025-07-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Unlock Premium:** Remove wrongfully hidden non ad browse sections ([#5403](https://github.com/ReVanced/revanced-patches/issues/5403)) ([8633544](https://github.com/ReVanced/revanced-patches/commit/8633544decc0814d7a548fbc5576b4bdd1d7eee0))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Spotify:** Remove support for old versions ([#5404](https://github.com/ReVanced/revanced-patches/issues/5404)) ([9d31238](https://github.com/ReVanced/revanced-patches/commit/9d31238803a45e957472760fc40c3862da2cf3f0))
|
||||
|
||||
# [5.31.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.15...v5.31.0-dev.16) (2025-07-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Spotify - Spoof client:** Fix issues like songs skipping by spoofing to iOS ([#5388](https://github.com/ReVanced/revanced-patches/issues/5388)) ([e36d4c1](https://github.com/ReVanced/revanced-patches/commit/e36d4c1986b58815c7659e6ef44011166873f9c8))
|
||||
* **YouTube:** Disable two-finger tap gesture for skipping chapters ([#5374](https://github.com/ReVanced/revanced-patches/issues/5374)) ([71db0a2](https://github.com/ReVanced/revanced-patches/commit/71db0a2661b5f76eb5048cdeed83f26fbfdf4fee))
|
||||
|
||||
# [5.31.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.14...v5.31.0-dev.15) (2025-07-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Handle empty list of announcements ([de9d720](https://github.com/ReVanced/revanced-patches/commit/de9d7209f4e818a618a7fd9000013ae8ebd728f2))
|
||||
|
||||
# [5.31.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.13...v5.31.0-dev.14) (2025-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Bacon Reader - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5402](https://github.com/ReVanced/revanced-patches/issues/5402)) ([72459bb](https://github.com/ReVanced/revanced-patches/commit/72459bb2eaf4691e32822dfdd1db3240e2fe98dd))
|
||||
|
||||
# [5.31.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.12...v5.31.0-dev.13) (2025-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Slide to seek:** Show tap and hold 2x speed overlay when active ([#5398](https://github.com/ReVanced/revanced-patches/issues/5398)) ([dbc9c5f](https://github.com/ReVanced/revanced-patches/commit/dbc9c5f00c1f5bbb95f8822667cc1ac3c613fa00))
|
||||
|
||||
# [5.31.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.11...v5.31.0-dev.12) (2025-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Sync for Reddit - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5392](https://github.com/ReVanced/revanced-patches/issues/5392)) ([47e6b62](https://github.com/ReVanced/revanced-patches/commit/47e6b62f3d8b07960cfb2963f441222d3e67df92))
|
||||
|
||||
# [5.31.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.10...v5.31.0-dev.11) (2025-07-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Cricbuzz - Hide ads:** Hide Cricbuzz11 UI elements ([#5381](https://github.com/ReVanced/revanced-patches/issues/5381)) ([a42c98f](https://github.com/ReVanced/revanced-patches/commit/a42c98f8b51fd37d815fd38b75a2b7ccc4fb049b))
|
||||
|
||||
# [5.31.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.9...v5.31.0-dev.10) (2025-07-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Do not hide playlist sort button if 'Hide AI comments summary' is on ([5f3e48e](https://github.com/ReVanced/revanced-patches/commit/5f3e48ec5853f6439800ef58239291c34bcab5f6))
|
||||
|
||||
# [5.31.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.8...v5.31.0-dev.9) (2025-07-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix accidental changes ([e2ac841](https://github.com/ReVanced/revanced-patches/commit/e2ac8419756e3c7d62e2c0430a2918a3c1c63666))
|
||||
|
||||
# [5.31.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.7...v5.31.0-dev.8) (2025-07-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Correctly name `Enable ROM signature spoofing` patch ([d85881a](https://github.com/ReVanced/revanced-patches/commit/d85881a6768232a999534677bebb248e640fe5ab))
|
||||
|
||||
# [5.31.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.6...v5.31.0-dev.7) (2025-07-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fix refactoring typo ([ec0ae42](https://github.com/ReVanced/revanced-patches/commit/ec0ae42496628cdeb2a639020fce94316b41b751))
|
||||
|
||||
# [5.31.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.5...v5.31.0-dev.6) (2025-07-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Playback speed:** Allow custom speeds with 0.01x precision ([#5360](https://github.com/ReVanced/revanced-patches/issues/5360)) ([0eecef0](https://github.com/ReVanced/revanced-patches/commit/0eecef00fc93d2a217944978e29dce82e3134e35))
|
||||
|
||||
# [5.31.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.4...v5.31.0-dev.5) (2025-07-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Change header:** Add in-app setting to change the app header ([#5346](https://github.com/ReVanced/revanced-patches/issues/5346)) ([9ba45b6](https://github.com/ReVanced/revanced-patches/commit/9ba45b6680595d732b47e8fa54bee98b7c7af179))
|
||||
|
||||
# [5.31.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.3...v5.31.0-dev.4) (2025-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide ads:** Hide new type of general ad ([#5345](https://github.com/ReVanced/revanced-patches/issues/5345)) ([f23716b](https://github.com/ReVanced/revanced-patches/commit/f23716bc52c03d8d0271bfe38b19247e6de7021d))
|
||||
|
||||
# [5.31.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.2...v5.31.0-dev.3) (2025-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify:** Remove other ads type from the browse screen ([#5333](https://github.com/ReVanced/revanced-patches/issues/5333)) ([c68533a](https://github.com/ReVanced/revanced-patches/commit/c68533a33a399ca813380b5c9ccddce434ceadf8))
|
||||
|
||||
# [5.31.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.1...v5.31.0-dev.2) (2025-07-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide layout components:** Add `Hide channel links preview` and `Hide 'Visit Community' button` in channel page ([#5320](https://github.com/ReVanced/revanced-patches/issues/5320)) ([9d9cce3](https://github.com/ReVanced/revanced-patches/commit/9d9cce3ec5550b2fea88df745f1700bb2f17eb9e))
|
||||
|
||||
# [5.31.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.30.0...v5.31.0-dev.1) (2025-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **SoundCloud:** Constrain patches to last working app target ([e8ea89f](https://github.com/ReVanced/revanced-patches/commit/e8ea89fc1a3f0531a0af7529663f13328aca4fe7))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Lightroom:** Constrain patches to last working version ([#5335](https://github.com/ReVanced/revanced-patches/issues/5335)) ([32ce70e](https://github.com/ReVanced/revanced-patches/commit/32ce70e994f354b9a569376bb89eb38b3190e6f9))
|
||||
|
||||
# [5.30.0](https://github.com/ReVanced/revanced-patches/compare/v5.29.0...v5.30.0) (2025-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Spoof client patch:** Block sending bad integrity verdicts to potentially fix account suspensions ([#5274](https://github.com/ReVanced/revanced-patches/issues/5274)) ([f7b574c](https://github.com/ReVanced/revanced-patches/commit/f7b574ca79c5a616cfe33a3fc75bd8cf68571f7d))
|
||||
* **Spotify - Spoof client:** Handle remaining edge cases to obtain a session ([#5285](https://github.com/ReVanced/revanced-patches/issues/5285)) ([2bb2d59](https://github.com/ReVanced/revanced-patches/commit/2bb2d594936093774e232ad8b274c81e805c5bf6))
|
||||
* **Spotify - Spoof client:** Skip native login screens ([#5228](https://github.com/ReVanced/revanced-patches/issues/5228)) ([c5ebc63](https://github.com/ReVanced/revanced-patches/commit/c5ebc6336ed17cc9cc7f1348282a2aa3c173fb95))
|
||||
* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([73fd832](https://github.com/ReVanced/revanced-patches/commit/73fd83222e089a5fd6e1526e5c12f5a1e9893a35))
|
||||
* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions by simplifying fingerprint ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([dad0ff4](https://github.com/ReVanced/revanced-patches/commit/dad0ff4fba74c2b020fbde6c6d5eb66e10e6f1f7))
|
||||
* **Spotify:** Add `Spoof client` patch to fix various issues by using a web platform access token ([#5173](https://github.com/ReVanced/revanced-patches/issues/5173)) ([b7b75bb](https://github.com/ReVanced/revanced-patches/commit/b7b75bb9d8d5fd505121e752b8a20e61ff28d1b2))
|
||||
* **YouTube - Hide ads:** Fix "Hide shopping links" ([#5267](https://github.com/ReVanced/revanced-patches/issues/5267)) ([2fe4607](https://github.com/ReVanced/revanced-patches/commit/2fe46079d78ab98076d3a4cdf01c8bfdbdea45c0))
|
||||
* **YouTube - Hide layout components:** Fix "Hide AI Comments summary" in Comments ([#5284](https://github.com/ReVanced/revanced-patches/issues/5284)) ([d42370e](https://github.com/ReVanced/revanced-patches/commit/d42370ef71f4608abc64b6ef4a3fb0c5bd5e3eb6))
|
||||
* **YouTube - Hide layout components:** Fix "Hide AI-generated video summary" in video description ([#5269](https://github.com/ReVanced/revanced-patches/issues/5269)) ([5203da0](https://github.com/ReVanced/revanced-patches/commit/5203da0ae58e467657bc915ab0af5b9904c4f492))
|
||||
* **YouTube - Hide layout components:** Fix "Hide ticket shelf" hiding unwanted components ([#5292](https://github.com/ReVanced/revanced-patches/issues/5292)) ([d6b1f7a](https://github.com/ReVanced/revanced-patches/commit/d6b1f7a6e18b1c0eb4374c5e22a1c746dcb3a522))
|
||||
* **YouTube - Hide Shorts components:** Fix hiding of untoggled components ([#5266](https://github.com/ReVanced/revanced-patches/issues/5266)) ([008e192](https://github.com/ReVanced/revanced-patches/commit/008e192779a8658e894d5718baa732717bf96e40))
|
||||
* **YouTube - SponsorBlock:** Do not show undo skip if PiP is active ([#5314](https://github.com/ReVanced/revanced-patches/issues/5314)) ([18af8de](https://github.com/ReVanced/revanced-patches/commit/18af8dead2c6c7f0d99cd75b69948240e0bcd12c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Spotify:** Remove ads section from browse ([#5193](https://github.com/ReVanced/revanced-patches/issues/5193)) ([ebd4dcc](https://github.com/ReVanced/revanced-patches/commit/ebd4dccf12a5fbd31d2d53c19a792c389a4641d7))
|
||||
* **YouTube - Hide layout components:** Add `Hide in history` option to filter bar ([#5271](https://github.com/ReVanced/revanced-patches/issues/5271)) ([ba242a3](https://github.com/ReVanced/revanced-patches/commit/ba242a36b040b82e84870e5e240734637125a472))
|
||||
* **YouTube - SponsorBlock:** Add "Undo automatic skip toast" ([#5277](https://github.com/ReVanced/revanced-patches/issues/5277)) ([7fa169a](https://github.com/ReVanced/revanced-patches/commit/7fa169ae262c880019c5a069a2d6bdc7f94885f1))
|
||||
|
||||
# [5.30.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.9...v5.30.0-dev.10) (2025-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions by simplifying fingerprint ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([dad0ff4](https://github.com/ReVanced/revanced-patches/commit/dad0ff4fba74c2b020fbde6c6d5eb66e10e6f1f7))
|
||||
|
||||
# [5.30.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.8...v5.30.0-dev.9) (2025-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([73fd832](https://github.com/ReVanced/revanced-patches/commit/73fd83222e089a5fd6e1526e5c12f5a1e9893a35))
|
||||
|
||||
# [5.30.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.7...v5.30.0-dev.8) (2025-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Spoof client:** Skip native login screens ([#5228](https://github.com/ReVanced/revanced-patches/issues/5228)) ([c5ebc63](https://github.com/ReVanced/revanced-patches/commit/c5ebc6336ed17cc9cc7f1348282a2aa3c173fb95))
|
||||
|
||||
# [5.30.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.6...v5.30.0-dev.7) (2025-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Spoof client:** Handle remaining edge cases to obtain a session ([#5285](https://github.com/ReVanced/revanced-patches/issues/5285)) ([2bb2d59](https://github.com/ReVanced/revanced-patches/commit/2bb2d594936093774e232ad8b274c81e805c5bf6))
|
||||
|
||||
# [5.30.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.5...v5.30.0-dev.6) (2025-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - SponsorBlock:** Do not show undo skip if PiP is active ([#5314](https://github.com/ReVanced/revanced-patches/issues/5314)) ([18af8de](https://github.com/ReVanced/revanced-patches/commit/18af8dead2c6c7f0d99cd75b69948240e0bcd12c))
|
||||
|
||||
# [5.30.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.4...v5.30.0-dev.5) (2025-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Fix "Hide ticket shelf" hiding unwanted components ([#5292](https://github.com/ReVanced/revanced-patches/issues/5292)) ([d6b1f7a](https://github.com/ReVanced/revanced-patches/commit/d6b1f7a6e18b1c0eb4374c5e22a1c746dcb3a522))
|
||||
|
||||
# [5.30.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.3...v5.30.0-dev.4) (2025-06-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - SponsorBlock:** Add "Undo automatic skip toast" ([#5277](https://github.com/ReVanced/revanced-patches/issues/5277)) ([7fa169a](https://github.com/ReVanced/revanced-patches/commit/7fa169ae262c880019c5a069a2d6bdc7f94885f1))
|
||||
|
||||
# [5.30.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.2...v5.30.0-dev.3) (2025-06-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Fix "Hide AI Comments summary" in Comments ([#5284](https://github.com/ReVanced/revanced-patches/issues/5284)) ([d42370e](https://github.com/ReVanced/revanced-patches/commit/d42370ef71f4608abc64b6ef4a3fb0c5bd5e3eb6))
|
||||
|
||||
# [5.30.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.1...v5.30.0-dev.2) (2025-06-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Spoof client patch:** Block sending bad integrity verdicts to potentially fix account suspensions ([#5274](https://github.com/ReVanced/revanced-patches/issues/5274)) ([f7b574c](https://github.com/ReVanced/revanced-patches/commit/f7b574ca79c5a616cfe33a3fc75bd8cf68571f7d))
|
||||
|
||||
# [5.30.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.1-dev.1...v5.30.0-dev.1) (2025-06-27)
|
||||
|
||||
|
||||
|
||||
4
extensions/cricbuzz/build.gradle.kts
Normal file
4
extensions/cricbuzz/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:cricbuzz:stub"))
|
||||
}
|
||||
1
extensions/cricbuzz/src/main/AndroidManifest.xml
Normal file
1
extensions/cricbuzz/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,28 @@
|
||||
package app.revanced.extension.cricbuzz.ads;
|
||||
|
||||
import com.cricbuzz.android.data.rest.model.BottomBar;
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class HideAdsPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void filterCb11(List<BottomBar> list) {
|
||||
try {
|
||||
Iterator<BottomBar> iterator = list.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
BottomBar bar = iterator.next();
|
||||
if (bar.getName().equals("Cricbuzz11")) {
|
||||
Logger.printInfo(() -> "Removing Cricbuzz11 bar: " + bar);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "filterCb11 failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
extensions/cricbuzz/stub/build.gradle.kts
Normal file
17
extensions/cricbuzz/stub/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.revanced.extension"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
1
extensions/cricbuzz/stub/src/main/AndroidManifest.xml
Normal file
1
extensions/cricbuzz/stub/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
||||
<manifest/>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.cricbuzz.android.data.rest.model;
|
||||
|
||||
public final class BottomBar {
|
||||
public final String getName() { throw new UnsupportedOperationException(); }
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package app.revanced.extension.primevideo.videoplayer;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.RectF;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.PixelFormat;
|
||||
import java.util.Arrays;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
import com.amazon.video.sdk.player.Player;
|
||||
|
||||
public class PlaybackSpeedPatch {
|
||||
private static Player player;
|
||||
private static final float[] SPEED_VALUES = {0.5f, 0.7f, 0.8f, 0.9f, 0.95f, 1.0f, 1.05f, 1.1f, 1.2f, 1.3f, 1.5f, 2.0f};
|
||||
private static final String SPEED_BUTTON_TAG = "speed_overlay";
|
||||
|
||||
public static void setPlayer(Player playerInstance) {
|
||||
player = playerInstance;
|
||||
if (player != null) {
|
||||
// Reset playback rate when switching between episodes to ensure correct display.
|
||||
player.setPlaybackRate(1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public static void initializeSpeedOverlay(View userControlsView) {
|
||||
try {
|
||||
LinearLayout buttonContainer = Utils.getChildViewByResourceName(userControlsView, "ButtonContainerPlayerTop");
|
||||
|
||||
// If the speed overlay exists we should return early.
|
||||
if (Utils.getChildView(buttonContainer, false, child ->
|
||||
child instanceof ImageView && SPEED_BUTTON_TAG.equals(child.getTag())) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageView speedButton = createSpeedButton(userControlsView.getContext());
|
||||
speedButton.setOnClickListener(v -> changePlaybackSpeed(speedButton));
|
||||
buttonContainer.addView(speedButton, 0);
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
Logger.printException(() -> "initializeSpeedOverlay, no button container found", e);
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "initializeSpeedOverlay failure", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ImageView createSpeedButton(Context context) {
|
||||
ImageView speedButton = new ImageView(context);
|
||||
speedButton.setContentDescription("Playback Speed");
|
||||
speedButton.setTag(SPEED_BUTTON_TAG);
|
||||
speedButton.setClickable(true);
|
||||
speedButton.setFocusable(true);
|
||||
speedButton.setScaleType(ImageView.ScaleType.CENTER);
|
||||
|
||||
SpeedIconDrawable speedIcon = new SpeedIconDrawable();
|
||||
speedButton.setImageDrawable(speedIcon);
|
||||
|
||||
int buttonSize = Utils.dipToPixels(48);
|
||||
speedButton.setMinimumWidth(buttonSize);
|
||||
speedButton.setMinimumHeight(buttonSize);
|
||||
|
||||
return speedButton;
|
||||
}
|
||||
|
||||
private static String[] getSpeedOptions() {
|
||||
String[] options = new String[SPEED_VALUES.length];
|
||||
for (int i = 0; i < SPEED_VALUES.length; i++) {
|
||||
options[i] = SPEED_VALUES[i] + "x";
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private static void changePlaybackSpeed(ImageView imageView) {
|
||||
if (player == null) {
|
||||
Logger.printException(() -> "Player not available");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
player.pause();
|
||||
AlertDialog dialog = createSpeedPlaybackDialog(imageView);
|
||||
dialog.setOnDismissListener(dialogInterface -> player.play());
|
||||
dialog.show();
|
||||
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "changePlaybackSpeed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static AlertDialog createSpeedPlaybackDialog(ImageView imageView) {
|
||||
Context context = imageView.getContext();
|
||||
int currentSelection = getCurrentSpeedSelection();
|
||||
|
||||
return new AlertDialog.Builder(context)
|
||||
.setTitle("Select Playback Speed")
|
||||
.setSingleChoiceItems(getSpeedOptions(), currentSelection,
|
||||
PlaybackSpeedPatch::handleSpeedSelection)
|
||||
.create();
|
||||
}
|
||||
|
||||
private static int getCurrentSpeedSelection() {
|
||||
try {
|
||||
float currentRate = player.getPlaybackRate();
|
||||
int index = Arrays.binarySearch(SPEED_VALUES, currentRate);
|
||||
return Math.max(index, 0); // Use slowest speed if not found.
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "getCurrentSpeedSelection error getting current playback speed", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleSpeedSelection(android.content.DialogInterface dialog, int selectedIndex) {
|
||||
try {
|
||||
float selectedSpeed = SPEED_VALUES[selectedIndex];
|
||||
player.setPlaybackRate(selectedSpeed);
|
||||
player.play();
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "handleSpeedSelection error setting playback speed", e);
|
||||
} finally {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedIconDrawable extends Drawable {
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
int w = getBounds().width();
|
||||
int h = getBounds().height();
|
||||
float centerX = w / 2f;
|
||||
// Position gauge in lower portion.
|
||||
float centerY = h * 0.7f;
|
||||
float radius = Math.min(w, h) / 2f * 0.8f;
|
||||
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(radius * 0.1f);
|
||||
|
||||
// Draw semicircle.
|
||||
RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
|
||||
canvas.drawArc(oval, 180, 180, false, paint);
|
||||
|
||||
// Draw three tick marks.
|
||||
paint.setStrokeWidth(radius * 0.06f);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
float angle = 180 + (i * 45); // 180°, 225°, 270°.
|
||||
float angleRad = (float) Math.toRadians(angle);
|
||||
|
||||
float startX = centerX + (radius * 0.8f) * (float) Math.cos(angleRad);
|
||||
float startY = centerY + (radius * 0.8f) * (float) Math.sin(angleRad);
|
||||
float endX = centerX + radius * (float) Math.cos(angleRad);
|
||||
float endY = centerY + radius * (float) Math.sin(angleRad);
|
||||
|
||||
canvas.drawLine(startX, startY, endX, endY, paint);
|
||||
}
|
||||
|
||||
// Draw needle.
|
||||
paint.setStrokeWidth(radius * 0.08f);
|
||||
float needleAngle = 200; // Slightly right of center.
|
||||
float needleAngleRad = (float) Math.toRadians(needleAngle);
|
||||
|
||||
float needleEndX = centerX + (radius * 0.6f) * (float) Math.cos(needleAngleRad);
|
||||
float needleEndY = centerY + (radius * 0.6f) * (float) Math.sin(needleAngleRad);
|
||||
|
||||
canvas.drawLine(centerX, centerY, needleEndX, needleEndY, paint);
|
||||
|
||||
// Center dot.
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawCircle(centerX, centerY, radius * 0.06f, paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth() {
|
||||
return Utils.dipToPixels(32);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight() {
|
||||
return Utils.dipToPixels(32);
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,10 @@ public interface VideoPlayer {
|
||||
long getCurrentPosition();
|
||||
|
||||
void seekTo(long positionMs);
|
||||
|
||||
void pause();
|
||||
|
||||
void play();
|
||||
|
||||
boolean isPlaying();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.amazon.video.sdk.player;
|
||||
|
||||
public interface Player {
|
||||
float getPlaybackRate();
|
||||
|
||||
void setPlaybackRate(float rate);
|
||||
|
||||
void play();
|
||||
|
||||
void pause();
|
||||
}
|
||||
@@ -311,6 +311,10 @@ public class Utils {
|
||||
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
||||
}
|
||||
|
||||
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||
return getContext().getResources().getStringArray(getResourceIdentifier(resourceIdentifierName, "array"));
|
||||
}
|
||||
|
||||
public interface MatchFilter<T> {
|
||||
boolean matches(T object);
|
||||
}
|
||||
@@ -579,7 +583,7 @@ public class Utils {
|
||||
Context currentContext = context;
|
||||
|
||||
if (currentContext == null) {
|
||||
Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast, null);
|
||||
Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||
Toast.makeText(currentContext, messageToToast, toastDuration).show();
|
||||
@@ -809,7 +813,7 @@ public class Utils {
|
||||
|
||||
// Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
|
||||
ScrollView contentScrollView = null;
|
||||
LinearLayout contentContainer = null;
|
||||
LinearLayout contentContainer;
|
||||
if (message != null || editText != null) {
|
||||
contentScrollView = new ScrollView(context);
|
||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
||||
@@ -833,7 +837,7 @@ public class Utils {
|
||||
contentScrollView.addView(contentContainer);
|
||||
|
||||
// Message (if not replaced by EditText).
|
||||
if (editText == null && message != null) {
|
||||
if (editText == null) {
|
||||
TextView messageView = new TextView(context);
|
||||
messageView.setText(message); // Supports Spanned (HTML).
|
||||
messageView.setTextSize(16);
|
||||
@@ -1434,6 +1438,28 @@ public class Utils {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a percentage of the screen height to actual device pixels.
|
||||
*
|
||||
* @param percentage The percentage of the screen height (e.g., 30 for 30%).
|
||||
* @return The device pixel value corresponding to the percentage of screen height.
|
||||
*/
|
||||
public static int percentageHeightToPixels(int percentage) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
return (int) (metrics.heightPixels * (percentage / 100.0f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a percentage of the screen width to actual device pixels.
|
||||
*
|
||||
* @param percentage The percentage of the screen width (e.g., 30 for 30%).
|
||||
* @return The device pixel value corresponding to the percentage of screen width.
|
||||
*/
|
||||
public static int percentageWidthToPixels(int percentage) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
return (int) (metrics.widthPixels * (percentage / 100.0f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
|
||||
* <p>
|
||||
|
||||
@@ -71,15 +71,20 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
||||
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private T getEnumFromString(String enumName) {
|
||||
/**
|
||||
* @param enumName Enum name. Casing does not matter.
|
||||
* @return Enum of this type with the same declared name.
|
||||
* @throws IllegalArgumentException if the name is not a valid enum of this type.
|
||||
*/
|
||||
protected T getEnumFromString(String enumName) {
|
||||
//noinspection ConstantConditions
|
||||
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
|
||||
if (value.name().equalsIgnoreCase(enumName)) {
|
||||
// noinspection unchecked
|
||||
//noinspection unchecked
|
||||
return (T) value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown enum value: " + enumName);
|
||||
}
|
||||
|
||||
@@ -103,7 +108,9 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
||||
* Availability based on if this setting is currently set to any of the provided types.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final Setting.Availability availability(@NonNull T... types) {
|
||||
public final Setting.Availability availability(T... types) {
|
||||
Objects.requireNonNull(types);
|
||||
|
||||
return () -> {
|
||||
T currentEnumType = get();
|
||||
for (T enumType : types) {
|
||||
|
||||
@@ -28,16 +28,14 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* Availability based on a single parent setting being enabled.
|
||||
*/
|
||||
@NonNull
|
||||
public static Availability parent(@NonNull BooleanSetting parent) {
|
||||
public static Availability parent(BooleanSetting parent) {
|
||||
return parent::get;
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability based on all parents being enabled.
|
||||
*/
|
||||
@NonNull
|
||||
public static Availability parentsAll(@NonNull BooleanSetting... parents) {
|
||||
public static Availability parentsAll(BooleanSetting... parents) {
|
||||
return () -> {
|
||||
for (BooleanSetting parent : parents) {
|
||||
if (!parent.get()) return false;
|
||||
@@ -49,8 +47,7 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* Availability based on any parent being enabled.
|
||||
*/
|
||||
@NonNull
|
||||
public static Availability parentsAny(@NonNull BooleanSetting... parents) {
|
||||
public static Availability parentsAny(BooleanSetting... parents) {
|
||||
return () -> {
|
||||
for (BooleanSetting parent : parents) {
|
||||
if (parent.get()) return true;
|
||||
@@ -79,7 +76,7 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
|
||||
*/
|
||||
public static void addImportExportCallback(@NonNull ImportExportCallback callback) {
|
||||
public static void addImportExportCallback(ImportExportCallback callback) {
|
||||
importExportCallbacks.add(Objects.requireNonNull(callback));
|
||||
}
|
||||
|
||||
@@ -100,14 +97,13 @@ public abstract class Setting<T> {
|
||||
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
|
||||
|
||||
@Nullable
|
||||
public static Setting<?> getSettingFromPath(@NonNull String str) {
|
||||
public static Setting<?> getSettingFromPath(String str) {
|
||||
return PATH_TO_SETTINGS.get(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All settings that have been created.
|
||||
*/
|
||||
@NonNull
|
||||
public static List<Setting<?>> allLoadedSettings() {
|
||||
return Collections.unmodifiableList(SETTINGS);
|
||||
}
|
||||
@@ -115,7 +111,6 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* @return All settings that have been created, sorted by keys.
|
||||
*/
|
||||
@NonNull
|
||||
private static List<Setting<?>> allLoadedSettingsSorted() {
|
||||
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
||||
return allLoadedSettings();
|
||||
@@ -124,13 +119,11 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* The key used to store the value in the shared preferences.
|
||||
*/
|
||||
@NonNull
|
||||
public final String key;
|
||||
|
||||
/**
|
||||
* The default value of the setting.
|
||||
*/
|
||||
@NonNull
|
||||
public final T defaultValue;
|
||||
|
||||
/**
|
||||
@@ -161,7 +154,6 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* The value of the setting.
|
||||
*/
|
||||
@NonNull
|
||||
protected volatile T value;
|
||||
|
||||
public Setting(String key, T defaultValue) {
|
||||
@@ -199,8 +191,8 @@ public abstract class Setting<T> {
|
||||
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
|
||||
* @param availability Condition that must be true, for this setting to be available to configure.
|
||||
*/
|
||||
public Setting(@NonNull String key,
|
||||
@NonNull T defaultValue,
|
||||
public Setting(String key,
|
||||
T defaultValue,
|
||||
boolean rebootApp,
|
||||
boolean includeWithImportExport,
|
||||
@Nullable String userDialogMessage,
|
||||
@@ -227,7 +219,7 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
|
||||
*/
|
||||
public static <T> void migrateOldSettingToNew(@NonNull Setting<T> oldSetting, @NonNull Setting<T> newSetting) {
|
||||
public static <T> void migrateOldSettingToNew(Setting<T> oldSetting, Setting<T> newSetting) {
|
||||
if (oldSetting == newSetting) throw new IllegalArgumentException();
|
||||
|
||||
if (!oldSetting.isSetToDefault()) {
|
||||
@@ -243,7 +235,7 @@ public abstract class Setting<T> {
|
||||
* This method will be deleted in the future.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
|
||||
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||
return; // Nothing to do.
|
||||
}
|
||||
@@ -285,7 +277,7 @@ public abstract class Setting<T> {
|
||||
* This intentionally is a static method to deter
|
||||
* accidental usage when {@link #save(Object)} was intended.
|
||||
*/
|
||||
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
|
||||
public static void privateSetValueFromString(Setting<?> setting, String newValue) {
|
||||
setting.setValueFromString(newValue);
|
||||
|
||||
// Clear the preference value since default is used, to allow changing
|
||||
@@ -299,7 +291,7 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
|
||||
*/
|
||||
protected abstract void setValueFromString(@NonNull String newValue);
|
||||
protected abstract void setValueFromString(String newValue);
|
||||
|
||||
/**
|
||||
* Load and set the value of {@link #value}.
|
||||
@@ -309,7 +301,7 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* Persistently saves the value.
|
||||
*/
|
||||
public final void save(@NonNull T newValue) {
|
||||
public final void save(T newValue) {
|
||||
if (value.equals(newValue)) {
|
||||
return;
|
||||
}
|
||||
@@ -406,7 +398,6 @@ public abstract class Setting<T> {
|
||||
json.put(importExportKey, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String exportToJson(@Nullable Context alertDialogContext) {
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
@@ -445,7 +436,7 @@ public abstract class Setting<T> {
|
||||
/**
|
||||
* @return if any settings that require a reboot were changed.
|
||||
*/
|
||||
public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) {
|
||||
public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) {
|
||||
try {
|
||||
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
|
||||
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
@@ -26,7 +24,7 @@ public class CustomDialogListPreference extends ListPreference {
|
||||
/**
|
||||
* Custom ArrayAdapter to handle checkmark visibility.
|
||||
*/
|
||||
private static class ListPreferenceArrayAdapter extends ArrayAdapter<CharSequence> {
|
||||
public static class ListPreferenceArrayAdapter extends ArrayAdapter<CharSequence> {
|
||||
private static class SubViewDataContainer {
|
||||
ImageView checkIcon;
|
||||
View placeholder;
|
||||
|
||||
@@ -1,28 +1,15 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.graphics.drawable.shapes.RectShape;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ dependencies {
|
||||
compileOnly(project(":extensions:spotify:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
|
||||
implementation(project(":extensions:spotify:utils"))
|
||||
implementation(libs.nanohttpd)
|
||||
implementation(libs.protobuf.javalite)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package app.revanced.extension.spotify.layout.hide.createbutton;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.spotify.shared.ComponentFilters.*;
|
||||
import app.revanced.extension.spotify.shared.ComponentFilters.ComponentFilter;
|
||||
import app.revanced.extension.spotify.shared.ComponentFilters.ResourceIdComponentFilter;
|
||||
import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HideCreateButtonPatch {
|
||||
@@ -53,7 +55,9 @@ public final class HideCreateButtonPatch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
} catch (Throwable ex) {
|
||||
// Catch Throwable as calling toString can cause crashes with wrongfully generated code that throws
|
||||
// NoSuchMethod errors.
|
||||
Logger.printException(() -> "returnNullIfIsCreateButton failure", ex);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package app.revanced.extension.spotify.misc.fix;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import static app.revanced.extension.spotify.misc.fix.Constants.*;
|
||||
|
||||
class ClientTokenService {
|
||||
private static final String IOS_CLIENT_ID = "58bd3c95768941ea9eb4350aaa033eb3";
|
||||
private static final String IOS_USER_AGENT;
|
||||
|
||||
static {
|
||||
String clientVersion = getClientVersion();
|
||||
int commitHashIndex = clientVersion.lastIndexOf(".");
|
||||
String version = clientVersion.substring(
|
||||
clientVersion.indexOf("-") + 1,
|
||||
clientVersion.lastIndexOf(".", commitHashIndex - 1)
|
||||
);
|
||||
|
||||
IOS_USER_AGENT = "Spotify/" + version + " iOS/" + getSystemVersion() + " (" + getHardwareMachine() + ")";
|
||||
}
|
||||
|
||||
private static final ConnectivitySdkData.Builder IOS_CONNECTIVITY_SDK_DATA =
|
||||
ConnectivitySdkData.newBuilder()
|
||||
.setPlatformSpecificData(PlatformSpecificData.newBuilder()
|
||||
.setIos(NativeIOSData.newBuilder()
|
||||
.setHwMachine(getHardwareMachine())
|
||||
.setSystemVersion(getSystemVersion())
|
||||
)
|
||||
);
|
||||
|
||||
private static final ClientDataRequest.Builder IOS_CLIENT_DATA_REQUEST =
|
||||
ClientDataRequest.newBuilder()
|
||||
.setClientVersion(getClientVersion())
|
||||
.setClientId(IOS_CLIENT_ID);
|
||||
|
||||
private static final ClientTokenRequest.Builder IOS_CLIENT_TOKEN_REQUEST =
|
||||
ClientTokenRequest.newBuilder()
|
||||
.setRequestType(ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST);
|
||||
|
||||
|
||||
@NonNull
|
||||
static ClientTokenRequest newIOSClientTokenRequest(String deviceId) {
|
||||
Logger.printInfo(() -> "Creating new iOS client token request with device ID: " + deviceId);
|
||||
|
||||
return IOS_CLIENT_TOKEN_REQUEST
|
||||
.setClientData(IOS_CLIENT_DATA_REQUEST
|
||||
.setConnectivitySdkData(IOS_CONNECTIVITY_SDK_DATA
|
||||
.setDeviceId(deviceId)
|
||||
)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static ClientTokenResponse getClientTokenResponse(@NonNull ClientTokenRequest request) {
|
||||
if (request.getRequestType() == ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST) {
|
||||
Logger.printInfo(() -> "Requesting iOS client token");
|
||||
String deviceId = request.getClientData().getConnectivitySdkData().getDeviceId();
|
||||
request = newIOSClientTokenRequest(deviceId);
|
||||
}
|
||||
|
||||
ClientTokenResponse response;
|
||||
try {
|
||||
response = requestClientToken(request);
|
||||
} catch (IOException ex) {
|
||||
Logger.printException(() -> "Failed to handle request", ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static ClientTokenResponse requestClientToken(@NonNull ClientTokenRequest request) throws IOException {
|
||||
HttpURLConnection urlConnection = (HttpURLConnection) new URL(CLIENT_TOKEN_API_URL).openConnection();
|
||||
urlConnection.setRequestMethod("POST");
|
||||
urlConnection.setDoOutput(true);
|
||||
urlConnection.setRequestProperty("Content-Type", "application/x-protobuf");
|
||||
urlConnection.setRequestProperty("Accept", "application/x-protobuf");
|
||||
urlConnection.setRequestProperty("User-Agent", IOS_USER_AGENT);
|
||||
|
||||
byte[] requestArray = request.toByteArray();
|
||||
urlConnection.setFixedLengthStreamingMode(requestArray.length);
|
||||
urlConnection.getOutputStream().write(requestArray);
|
||||
|
||||
try (InputStream inputStream = urlConnection.getInputStream()) {
|
||||
return ClientTokenResponse.parseFrom(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static ClientTokenResponse serveClientTokenRequest(@NonNull InputStream inputStream) {
|
||||
ClientTokenRequest request;
|
||||
try {
|
||||
request = ClientTokenRequest.parseFrom(inputStream);
|
||||
} catch (IOException ex) {
|
||||
Logger.printException(() -> "Failed to parse request from input stream", ex);
|
||||
return null;
|
||||
}
|
||||
Logger.printInfo(() -> "Request of type: " + request.getRequestType());
|
||||
|
||||
ClientTokenResponse response = getClientTokenResponse(request);
|
||||
if (response != null) Logger.printInfo(() -> "Response of type: " + response.getResponseType());
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package app.revanced.extension.spotify.misc.fix;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
class Constants {
|
||||
static final String CLIENT_TOKEN_API_PATH = "/v1/clienttoken";
|
||||
static final String CLIENT_TOKEN_API_URL = "https://clienttoken.spotify.com" + CLIENT_TOKEN_API_PATH;
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
@NonNull
|
||||
static String getClientVersion() {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
@NonNull
|
||||
static String getSystemVersion() {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
@NonNull
|
||||
static String getHardwareMachine() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package app.revanced.extension.spotify.misc.fix;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.spotify.login5.v4.proto.Login5.*;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.MessageLite;
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
|
||||
|
||||
class LoginRequestListener extends NanoHTTPD {
|
||||
LoginRequestListener(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Response serve(IHTTPSession request) {
|
||||
Logger.printInfo(() -> "Serving request for URI: " + request.getUri());
|
||||
|
||||
InputStream requestBodyInputStream = getRequestBodyInputStream(request);
|
||||
|
||||
LoginRequest loginRequest;
|
||||
try {
|
||||
loginRequest = LoginRequest.parseFrom(requestBodyInputStream);
|
||||
} catch (IOException e) {
|
||||
Logger.printException(() -> "Failed to parse LoginRequest", e);
|
||||
return newResponse(INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
MessageLite loginResponse;
|
||||
|
||||
// A request may be made concurrently by Spotify,
|
||||
// however a webview can only handle one request at a time due to singleton cookie manager.
|
||||
// Therefore, synchronize to ensure that only one webview handles the request at a time.
|
||||
synchronized (this) {
|
||||
loginResponse = getLoginResponse(loginRequest);
|
||||
}
|
||||
|
||||
if (loginResponse != null) {
|
||||
return newResponse(Response.Status.OK, loginResponse);
|
||||
}
|
||||
|
||||
return newResponse(INTERNAL_ERROR);
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
private static LoginResponse getLoginResponse(@NonNull LoginRequest loginRequest) {
|
||||
Session session;
|
||||
|
||||
boolean isInitialLogin = !loginRequest.hasStoredCredential();
|
||||
if (isInitialLogin) {
|
||||
Logger.printInfo(() -> "Received request for initial login");
|
||||
session = WebApp.currentSession; // Session obtained from WebApp.login.
|
||||
} else {
|
||||
Logger.printInfo(() -> "Received request to restore saved session");
|
||||
session = Session.read(loginRequest.getStoredCredential().getUsername());
|
||||
}
|
||||
|
||||
return toLoginResponse(session, isInitialLogin);
|
||||
}
|
||||
|
||||
|
||||
private static LoginResponse toLoginResponse(Session session, boolean isInitialLogin) {
|
||||
LoginResponse.Builder builder = LoginResponse.newBuilder();
|
||||
|
||||
if (session == null) {
|
||||
if (isInitialLogin) {
|
||||
Logger.printInfo(() -> "Session is null, returning try again later error for initial login");
|
||||
builder.setError(LoginError.TRY_AGAIN_LATER);
|
||||
} else {
|
||||
Logger.printInfo(() -> "Session is null, returning invalid credentials error for stored credential login");
|
||||
builder.setError(LoginError.INVALID_CREDENTIALS);
|
||||
}
|
||||
} else if (session.username == null) {
|
||||
Logger.printInfo(() -> "Session username is null, returning invalid credentials error");
|
||||
builder.setError(LoginError.INVALID_CREDENTIALS);
|
||||
} else if (session.accessTokenExpired()) {
|
||||
Logger.printInfo(() -> "Access token has expired, renewing session");
|
||||
WebApp.renewSession(session.cookies);
|
||||
return toLoginResponse(WebApp.currentSession, isInitialLogin);
|
||||
} else {
|
||||
session.save();
|
||||
Logger.printInfo(() -> "Returning session for username: " + session.username);
|
||||
builder.setOk(LoginOk.newBuilder()
|
||||
.setUsername(session.username)
|
||||
.setAccessToken(session.accessToken)
|
||||
.setStoredCredential(ByteString.fromHex("00")) // Placeholder, as it cannot be null or empty.
|
||||
.setAccessTokenExpiresIn(session.accessTokenExpiresInSeconds())
|
||||
.build());
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static InputStream limitedInputStream(InputStream inputStream, long contentLength) {
|
||||
return new FilterInputStream(inputStream) {
|
||||
private long remaining = contentLength;
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (remaining <= 0) return -1;
|
||||
int result = super.read();
|
||||
if (result != -1) remaining--;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (remaining <= 0) return -1;
|
||||
len = (int) Math.min(len, remaining);
|
||||
int result = super.read(b, off, len);
|
||||
if (result != -1) remaining -= result;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static InputStream getRequestBodyInputStream(@NonNull IHTTPSession request) {
|
||||
long requestContentLength =
|
||||
Long.parseLong(Objects.requireNonNull(request.getHeaders().get("content-length")));
|
||||
return limitedInputStream(request.getInputStream(), requestContentLength);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NonNull
|
||||
private static Response newResponse(Response.Status status) {
|
||||
return newResponse(status, null);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
|
||||
if (messageLite == null) {
|
||||
return newFixedLengthResponse(status, "application/x-protobuf", null);
|
||||
}
|
||||
|
||||
byte[] messageBytes = messageLite.toByteArray();
|
||||
InputStream stream = new ByteArrayInputStream(messageBytes);
|
||||
return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package app.revanced.extension.spotify.misc.fix;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.ClientTokenResponse;
|
||||
import com.google.protobuf.MessageLite;
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
import static app.revanced.extension.spotify.misc.fix.ClientTokenService.serveClientTokenRequest;
|
||||
import static app.revanced.extension.spotify.misc.fix.Constants.CLIENT_TOKEN_API_PATH;
|
||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
|
||||
|
||||
class RequestListener extends NanoHTTPD {
|
||||
RequestListener(int port) {
|
||||
super(port);
|
||||
|
||||
try {
|
||||
start();
|
||||
} catch (IOException ex) {
|
||||
Logger.printException(() -> "Failed to start request listener on port " + port, ex);
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Response serve(@NonNull IHTTPSession session) {
|
||||
String uri = session.getUri();
|
||||
if (!uri.equals(CLIENT_TOKEN_API_PATH)) return INTERNAL_ERROR_RESPONSE;
|
||||
|
||||
Logger.printInfo(() -> "Serving request for URI: " + uri);
|
||||
|
||||
ClientTokenResponse response = serveClientTokenRequest(getInputStream(session));
|
||||
if (response != null) return newResponse(Response.Status.OK, response);
|
||||
|
||||
Logger.printException(() -> "Failed to serve client token request");
|
||||
return INTERNAL_ERROR_RESPONSE;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static InputStream newLimitedInputStream(InputStream inputStream, long contentLength) {
|
||||
return new FilterInputStream(inputStream) {
|
||||
private long remaining = contentLength;
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if (remaining <= 0) return -1;
|
||||
int result = super.read();
|
||||
if (result != -1) remaining--;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (remaining <= 0) return -1;
|
||||
len = (int) Math.min(len, remaining);
|
||||
int result = super.read(b, off, len);
|
||||
if (result != -1) remaining -= result;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static InputStream getInputStream(@NonNull IHTTPSession session) {
|
||||
long requestContentLength = Long.parseLong(Objects.requireNonNull(session.getHeaders().get("content-length")));
|
||||
return newLimitedInputStream(session.getInputStream(), requestContentLength);
|
||||
}
|
||||
|
||||
private static final Response INTERNAL_ERROR_RESPONSE = newResponse(INTERNAL_ERROR);
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NonNull
|
||||
private static Response newResponse(Response.Status status) {
|
||||
return newResponse(status, null);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
|
||||
if (messageLite == null) {
|
||||
return newFixedLengthResponse(status, "application/x-protobuf", null);
|
||||
}
|
||||
|
||||
byte[] messageBytes = messageLite.toByteArray();
|
||||
InputStream stream = new ByteArrayInputStream(messageBytes);
|
||||
return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package app.revanced.extension.spotify.misc.fix;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import static android.content.Context.MODE_PRIVATE;
|
||||
|
||||
class Session {
|
||||
/**
|
||||
* Username of the account. Null if this session does not have an authenticated user.
|
||||
*/
|
||||
@Nullable
|
||||
final String username;
|
||||
/**
|
||||
* Access token for this session.
|
||||
*/
|
||||
final String accessToken;
|
||||
/**
|
||||
* Session expiration timestamp in milliseconds.
|
||||
*/
|
||||
final Long expirationTime;
|
||||
/**
|
||||
* Authentication cookies for this session.
|
||||
*/
|
||||
final String cookies;
|
||||
|
||||
/**
|
||||
* @param username Username of the account. Empty if this session does not have an authenticated user.
|
||||
* @param accessToken Access token for this session.
|
||||
* @param cookies Authentication cookies for this session.
|
||||
*/
|
||||
Session(@Nullable String username, String accessToken, String cookies) {
|
||||
this(username, accessToken, System.currentTimeMillis() + 60 * 60 * 1000, cookies);
|
||||
}
|
||||
|
||||
private Session(@Nullable String username, String accessToken, long expirationTime, String cookies) {
|
||||
this.username = username;
|
||||
this.accessToken = accessToken;
|
||||
this.expirationTime = expirationTime;
|
||||
this.cookies = cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of milliseconds until the access token expires.
|
||||
*/
|
||||
long accessTokenExpiresInMillis() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
return expirationTime - currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of seconds until the access token expires.
|
||||
*/
|
||||
int accessTokenExpiresInSeconds() {
|
||||
return (int) accessTokenExpiresInMillis() / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the access token has expired, false otherwise.
|
||||
*/
|
||||
boolean accessTokenExpired() {
|
||||
return accessTokenExpiresInMillis() <= 0;
|
||||
}
|
||||
|
||||
void save() {
|
||||
Logger.printInfo(() -> "Saving session: " + this);
|
||||
|
||||
SharedPreferences.Editor editor = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE).edit();
|
||||
|
||||
String json;
|
||||
try {
|
||||
json = new JSONObject()
|
||||
.put("accessToken", accessToken)
|
||||
.put("expirationTime", expirationTime)
|
||||
.put("cookies", cookies).toString();
|
||||
} catch (JSONException ex) {
|
||||
Logger.printException(() -> "Failed to convert session to stored credential", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
editor.putString("session_" + username, json);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static Session read(String username) {
|
||||
Logger.printInfo(() -> "Reading saved session for username: " + username);
|
||||
|
||||
SharedPreferences sharedPreferences = Utils.getContext().getSharedPreferences("revanced", MODE_PRIVATE);
|
||||
String savedJson = sharedPreferences.getString("session_" + username, null);
|
||||
if (savedJson == null) {
|
||||
Logger.printInfo(() -> "No session found in shared preferences");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject json = new JSONObject(savedJson);
|
||||
String accessToken = json.getString("accessToken");
|
||||
long expirationTime = json.getLong("expirationTime");
|
||||
String cookies = json.getString("cookies");
|
||||
|
||||
return new Session(username, accessToken, expirationTime, cookies);
|
||||
} catch (JSONException ex) {
|
||||
Logger.printException(() -> "Failed to read session from shared preferences", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Session(" +
|
||||
"username=" + username +
|
||||
", accessToken=" + accessToken +
|
||||
", expirationTime=" + expirationTime +
|
||||
", cookies=" + cookies +
|
||||
')';
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,25 @@
|
||||
package app.revanced.extension.spotify.misc.fix;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class SpoofClientPatch {
|
||||
private static LoginRequestListener listener;
|
||||
private static RequestListener listener;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <br>
|
||||
* Start login server.
|
||||
* Injection point. Launch requests listener server.
|
||||
*/
|
||||
public static void listen(int port) {
|
||||
public synchronized static void launchListener(int port) {
|
||||
if (listener != null) {
|
||||
Logger.printInfo(() -> "Listener already running on port " + port);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
listener = new LoginRequestListener(port);
|
||||
listener.start();
|
||||
Logger.printInfo(() -> "Listener running on port " + port);
|
||||
Logger.printInfo(() -> "Launching listener on port " + port);
|
||||
listener = new RequestListener(port);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "listen failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <br>
|
||||
* Launch login web view.
|
||||
*/
|
||||
public static void login(LayoutInflater inflater) {
|
||||
try {
|
||||
WebApp.login(inflater.getContext());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "login failure", ex);
|
||||
Logger.printException(() -> "launchListener failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
package app.revanced.extension.spotify.misc.fix;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.view.*;
|
||||
import android.webkit.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.spotify.UserAgent;
|
||||
|
||||
class WebApp {
|
||||
private static final String OPEN_SPOTIFY_COM = "open.spotify.com";
|
||||
private static final String OPEN_SPOTIFY_COM_URL = "https://" + OPEN_SPOTIFY_COM;
|
||||
private static final String OPEN_SPOTIFY_COM_PREFERENCES_URL = OPEN_SPOTIFY_COM_URL + "/preferences";
|
||||
private static final String ACCOUNTS_SPOTIFY_COM_LOGIN_URL = "https://accounts.spotify.com/login?continue=" +
|
||||
"https%3A%2F%2Fopen.spotify.com%2Fpreferences";
|
||||
|
||||
private static final int GET_SESSION_TIMEOUT_SECONDS = 10;
|
||||
private static final String JAVASCRIPT_INTERFACE_NAME = "androidInterface";
|
||||
private static final String USER_AGENT = getWebUserAgent();
|
||||
|
||||
/**
|
||||
* Current webview in use. Any use of the object must be done on the main thread.
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static volatile WebView currentWebView;
|
||||
|
||||
/**
|
||||
* A session obtained from the webview after logging in or renewing the session.
|
||||
*/
|
||||
@Nullable
|
||||
static volatile Session currentSession;
|
||||
|
||||
static void login(Context context) {
|
||||
Logger.printInfo(() -> "Starting login");
|
||||
|
||||
Dialog dialog = new Dialog(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen);
|
||||
|
||||
// Ensure that the keyboard does not cover the webview content.
|
||||
Window window = dialog.getWindow();
|
||||
//noinspection StatementWithEmptyBody
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.getDecorView().setOnApplyWindowInsetsListener((v, insets) -> {
|
||||
v.setPadding(0, 0, 0, insets.getInsets(WindowInsets.Type.ime()).bottom);
|
||||
|
||||
return WindowInsets.CONSUMED;
|
||||
});
|
||||
} else {
|
||||
// TODO: Implement for lower Android versions.
|
||||
}
|
||||
|
||||
newWebView(
|
||||
// Can't use Utils.getContext() here, because autofill won't work.
|
||||
// See https://stackoverflow.com/a/79182053/11213244.
|
||||
context,
|
||||
new WebViewCallback() {
|
||||
@Override
|
||||
void onInitialized(WebView webView) {
|
||||
// Ensure that cookies are cleared before loading the login page.
|
||||
CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
|
||||
Logger.printInfo(() -> "Loading URL: " + ACCOUNTS_SPOTIFY_COM_LOGIN_URL);
|
||||
webView.loadUrl(ACCOUNTS_SPOTIFY_COM_LOGIN_URL);
|
||||
});
|
||||
|
||||
dialog.setCancelable(false);
|
||||
dialog.setContentView(webView);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
void onLoggedIn(String cookies) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
void onReceivedSession(WebView webView, Session session) {
|
||||
Logger.printInfo(() -> "Received session from login: " + session);
|
||||
currentSession = session;
|
||||
currentWebView = null;
|
||||
webView.stopLoading();
|
||||
webView.destroy();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static void renewSession(String cookies) {
|
||||
Logger.printInfo(() -> "Renewing session with cookies: " + cookies);
|
||||
|
||||
CountDownLatch getSessionLatch = new CountDownLatch(1);
|
||||
|
||||
newWebView(
|
||||
Utils.getContext(),
|
||||
new WebViewCallback() {
|
||||
@Override
|
||||
public void onInitialized(WebView webView) {
|
||||
Logger.printInfo(() -> "Loading URL: " + OPEN_SPOTIFY_COM_PREFERENCES_URL +
|
||||
" with cookies: " + cookies);
|
||||
setCookies(cookies);
|
||||
webView.loadUrl(OPEN_SPOTIFY_COM_PREFERENCES_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedSession(WebView webView, Session session) {
|
||||
Logger.printInfo(() -> "Received session: " + session);
|
||||
currentSession = session;
|
||||
getSessionLatch.countDown();
|
||||
currentWebView = null;
|
||||
webView.stopLoading();
|
||||
webView.destroy();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
final boolean isAcquired = getSessionLatch.await(GET_SESSION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!isAcquired) {
|
||||
Logger.printException(() -> "Failed to retrieve session within " + GET_SESSION_TIMEOUT_SECONDS + " seconds");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Logger.printException(() -> "Interrupted while waiting to retrieve session", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
currentWebView = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* All methods are called on the main thread.
|
||||
*/
|
||||
abstract static class WebViewCallback {
|
||||
void onInitialized(WebView webView) {
|
||||
}
|
||||
|
||||
void onLoggedIn(String cookies) {
|
||||
}
|
||||
|
||||
void onReceivedSession(WebView webView, Session session) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private static void newWebView(
|
||||
Context context,
|
||||
WebViewCallback webViewCallback
|
||||
) {
|
||||
Utils.runOnMainThreadNowOrLater(() -> {
|
||||
WebView webView = currentWebView;
|
||||
if (webView != null) {
|
||||
// Old webview is still hanging around.
|
||||
// Could happen if the network request failed and thus no callback is made.
|
||||
// But in practice this never happens.
|
||||
Logger.printException(() -> "Cleaning up prior webview");
|
||||
webView.stopLoading();
|
||||
webView.destroy();
|
||||
}
|
||||
|
||||
webView = new WebView(context);
|
||||
WebSettings settings = webView.getSettings();
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setUserAgentString(USER_AGENT);
|
||||
// WebViewClient is always called off the main thread,
|
||||
// but callback interface methods are called on the main thread.
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
if (OPEN_SPOTIFY_COM.equals(request.getUrl().getHost())) {
|
||||
Utils.runOnMainThread(() -> webViewCallback.onLoggedIn(getCurrentCookies()));
|
||||
}
|
||||
|
||||
return super.shouldInterceptRequest(view, request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
Logger.printInfo(() -> "Page started loading: " + url);
|
||||
|
||||
if (!url.startsWith(OPEN_SPOTIFY_COM_URL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printInfo(() -> "Evaluating script to get session on url: " + url);
|
||||
String getSessionScript = "Object.defineProperty(Object.prototype, \"_username\", {" +
|
||||
" configurable: true," +
|
||||
" set(username) {" +
|
||||
" accessToken = this._builder?.accessToken;" +
|
||||
" if (accessToken) {" +
|
||||
" " + JAVASCRIPT_INTERFACE_NAME + ".getSession(username, accessToken);" +
|
||||
" delete Object.prototype._username;" +
|
||||
" }" +
|
||||
" " +
|
||||
" Object.defineProperty(this, \"_username\", {" +
|
||||
" configurable: true," +
|
||||
" enumerable: true," +
|
||||
" writable: true," +
|
||||
" value: username" +
|
||||
" })" +
|
||||
" " +
|
||||
" }" +
|
||||
"});";
|
||||
|
||||
view.evaluateJavascript(getSessionScript, null);
|
||||
}
|
||||
});
|
||||
|
||||
final WebView callbackWebView = webView;
|
||||
webView.addJavascriptInterface(new Object() {
|
||||
@SuppressWarnings("unused")
|
||||
@JavascriptInterface
|
||||
public void getSession(String username, String accessToken) {
|
||||
Session session = new Session(username, accessToken, getCurrentCookies());
|
||||
Utils.runOnMainThread(() -> webViewCallback.onReceivedSession(callbackWebView, session));
|
||||
}
|
||||
}, JAVASCRIPT_INTERFACE_NAME);
|
||||
|
||||
currentWebView = webView;
|
||||
|
||||
CookieManager.getInstance().removeAllCookies((anyRemoved) -> {
|
||||
Logger.printInfo(() -> "WebView initialized with user agent: " + USER_AGENT);
|
||||
webViewCallback.onInitialized(currentWebView);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static String getWebUserAgent() {
|
||||
String userAgentString = WebSettings.getDefaultUserAgent(Utils.getContext());
|
||||
try {
|
||||
return new UserAgent(userAgentString)
|
||||
.withCommentReplaced("Android", "Windows NT 10.0; Win64; x64")
|
||||
.withoutProduct("Mobile")
|
||||
.toString();
|
||||
} catch (IllegalArgumentException e) {
|
||||
userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edge/137.0.0.0";
|
||||
String fallback = userAgentString;
|
||||
Logger.printException(() -> "Failed to get user agent, falling back to " + fallback, e);
|
||||
}
|
||||
|
||||
return userAgentString;
|
||||
}
|
||||
|
||||
private static String getCurrentCookies() {
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
return cookieManager.getCookie(OPEN_SPOTIFY_COM_URL);
|
||||
}
|
||||
|
||||
private static void setCookies(@NonNull String cookies) {
|
||||
CookieManager cookieManager = CookieManager.getInstance();
|
||||
|
||||
String[] cookiesList = cookies.split(";");
|
||||
for (String cookie : cookiesList) {
|
||||
cookieManager.setCookie(OPEN_SPOTIFY_COM_URL, cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package spotify.clienttoken.data.v0;
|
||||
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
option java_package = "app.revanced.extension.spotify.misc.fix.clienttoken.data.v0";
|
||||
|
||||
message ClientTokenRequest {
|
||||
ClientTokenRequestType request_type = 1;
|
||||
|
||||
oneof request {
|
||||
ClientDataRequest client_data = 2;
|
||||
}
|
||||
}
|
||||
|
||||
enum ClientTokenRequestType {
|
||||
REQUEST_UNKNOWN = 0;
|
||||
REQUEST_CLIENT_DATA_REQUEST = 1;
|
||||
REQUEST_CHALLENGE_ANSWERS_REQUEST = 2;
|
||||
}
|
||||
|
||||
message ClientDataRequest {
|
||||
string client_version = 1;
|
||||
string client_id = 2;
|
||||
|
||||
oneof data {
|
||||
ConnectivitySdkData connectivity_sdk_data = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message ConnectivitySdkData {
|
||||
PlatformSpecificData platform_specific_data = 1;
|
||||
string device_id = 2;
|
||||
}
|
||||
|
||||
message PlatformSpecificData {
|
||||
oneof data {
|
||||
NativeIOSData ios = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message NativeIOSData {
|
||||
int32 user_interface_idiom = 1;
|
||||
bool target_iphone_simulator = 2;
|
||||
string hw_machine = 3;
|
||||
string system_version = 4;
|
||||
string simulator_model_identifier = 5;
|
||||
}
|
||||
|
||||
message ClientTokenResponse {
|
||||
ClientTokenResponseType response_type = 1;
|
||||
|
||||
oneof response {
|
||||
GrantedTokenResponse granted_token = 2;
|
||||
}
|
||||
}
|
||||
|
||||
enum ClientTokenResponseType {
|
||||
RESPONSE_UNKNOWN = 0;
|
||||
RESPONSE_GRANTED_TOKEN_RESPONSE = 1;
|
||||
RESPONSE_CHALLENGES_RESPONSE = 2;
|
||||
}
|
||||
|
||||
message GrantedTokenResponse {
|
||||
string token = 1;
|
||||
int32 expires_after_seconds = 2;
|
||||
int32 refresh_after_seconds = 3;
|
||||
repeated TokenDomain domains = 4;
|
||||
}
|
||||
|
||||
message TokenDomain {
|
||||
string domain = 1;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package spotify.login5.v4;
|
||||
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
option java_package = "app.revanced.extension.spotify.login5.v4.proto";
|
||||
|
||||
message StoredCredential {
|
||||
string username = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
message LoginRequest {
|
||||
oneof login_method {
|
||||
StoredCredential stored_credential = 100;
|
||||
}
|
||||
}
|
||||
|
||||
message LoginOk {
|
||||
string username = 1;
|
||||
string access_token = 2;
|
||||
bytes stored_credential = 3;
|
||||
int32 access_token_expires_in = 4;
|
||||
}
|
||||
|
||||
message LoginResponse {
|
||||
oneof response {
|
||||
LoginOk ok = 1;
|
||||
LoginError error = 2;
|
||||
}
|
||||
}
|
||||
|
||||
enum LoginError {
|
||||
UNKNOWN_ERROR = 0;
|
||||
INVALID_CREDENTIALS = 1;
|
||||
BAD_REQUEST = 2;
|
||||
UNSUPPORTED_LOGIN_PROTOCOL = 3;
|
||||
TIMEOUT = 4;
|
||||
UNKNOWN_IDENTIFIER = 5;
|
||||
TOO_MANY_ATTEMPTS = 6;
|
||||
INVALID_PHONENUMBER = 7;
|
||||
TRY_AGAIN_LATER = 8;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package app.revanced;
|
||||
|
||||
public interface ContextMenuItemPlaceholder {
|
||||
Object getViewModel();
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.spotify.useraccount.v1;
|
||||
|
||||
/**
|
||||
* Used for target 8.6.98.900. Class is still present in newer app targets.
|
||||
*/
|
||||
public class AccountAttribute {
|
||||
public Object value_;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
plugins {
|
||||
java
|
||||
antlr
|
||||
}
|
||||
|
||||
dependencies {
|
||||
antlr(libs.antlr4)
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
tasks {
|
||||
generateGrammarSource {
|
||||
arguments = listOf("-visitor")
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
grammar UserAgent;
|
||||
|
||||
@header { package app.revanced.extension.spotify; }
|
||||
|
||||
userAgent
|
||||
: product (WS product)* EOF
|
||||
;
|
||||
|
||||
product
|
||||
: name ('/' version)? (WS comment)?
|
||||
;
|
||||
|
||||
name
|
||||
: STRING
|
||||
;
|
||||
|
||||
version
|
||||
: STRING ('.' STRING)*
|
||||
;
|
||||
|
||||
comment
|
||||
: COMMENT
|
||||
;
|
||||
|
||||
COMMENT
|
||||
: '(' ~ ')'* ')'
|
||||
;
|
||||
|
||||
STRING
|
||||
: [a-zA-Z0-9]+
|
||||
;
|
||||
|
||||
WS
|
||||
: [ \r\n]+
|
||||
;
|
||||
@@ -1,60 +0,0 @@
|
||||
package app.revanced.extension.spotify;
|
||||
|
||||
import org.antlr.v4.runtime.CharStream;
|
||||
import org.antlr.v4.runtime.CharStreams;
|
||||
import org.antlr.v4.runtime.CommonTokenStream;
|
||||
import org.antlr.v4.runtime.TokenStreamRewriter;
|
||||
import org.antlr.v4.runtime.tree.ParseTreeWalker;
|
||||
|
||||
public class UserAgent {
|
||||
private final UserAgentParser.UserAgentContext tree;
|
||||
private final TokenStreamRewriter rewriter;
|
||||
private final ParseTreeWalker walker;
|
||||
|
||||
public UserAgent(String userAgentString) {
|
||||
CharStream input = CharStreams.fromString(userAgentString);
|
||||
UserAgentLexer lexer = new UserAgentLexer(input);
|
||||
CommonTokenStream tokens = new CommonTokenStream(lexer);
|
||||
|
||||
tree = new UserAgentParser(tokens).userAgent();
|
||||
walker = new ParseTreeWalker();
|
||||
rewriter = new TokenStreamRewriter(tokens);
|
||||
}
|
||||
|
||||
public UserAgent withoutProduct(String name) {
|
||||
walker.walk(new UserAgentBaseListener() {
|
||||
@Override
|
||||
public void exitProduct(UserAgentParser.ProductContext ctx) {
|
||||
if (!ctx.name().getText().contains(name)) return;
|
||||
|
||||
int startIndex = ctx.getStart().getTokenIndex();
|
||||
if (startIndex != 0) startIndex -= 1; // Also remove the preceding whitespace.
|
||||
|
||||
int stopIndex = ctx.getStop().getTokenIndex();
|
||||
|
||||
|
||||
rewriter.delete(startIndex, stopIndex);
|
||||
}
|
||||
}, tree);
|
||||
|
||||
return new UserAgent(rewriter.getText().trim());
|
||||
}
|
||||
|
||||
public UserAgent withCommentReplaced(String containing, String replacement) {
|
||||
walker.walk(new UserAgentBaseListener() {
|
||||
@Override
|
||||
public void exitComment(UserAgentParser.CommentContext ctx) {
|
||||
if (ctx.getText().contains(containing)) {
|
||||
rewriter.replace(ctx.getStart(), ctx.getStop(), "(" + replacement + ")");
|
||||
}
|
||||
}
|
||||
}, tree);
|
||||
|
||||
return new UserAgent(rewriter.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return rewriter.getText();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ChangeHeaderPatch {
|
||||
|
||||
public enum HeaderLogo {
|
||||
DEFAULT(null, null),
|
||||
REGULAR("ytWordmarkHeader", "yt_ringo2_wordmark_header"),
|
||||
PREMIUM("ytPremiumWordmarkHeader", "yt_ringo2_premium_wordmark_header"),
|
||||
REVANCED("revanced_header_logo", "revanced_header_logo"),
|
||||
REVANCED_MINIMAL("revanced_header_logo_minimal", "revanced_header_logo_minimal"),
|
||||
CUSTOM("custom_header", "custom_header");
|
||||
|
||||
@Nullable
|
||||
private final String attributeName;
|
||||
@Nullable
|
||||
private final String drawableName;
|
||||
|
||||
HeaderLogo(@Nullable String attributeName, @Nullable String drawableName) {
|
||||
this.attributeName = attributeName;
|
||||
this.drawableName = drawableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The attribute id of this header logo, or NULL if the logo should not be replaced.
|
||||
*/
|
||||
@Nullable
|
||||
private Integer getAttributeId() {
|
||||
if (attributeName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int identifier = Utils.getResourceIdentifier(attributeName, "attr");
|
||||
if (identifier == 0) {
|
||||
// Identifier is zero if custom header setting was included in imported settings
|
||||
// and a custom image was not included during patching.
|
||||
Logger.printDebug(() -> "Could not find attribute: " + drawableName);
|
||||
Settings.HEADER_LOGO.resetToDefault();
|
||||
return null;
|
||||
}
|
||||
|
||||
return identifier;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Drawable getDrawable() {
|
||||
if (drawableName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String drawableFullName = drawableName + (Utils.isDarkModeEnabled()
|
||||
? "_dark"
|
||||
: "_light");
|
||||
|
||||
final int identifier = Utils.getResourceIdentifier(drawableFullName, "drawable");
|
||||
if (identifier == 0) {
|
||||
Logger.printDebug(() -> "Could not find drawable: " + drawableFullName);
|
||||
Settings.HEADER_LOGO.resetToDefault();
|
||||
return null;
|
||||
}
|
||||
return Utils.getContext().getDrawable(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getHeaderAttributeId(int original) {
|
||||
return Objects.requireNonNullElse(Settings.HEADER_LOGO.get().getAttributeId(), original);
|
||||
}
|
||||
|
||||
public static Drawable getDrawable(Drawable original) {
|
||||
Drawable logo = Settings.HEADER_LOGO.get().getDrawable();
|
||||
if (logo != null) {
|
||||
return logo;
|
||||
}
|
||||
|
||||
// TODO: If 'Hide Doodles' is enabled, this will force the regular logo regardless
|
||||
// what account the user has. This can be improved the next time a Doodle is
|
||||
// active and the attribute id is passed to this method so the correct
|
||||
// regular/premium logo is returned.
|
||||
logo = HeaderLogo.REGULAR.getDrawable();
|
||||
if (logo != null) {
|
||||
return logo;
|
||||
}
|
||||
|
||||
// Should never happen.
|
||||
Logger.printException(() -> "Could not find regular header logo resource");
|
||||
return original;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class DisableDoubleTapActionsPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @return If "should skip to chapter start" flag is set.
|
||||
*/
|
||||
public static boolean disableDoubleTapChapters(boolean original) {
|
||||
return original && !Settings.DISABLE_CHAPTER_SKIP_DOUBLE_TAP.get();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import static app.revanced.extension.youtube.settings.preference.ExternalDownloaderPreference.showDialogIfAppIsNotInstalled;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringRef;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@@ -36,7 +34,7 @@ public final class DownloadsPatch {
|
||||
*
|
||||
* Appears to always be called from the main thread.
|
||||
*/
|
||||
public static boolean inAppDownloadButtonOnClick(@NonNull String videoId) {
|
||||
public static boolean inAppDownloadButtonOnClick(String videoId) {
|
||||
try {
|
||||
if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) {
|
||||
return false;
|
||||
@@ -48,6 +46,9 @@ public final class DownloadsPatch {
|
||||
boolean isActivityContext = true;
|
||||
if (context == null) {
|
||||
// Utils context is the application context, and not an activity context.
|
||||
//
|
||||
// Edit: This check may no longer be needed since YT can now
|
||||
// only be launched from the main Activity (embedded usage in other apps no longer works).
|
||||
context = Utils.getContext();
|
||||
isActivityContext = false;
|
||||
}
|
||||
@@ -64,8 +65,7 @@ public final class DownloadsPatch {
|
||||
* @param isActivityContext If the context parameter is for an Activity. If this is false, then
|
||||
* the downloader is opened as a new task (which forces YT to minimize).
|
||||
*/
|
||||
public static void launchExternalDownloader(@NonNull String videoId,
|
||||
@NonNull Context context, boolean isActivityContext) {
|
||||
public static void launchExternalDownloader(String videoId, Context context, boolean isActivityContext) {
|
||||
try {
|
||||
Objects.requireNonNull(videoId);
|
||||
Logger.printDebug(() -> "Launching external downloader with context: " + context);
|
||||
@@ -73,16 +73,8 @@ public final class DownloadsPatch {
|
||||
// Trim string to avoid any accidental whitespace.
|
||||
var downloaderPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim();
|
||||
|
||||
boolean packageEnabled = false;
|
||||
try {
|
||||
packageEnabled = context.getPackageManager().getApplicationInfo(downloaderPackageName, 0).enabled;
|
||||
} catch (PackageManager.NameNotFoundException error) {
|
||||
Logger.printDebug(() -> "External downloader could not be found: " + error);
|
||||
}
|
||||
|
||||
// If the package is not installed, show the toast
|
||||
if (!packageEnabled) {
|
||||
Utils.showToastLong(StringRef.str("revanced_external_downloader_not_installed_warning", downloaderPackageName));
|
||||
// If the package is not installed, show a dialog.
|
||||
if (showDialogIfAppIsNotInstalled(context, downloaderPackageName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ public final class HideRelatedVideoOverlayPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean hideRelatedVideoOverlay() {
|
||||
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
|
||||
return Settings.HIDE_RELATED_VIDEOS_OVERLAY.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,11 @@ public final class AnnouncementsPatch {
|
||||
int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue;
|
||||
try {
|
||||
final var announcementIds = new JSONArray(jsonString);
|
||||
if (announcementIds.length() == 0) return true;
|
||||
|
||||
id = announcementIds.getJSONObject(0).getInt("id");
|
||||
|
||||
} catch (Throwable ex) {
|
||||
Logger.printException(() -> "Failed to parse announcement IDs", ex);
|
||||
Logger.printException(() -> "Failed to parse announcement ID", ex);
|
||||
}
|
||||
|
||||
// Do not show the announcement, if the last announcement id is the same as the current one.
|
||||
|
||||
@@ -10,8 +10,8 @@ import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
public class AnnouncementsRoutes {
|
||||
private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v4";
|
||||
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=youtube");
|
||||
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=youtube");
|
||||
public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=\uD83C\uDF9E\uFE0F%20YouTube");
|
||||
public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=\uD83C\uDF9E\uFE0F%20YouTube");
|
||||
|
||||
private AnnouncementsRoutes() {
|
||||
}
|
||||
|
||||
@@ -34,10 +34,6 @@ public final class AdsFilter extends Filter {
|
||||
private final StringFilterGroup playerShoppingShelf;
|
||||
private final ByteArrayFilterGroup playerShoppingShelfBuffer;
|
||||
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final ByteArrayFilterGroup visitStoreButton;
|
||||
|
||||
private final StringFilterGroup shoppingLinks;
|
||||
|
||||
public AdsFilter() {
|
||||
exceptions.addPatterns(
|
||||
@@ -91,6 +87,7 @@ public final class AdsFilter extends Filter {
|
||||
"text_image_no_button_layout", // Tablet layout search results.
|
||||
"video_display_button_group_layout",
|
||||
"video_display_carousel_button_group_layout",
|
||||
"video_display_carousel_buttoned_short_dr_layout",
|
||||
"video_display_full_buttoned_short_dr_layout",
|
||||
"video_display_full_layout",
|
||||
"watch_metadata_app_promo"
|
||||
@@ -107,21 +104,19 @@ public final class AdsFilter extends Filter {
|
||||
);
|
||||
|
||||
final var viewProducts = new StringFilterGroup(
|
||||
Settings.HIDE_PRODUCTS_BANNER,
|
||||
Settings.HIDE_VIEW_PRODUCTS_BANNER,
|
||||
"product_item",
|
||||
"products_in_video",
|
||||
"shopping_overlay.eml", // Video player overlay shopping links.
|
||||
"shopping_carousel.eml" // Channel profile shopping shelf.
|
||||
"shopping_overlay.eml" // Video player overlay shopping links.
|
||||
);
|
||||
|
||||
shoppingLinks = new StringFilterGroup(
|
||||
final var shoppingLinks = new StringFilterGroup(
|
||||
Settings.HIDE_SHOPPING_LINKS,
|
||||
"expandable_list",
|
||||
"shopping_description_shelf.eml"
|
||||
);
|
||||
|
||||
playerShoppingShelf = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYER_STORE_SHELF,
|
||||
Settings.HIDE_CREATOR_STORE_SHELF,
|
||||
"horizontal_shelf.eml"
|
||||
);
|
||||
|
||||
@@ -130,17 +125,6 @@ public final class AdsFilter extends Filter {
|
||||
"shopping_item_card_list"
|
||||
);
|
||||
|
||||
channelProfile = new StringFilterGroup(
|
||||
Settings.HIDE_VISIT_STORE_BUTTON,
|
||||
"channel_profile.eml",
|
||||
"page_header.eml"
|
||||
);
|
||||
|
||||
visitStoreButton = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"header_store_button"
|
||||
);
|
||||
|
||||
final var webLinkPanel = new StringFilterGroup(
|
||||
Settings.HIDE_WEB_SEARCH_RESULTS,
|
||||
"web_link_panel"
|
||||
@@ -148,7 +132,8 @@ public final class AdsFilter extends Filter {
|
||||
|
||||
final var merchandise = new StringFilterGroup(
|
||||
Settings.HIDE_MERCHANDISE_BANNERS,
|
||||
"product_carousel"
|
||||
"product_carousel",
|
||||
"shopping_carousel.eml" // Channel profile shopping shelf.
|
||||
);
|
||||
|
||||
final var selfSponsor = new StringFilterGroup(
|
||||
@@ -157,16 +142,15 @@ public final class AdsFilter extends Filter {
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
fullscreenAd,
|
||||
generalAds,
|
||||
merchandise,
|
||||
viewProducts,
|
||||
selfSponsor,
|
||||
fullscreenAd,
|
||||
channelProfile,
|
||||
webLinkPanel,
|
||||
shoppingLinks,
|
||||
movieAds,
|
||||
playerShoppingShelf,
|
||||
movieAds
|
||||
selfSponsor,
|
||||
shoppingLinks,
|
||||
viewProducts,
|
||||
webLinkPanel
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,11 +161,6 @@ public final class AdsFilter extends Filter {
|
||||
return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
// Check for the index because of likelihood of false positives.
|
||||
if (contentIndex != 0 && matchedGroup == shoppingLinks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exceptions.matches(path)) {
|
||||
return false;
|
||||
}
|
||||
@@ -193,10 +172,6 @@ public final class AdsFilter extends Filter {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchedGroup == channelProfile) {
|
||||
return visitStoreButton.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ package app.revanced.extension.youtube.patches.components;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
final class CommentsFilter extends Filter {
|
||||
|
||||
private final StringFilterGroup filterChipBar;
|
||||
private final StringFilterGroup chipBar;
|
||||
private final ByteArrayFilterGroup aiCommentsSummary;
|
||||
|
||||
public CommentsFilter() {
|
||||
@@ -16,6 +17,21 @@ final class CommentsFilter extends Filter {
|
||||
"live_chat_summary_banner.eml"
|
||||
);
|
||||
|
||||
chipBar = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_AI_SUMMARY,
|
||||
"chip_bar.eml"
|
||||
);
|
||||
|
||||
aiCommentsSummary = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"yt_fill_spark_"
|
||||
);
|
||||
|
||||
var channelGuidelines = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_CHANNEL_GUIDELINES,
|
||||
"channel_guidelines_entry_banner"
|
||||
);
|
||||
|
||||
var commentsByMembers = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_BY_MEMBERS_HEADER,
|
||||
"sponsorships_comments_header.eml",
|
||||
@@ -28,6 +44,11 @@ final class CommentsFilter extends Filter {
|
||||
"_comments"
|
||||
);
|
||||
|
||||
var communityGuidelines = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_COMMUNITY_GUIDELINES,
|
||||
"community_guidelines"
|
||||
);
|
||||
|
||||
var createAShort = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_CREATE_A_SHORT_BUTTON,
|
||||
"composer_short_creation_button.eml"
|
||||
@@ -50,33 +71,28 @@ final class CommentsFilter extends Filter {
|
||||
"composer_timestamp_button.eml"
|
||||
);
|
||||
|
||||
filterChipBar = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_AI_SUMMARY,
|
||||
"filter_chip_bar.eml"
|
||||
);
|
||||
|
||||
aiCommentsSummary = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"yt_fill_spark_"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
channelGuidelines,
|
||||
chatSummary,
|
||||
chipBar,
|
||||
commentsByMembers,
|
||||
comments,
|
||||
communityGuidelines,
|
||||
createAShort,
|
||||
previewComment,
|
||||
thanksButton,
|
||||
timestampButton,
|
||||
filterChipBar
|
||||
timestampButton
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == filterChipBar) {
|
||||
return aiCommentsSummary.check(protobufBufferArray).isFiltered();
|
||||
if (matchedGroup == chipBar) {
|
||||
// Playlist sort button uses same components and must only filter if the player is opened.
|
||||
return PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||
&& aiCommentsSummary.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -4,12 +4,14 @@ import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButt
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.patches.ChangeHeaderPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
@@ -30,7 +32,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
);
|
||||
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
private final StringFilterGroup inFeedSurvey;
|
||||
private final StringFilterGroup surveys;
|
||||
private final StringFilterGroup notifyMe;
|
||||
private final StringFilterGroup singleItemInformationPanel;
|
||||
private final StringFilterGroup expandableMetadata;
|
||||
@@ -40,6 +42,8 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
private final StringFilterGroup horizontalShelves;
|
||||
private final ByteArrayFilterGroup ticketShelf;
|
||||
private final StringFilterGroup chipBar;
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final ByteArrayFilterGroupList channelProfileBuffer;
|
||||
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions.addPatterns(
|
||||
@@ -81,18 +85,13 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"poll_post_responsive_root.eml"
|
||||
);
|
||||
|
||||
final var communityGuidelines = new StringFilterGroup(
|
||||
Settings.HIDE_COMMUNITY_GUIDELINES,
|
||||
"community_guidelines"
|
||||
);
|
||||
|
||||
final var subscribersCommunityGuidelines = new StringFilterGroup(
|
||||
Settings.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES,
|
||||
"sponsorships_comments_upsell"
|
||||
);
|
||||
|
||||
final var channelMemberShelf = new StringFilterGroup(
|
||||
Settings.HIDE_CHANNEL_MEMBER_SHELF,
|
||||
final var channelMembersShelf = new StringFilterGroup(
|
||||
Settings.HIDE_MEMBERS_SHELF,
|
||||
"member_recognition_shelf"
|
||||
);
|
||||
|
||||
@@ -111,8 +110,8 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"chip_bar"
|
||||
);
|
||||
|
||||
inFeedSurvey = new StringFilterGroup(
|
||||
Settings.HIDE_FEED_SURVEY,
|
||||
surveys = new StringFilterGroup(
|
||||
Settings.HIDE_SURVEYS,
|
||||
"in_feed_survey",
|
||||
"slimline_survey",
|
||||
"feed_nudge"
|
||||
@@ -139,13 +138,13 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
);
|
||||
|
||||
final var latestPosts = new StringFilterGroup(
|
||||
Settings.HIDE_HIDE_LATEST_POSTS,
|
||||
Settings.HIDE_LATEST_POSTS,
|
||||
"post_shelf"
|
||||
);
|
||||
|
||||
final var channelGuidelines = new StringFilterGroup(
|
||||
Settings.HIDE_HIDE_CHANNEL_GUIDELINES,
|
||||
"channel_guidelines_entry_banner"
|
||||
final var channelLinksPreview = new StringFilterGroup(
|
||||
Settings.HIDE_LINKS_PREVIEW,
|
||||
"attribution.eml"
|
||||
);
|
||||
|
||||
final var emergencyBox = new StringFilterGroup(
|
||||
@@ -170,7 +169,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
);
|
||||
|
||||
expandableMetadata = new StringFilterGroup(
|
||||
Settings.HIDE_EXPANDABLE_CHIP,
|
||||
Settings.HIDE_EXPANDABLE_CARD,
|
||||
"inline_expander"
|
||||
);
|
||||
|
||||
@@ -200,7 +199,6 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"image_shelf"
|
||||
);
|
||||
|
||||
|
||||
final var timedReactions = new StringFilterGroup(
|
||||
Settings.HIDE_TIMED_REACTIONS,
|
||||
"emoji_control_panel",
|
||||
@@ -227,7 +225,6 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"sponsorships"
|
||||
);
|
||||
|
||||
|
||||
final var channelWatermark = new StringFilterGroup(
|
||||
Settings.HIDE_VIDEO_CHANNEL_WATERMARK,
|
||||
"featured_channel_watermark_overlay"
|
||||
@@ -238,11 +235,27 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"mixed_content_shelf"
|
||||
);
|
||||
|
||||
final var searchResultRecommendationLabels = new StringFilterGroup(
|
||||
Settings.HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS,
|
||||
final var videoRecommendationLabels = new StringFilterGroup(
|
||||
Settings.HIDE_VIDEO_RECOMMENDATION_LABELS,
|
||||
"endorsement_header_footer.eml"
|
||||
);
|
||||
|
||||
channelProfile = new StringFilterGroup(
|
||||
null,
|
||||
"channel_profile.eml",
|
||||
"page_header.eml"
|
||||
);
|
||||
channelProfileBuffer = new ByteArrayFilterGroupList();
|
||||
channelProfileBuffer.addAll(new ByteArrayFilterGroup(
|
||||
Settings.HIDE_VISIT_STORE_BUTTON,
|
||||
"header_store_button"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_VISIT_COMMUNITY_BUTTON,
|
||||
"community_button"
|
||||
)
|
||||
);
|
||||
|
||||
horizontalShelves = new StringFilterGroup(
|
||||
Settings.HIDE_HORIZONTAL_SHELVES,
|
||||
"horizontal_video_shelf.eml",
|
||||
@@ -253,40 +266,40 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
|
||||
ticketShelf = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_TICKET_SHELF,
|
||||
"ticket"
|
||||
"ticket_item.eml"
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
expandableMetadata,
|
||||
inFeedSurvey,
|
||||
notifyMe,
|
||||
compactChannelBar,
|
||||
communityPosts,
|
||||
paidPromotion,
|
||||
searchResultRecommendationLabels,
|
||||
latestPosts,
|
||||
artistCard,
|
||||
audioTrackButton,
|
||||
channelLinksPreview,
|
||||
channelMembersShelf,
|
||||
channelProfile,
|
||||
channelWatermark,
|
||||
communityGuidelines,
|
||||
chipBar,
|
||||
compactBanner,
|
||||
compactChannelBar,
|
||||
compactChannelBarInner,
|
||||
communityPosts,
|
||||
emergencyBox,
|
||||
expandableMetadata,
|
||||
forYouShelf,
|
||||
horizontalShelves,
|
||||
imageShelf,
|
||||
infoPanel,
|
||||
latestPosts,
|
||||
medicalPanel,
|
||||
notifyMe,
|
||||
paidPromotion,
|
||||
playables,
|
||||
quickActions,
|
||||
relatedVideos,
|
||||
compactBanner,
|
||||
compactChannelBarInner,
|
||||
medicalPanel,
|
||||
infoPanel,
|
||||
singleItemInformationPanel,
|
||||
emergencyBox,
|
||||
subscribersCommunityGuidelines,
|
||||
subscriptionsChipBar,
|
||||
chipBar,
|
||||
channelGuidelines,
|
||||
audioTrackButton,
|
||||
artistCard,
|
||||
surveys,
|
||||
timedReactions,
|
||||
imageShelf,
|
||||
channelMemberShelf,
|
||||
forYouShelf,
|
||||
horizontalShelves
|
||||
videoRecommendationLabels
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,10 +317,14 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
|
||||
// The groups are excluded from the filter due to the exceptions list below.
|
||||
// Filter them separately here.
|
||||
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) {
|
||||
if (matchedGroup == notifyMe || matchedGroup == surveys || matchedGroup == expandableMetadata) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (matchedGroup == channelProfile) {
|
||||
return channelProfileBuffer.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
||||
|
||||
if (matchedGroup == compactChannelBarInner) {
|
||||
@@ -332,7 +349,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
* Injection point.
|
||||
* Called from a different place then the other filters.
|
||||
*/
|
||||
public static boolean filterMixPlaylists(final Object conversionContext, @Nullable final byte[] bytes) {
|
||||
public static boolean filterMixPlaylists(Object conversionContext, @Nullable final byte[] bytes) {
|
||||
try {
|
||||
if (!Settings.HIDE_MIX_PLAYLISTS.get()) {
|
||||
return false;
|
||||
@@ -422,13 +439,11 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@Nullable
|
||||
public static Drawable hideYoodles(Drawable animatedYoodle) {
|
||||
if (HIDE_DOODLES_ENABLED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return animatedYoodle;
|
||||
public static void setDoodleDrawable(ImageView imageView, Drawable original) {
|
||||
Drawable replacement = HIDE_DOODLES_ENABLED
|
||||
? ChangeHeaderPatch.getDrawable(original)
|
||||
: original;
|
||||
imageView.setImageDrawable(replacement);
|
||||
}
|
||||
|
||||
private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get();
|
||||
|
||||
@@ -48,7 +48,7 @@ public final class LithoFilterPatch {
|
||||
/**
|
||||
* Search through a byte array for all ASCII strings.
|
||||
*/
|
||||
private static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
|
||||
static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
|
||||
// Valid ASCII values (ignore control characters).
|
||||
final int minimumAscii = 32; // 32 = space character
|
||||
final int maximumAscii = 126; // 127 = delete character
|
||||
@@ -96,7 +96,7 @@ public final class LithoFilterPatch {
|
||||
private static final class DummyFilter extends Filter { }
|
||||
|
||||
private static final Filter[] filters = new Filter[] {
|
||||
new DummyFilter() // Replaced by patch.
|
||||
new DummyFilter() // Replaced patching, do not touch.
|
||||
};
|
||||
|
||||
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
||||
@@ -108,11 +108,7 @@ public final class LithoFilterPatch {
|
||||
* Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
|
||||
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
|
||||
*/
|
||||
private static final ThreadLocal<ByteBuffer> bufferThreadLocal = new ThreadLocal<>();
|
||||
/**
|
||||
* Results of calling {@link #filter(String, StringBuilder)}.
|
||||
*/
|
||||
private static final ThreadLocal<Boolean> filterResult = new ThreadLocal<>();
|
||||
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
|
||||
|
||||
static {
|
||||
for (Filter filter : filters) {
|
||||
@@ -168,57 +164,50 @@ public final class LithoFilterPatch {
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) {
|
||||
public static void setProtoBuffer(byte[] buffer) {
|
||||
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
||||
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
||||
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
||||
// or when the calling thread eventually dies.
|
||||
if (protobufBuffer == null) {
|
||||
bufferThreadLocal.set(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.21 and lower.
|
||||
*/
|
||||
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
|
||||
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
||||
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
||||
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
||||
// or when the calling thread eventually dies.
|
||||
if (buffer == null || !buffer.hasArray()) {
|
||||
// It appears the buffer can be cleared out just before the call to #filter()
|
||||
// Ignore this null value and retain the last buffer that was set.
|
||||
Logger.printDebug(() -> "Ignoring null protobuffer");
|
||||
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
|
||||
} else {
|
||||
bufferThreadLocal.set(protobufBuffer);
|
||||
setProtoBuffer(buffer.array());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean shouldFilter() {
|
||||
Boolean shouldFilter = filterResult.get();
|
||||
return shouldFilter != null && shouldFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread, and commonly called by multiple threads at the same time.
|
||||
*/
|
||||
public static void filter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
||||
filterResult.set(handleFiltering(lithoIdentifier, pathBuilder));
|
||||
}
|
||||
|
||||
private static boolean handleFiltering(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
||||
public static boolean shouldFilter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
||||
try {
|
||||
if (pathBuilder.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteBuffer protobufBuffer = bufferThreadLocal.get();
|
||||
final byte[] bufferArray;
|
||||
byte[] buffer = bufferThreadLocal.get();
|
||||
// Potentially the buffer may have been null or never set up until now.
|
||||
// Use an empty buffer so the litho id/path filters still work correctly.
|
||||
if (protobufBuffer == null) {
|
||||
bufferArray = EMPTY_BYTE_ARRAY;
|
||||
} else if (!protobufBuffer.hasArray()) {
|
||||
Logger.printDebug(() -> "Proto buffer does not have an array, using an empty buffer array");
|
||||
bufferArray = EMPTY_BYTE_ARRAY;
|
||||
} else {
|
||||
bufferArray = protobufBuffer.array();
|
||||
if (buffer == null) {
|
||||
buffer = EMPTY_BYTE_ARRAY;
|
||||
}
|
||||
|
||||
LithoFilterParameters parameter = new LithoFilterParameters(lithoIdentifier,
|
||||
pathBuilder.toString(), bufferArray);
|
||||
LithoFilterParameters parameter = new LithoFilterParameters(
|
||||
lithoIdentifier, pathBuilder.toString(), buffer);
|
||||
Logger.printDebug(() -> "Searching " + parameter);
|
||||
|
||||
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {
|
||||
|
||||
@@ -60,6 +60,11 @@ public class CustomPlaybackSpeedPatch {
|
||||
*/
|
||||
public static final float PLAYBACK_SPEED_MAXIMUM = 8;
|
||||
|
||||
/**
|
||||
* How much +/- speed adjustment buttons change the current speed.
|
||||
*/
|
||||
private static final double SPEED_ADJUSTMENT_CHANGE = 0.05;
|
||||
|
||||
/**
|
||||
* Scale used to convert user speed to {@link android.widget.ProgressBar#setProgress(int)}.
|
||||
*/
|
||||
@@ -390,9 +395,9 @@ public class CustomPlaybackSpeedPatch {
|
||||
});
|
||||
|
||||
minusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||
VideoInformation.getPlaybackSpeed() - 0.05f));
|
||||
(float) (VideoInformation.getPlaybackSpeed() - SPEED_ADJUSTMENT_CHANGE)));
|
||||
plusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||
VideoInformation.getPlaybackSpeed() + 0.05f));
|
||||
(float) (VideoInformation.getPlaybackSpeed() + SPEED_ADJUSTMENT_CHANGE)));
|
||||
|
||||
// Create GridLayout for preset speed buttons.
|
||||
GridLayout gridLayout = new GridLayout(context);
|
||||
@@ -611,15 +616,21 @@ public class CustomPlaybackSpeedPatch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the given playback speed to the nearest 0.05 increment and ensures it is within valid bounds.
|
||||
* Rounds the given playback speed to the nearest 0.05 increment,
|
||||
* unless the speed exactly matches a preset custom speed.
|
||||
*
|
||||
* @param speed The playback speed to round.
|
||||
* @return The rounded speed, constrained to the specified bounds.
|
||||
*/
|
||||
private static float roundSpeedToNearestIncrement(float speed) {
|
||||
// Allow speed as-is if it exactly matches a speed preset such as 1.03x.
|
||||
if (arrayContains(customPlaybackSpeeds, speed)) {
|
||||
return speed;
|
||||
}
|
||||
|
||||
// Round to nearest 0.05 speed. Must use double precision otherwise rounding error can occur.
|
||||
final double roundedSpeed = Math.round(speed / 0.05) * 0.05;
|
||||
return Utils.clamp((float) roundedSpeed, 0.05f, PLAYBACK_SPEED_MAXIMUM);
|
||||
final double roundedSpeed = Math.round(speed / SPEED_ADJUSTMENT_CHANGE) * SPEED_ADJUSTMENT_CHANGE;
|
||||
return Utils.clamp((float) roundedSpeed, (float) SPEED_ADJUSTMENT_CHANGE, PLAYBACK_SPEED_MAXIMUM);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,9 @@ import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
@@ -24,12 +26,15 @@ import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFrag
|
||||
* This class is responsible for injecting our own fragment by replacing the LicenseActivity.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class LicenseActivityHook {
|
||||
public class LicenseActivityHook extends Activity {
|
||||
|
||||
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
|
||||
|
||||
private static ViewGroup.LayoutParams toolbarLayoutParams;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public static SearchViewController searchViewController;
|
||||
|
||||
public static void setToolbarLayoutParams(Toolbar toolbar) {
|
||||
if (toolbarLayoutParams != null) {
|
||||
toolbar.setLayoutParams(toolbarLayoutParams);
|
||||
@@ -126,12 +131,13 @@ public class LicenseActivityHook {
|
||||
view -> view instanceof TextView);
|
||||
if (toolbarTextView != null) {
|
||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||
toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||
}
|
||||
setToolbarLayoutParams(toolbar);
|
||||
|
||||
// Add Search Icon and EditText for ReVancedPreferenceFragment only.
|
||||
// Add Search bar only for ReVancedPreferenceFragment.
|
||||
if (fragment instanceof ReVancedPreferenceFragment) {
|
||||
SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
|
||||
searchViewController = SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
|
||||
}
|
||||
|
||||
toolBarParent.addView(toolbar, 0);
|
||||
@@ -166,4 +172,10 @@ public class LicenseActivityHook {
|
||||
Utils.setIsDarkModeEnabled(themeOrdinal == 1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleConfigurationChanged(Activity activity, Configuration newConfig) {
|
||||
if (searchViewController != null) {
|
||||
searchViewController.handleOrientationChange(newConfig.orientation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.graphics.drawable.GradientDrawable;
|
||||
import android.util.Pair;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
@@ -51,6 +52,7 @@ public class SearchViewController {
|
||||
private final Deque<String> searchHistory;
|
||||
private final AutoCompleteTextView autoCompleteTextView;
|
||||
private final boolean showSettingsSearchHistory;
|
||||
private int currentOrientation;
|
||||
|
||||
/**
|
||||
* Creates a background drawable for the SearchView with rounded corners.
|
||||
@@ -83,8 +85,8 @@ public class SearchViewController {
|
||||
/**
|
||||
* Adds search view components to the activity.
|
||||
*/
|
||||
public static void addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
||||
new SearchViewController(activity, toolbar, fragment);
|
||||
public static SearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
||||
return new SearchViewController(activity, toolbar, fragment);
|
||||
}
|
||||
|
||||
private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
||||
@@ -93,6 +95,7 @@ public class SearchViewController {
|
||||
this.originalTitle = toolbar.getTitle();
|
||||
this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get();
|
||||
this.searchHistory = new LinkedList<>();
|
||||
this.currentOrientation = activity.getResources().getConfiguration().orientation;
|
||||
StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES;
|
||||
if (showSettingsSearchHistory) {
|
||||
String entries = searchEntries.get();
|
||||
@@ -115,6 +118,9 @@ public class SearchViewController {
|
||||
searchView.getContext().getResources().getIdentifier(
|
||||
"android:id/search_src_text", null, null));
|
||||
|
||||
// Disable fullscreen keyboard mode.
|
||||
autoCompleteTextView.setImeOptions(autoCompleteTextView.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||
|
||||
// Set background and query hint.
|
||||
searchView.setBackground(createBackgroundDrawable(toolbar.getContext()));
|
||||
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
||||
@@ -197,7 +203,7 @@ public class SearchViewController {
|
||||
if (isSearchActive) {
|
||||
closeSearch();
|
||||
} else {
|
||||
activity.onBackPressed();
|
||||
activity.finish();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "navigation click failure", ex);
|
||||
@@ -285,6 +291,16 @@ public class SearchViewController {
|
||||
}
|
||||
}
|
||||
|
||||
public void handleOrientationChange(int newOrientation) {
|
||||
if (newOrientation != currentOrientation) {
|
||||
currentOrientation = newOrientation;
|
||||
if (autoCompleteTextView != null) {
|
||||
autoCompleteTextView.dismissDropDown();
|
||||
Logger.printDebug(() -> "Orientation changed, search history dismissed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the search view and shows the keyboard.
|
||||
*/
|
||||
@@ -313,7 +329,7 @@ public class SearchViewController {
|
||||
/**
|
||||
* Closes the search view and hides the keyboard.
|
||||
*/
|
||||
private void closeSearch() {
|
||||
public void closeSearch() {
|
||||
isSearchActive = false;
|
||||
toolbar.getMenu().findItem(getResourceIdentifier(
|
||||
"action_search", "id")).setVisible(true);
|
||||
@@ -326,6 +342,19 @@ public class SearchViewController {
|
||||
imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
public static boolean handleBackPress() {
|
||||
if (LicenseActivityHook.searchViewController != null
|
||||
&& LicenseActivityHook.searchViewController.isSearchActive()) {
|
||||
LicenseActivityHook.searchViewController.closeSearch();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isSearchActive() {
|
||||
return isSearchActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom ArrayAdapter for search history.
|
||||
*/
|
||||
|
||||
@@ -5,8 +5,10 @@ import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.extension.shared.settings.Setting.Availability;
|
||||
import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
|
||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||
import static app.revanced.extension.shared.settings.Setting.parentsAll;
|
||||
import static app.revanced.extension.shared.settings.Setting.parentsAny;
|
||||
import static app.revanced.extension.youtube.patches.ChangeFormFactorPatch.FormFactor;
|
||||
import static app.revanced.extension.youtube.patches.ChangeHeaderPatch.HeaderLogo;
|
||||
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.ChangeStartPageTypeAvailability;
|
||||
import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage;
|
||||
import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.FullscreenMode;
|
||||
@@ -22,6 +24,7 @@ import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPa
|
||||
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
|
||||
import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability;
|
||||
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle;
|
||||
import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.SponsorBlockDuration;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.IGNORE;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||
@@ -52,13 +55,14 @@ public class Settings extends BaseSettings {
|
||||
public static final IntegerSetting VIDEO_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_video_quality_default_wifi", -2);
|
||||
public static final IntegerSetting VIDEO_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_video_quality_default_mobile", -2);
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", FALSE);
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, false,
|
||||
parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
|
||||
public static final IntegerSetting SHORTS_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_shorts_quality_default_wifi", -2, true);
|
||||
public static final IntegerSetting SHORTS_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_shorts_quality_default_mobile", -2, true);
|
||||
public static final BooleanSetting REMEMBER_SHORTS_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_shorts_quality_last_selected", FALSE);
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, false,
|
||||
parentsAny(REMEMBER_VIDEO_QUALITY_LAST_SELECTED, REMEMBER_SHORTS_QUALITY_LAST_SELECTED));
|
||||
public static final BooleanSetting ADVANCED_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_advanced_video_quality_menu", TRUE);
|
||||
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE);
|
||||
|
||||
// Speed
|
||||
public static final FloatSetting SPEED_TAP_AND_HOLD = new FloatSetting("revanced_speed_tap_and_hold", 2.0f, true);
|
||||
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", FALSE);
|
||||
@@ -68,23 +72,23 @@ public class Settings extends BaseSettings {
|
||||
public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", -2.0f);
|
||||
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.5\n3.0\n4.0\n5.0\n6.0\n7.0\n8.0", true);
|
||||
|
||||
// Audio
|
||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, new ForceOriginalAudioAvailability());
|
||||
|
||||
// Ads
|
||||
public static final BooleanSetting HIDE_CREATOR_STORE_SHELF = new BooleanSetting("revanced_hide_creator_store_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_END_SCREEN_STORE_BANNER = new BooleanSetting("revanced_hide_end_screen_store_banner", TRUE, true);
|
||||
public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE);
|
||||
public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE);
|
||||
public static final BooleanSetting HIDE_GET_PREMIUM = new BooleanSetting("revanced_hide_get_premium", TRUE);
|
||||
public static final BooleanSetting HIDE_HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts_ads", TRUE);
|
||||
public static final BooleanSetting HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts", TRUE);
|
||||
public static final BooleanSetting HIDE_MERCHANDISE_BANNERS = new BooleanSetting("revanced_hide_merchandise_banners", TRUE);
|
||||
public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYER_STORE_SHELF = new BooleanSetting("revanced_hide_player_store_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_PRODUCTS_BANNER = new BooleanSetting("revanced_hide_products_banner", TRUE);
|
||||
public static final BooleanSetting HIDE_SELF_SPONSOR = new BooleanSetting("revanced_hide_self_sponsor_ads", TRUE);
|
||||
public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE);
|
||||
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true);
|
||||
public static final BooleanSetting HIDE_VISIT_STORE_BUTTON = new BooleanSetting("revanced_hide_visit_store_button", TRUE);
|
||||
public static final BooleanSetting HIDE_VIEW_PRODUCTS_BANNER = new BooleanSetting("revanced_hide_view_products_banner", TRUE);
|
||||
public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE);
|
||||
|
||||
// Feed
|
||||
@@ -95,23 +99,23 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_COMPACT_BANNER = new BooleanSetting("revanced_hide_compact_banner", TRUE);
|
||||
public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", FALSE, true);
|
||||
public static final BooleanSetting HIDE_DOODLES = new BooleanSetting("revanced_hide_doodles", FALSE, true, "revanced_hide_doodles_user_dialog_message");
|
||||
public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE);
|
||||
public static final BooleanSetting HIDE_FEED_SURVEY = new BooleanSetting("revanced_hide_feed_survey", TRUE);
|
||||
public static final BooleanSetting HIDE_EXPANDABLE_CARD = new BooleanSetting("revanced_hide_expandable_card", TRUE);
|
||||
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_FEED = new BooleanSetting("revanced_hide_filter_bar_feed_in_feed", FALSE, true);
|
||||
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_HISTORY = new BooleanSetting("revanced_hide_filter_bar_feed_in_history", FALSE);
|
||||
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_filter_bar_feed_in_related_videos", FALSE, true);
|
||||
public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_SEARCH = new BooleanSetting("revanced_hide_filter_bar_feed_in_search", FALSE, true);
|
||||
public static final BooleanSetting HIDE_FLOATING_MICROPHONE_BUTTON = new BooleanSetting("revanced_hide_floating_microphone_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_HORIZONTAL_SHELVES = new BooleanSetting("revanced_hide_horizontal_shelves", TRUE);
|
||||
public static final BooleanSetting HIDE_IMAGE_SHELF = new BooleanSetting("revanced_hide_image_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_MIX_PLAYLISTS = new BooleanSetting("revanced_hide_mix_playlists", TRUE);
|
||||
public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE);
|
||||
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
|
||||
public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS = new BooleanSetting("revanced_hide_search_result_recommendation_labels", TRUE);
|
||||
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_SURVEYS = new BooleanSetting("revanced_hide_surveys", TRUE);
|
||||
public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", FALSE);
|
||||
public static final BooleanSetting HIDE_VIDEO_RECOMMENDATION_LABELS = new BooleanSetting("revanced_hide_video_recommendation_labels", TRUE);
|
||||
|
||||
// Alternative thumbnails
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
|
||||
@@ -123,6 +127,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", TRUE, new DeArrowAvailability());
|
||||
public static final EnumSetting<ThumbnailStillTime> ALT_THUMBNAIL_STILLS_TIME = new EnumSetting<>("revanced_alt_thumbnail_stills_time", ThumbnailStillTime.MIDDLE, new StillImagesAvailability());
|
||||
public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, new StillImagesAvailability());
|
||||
|
||||
// Hide keyword content
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_HOME = new BooleanSetting("revanced_hide_keyword_content_home", FALSE);
|
||||
public static final BooleanSetting HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_keyword_content_subscriptions", FALSE);
|
||||
@@ -130,31 +135,36 @@ public class Settings extends BaseSettings {
|
||||
public static final StringSetting HIDE_KEYWORD_CONTENT_PHRASES = new StringSetting("revanced_hide_keyword_content_phrases", "",
|
||||
parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_SEARCH));
|
||||
|
||||
// Channel page
|
||||
public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", FALSE);
|
||||
public static final BooleanSetting HIDE_LINKS_PREVIEW = new BooleanSetting("revanced_hide_links_preview", TRUE);
|
||||
public static final BooleanSetting HIDE_MEMBERS_SHELF = new BooleanSetting("revanced_hide_members_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_VISIT_COMMUNITY_BUTTON = new BooleanSetting("revanced_hide_visit_community_button", TRUE);
|
||||
public static final BooleanSetting HIDE_VISIT_STORE_BUTTON = new BooleanSetting("revanced_hide_visit_store_button", TRUE);
|
||||
|
||||
// Player
|
||||
public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE);
|
||||
public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE);
|
||||
public static final BooleanSetting DISABLE_AUTO_CAPTIONS = new BooleanSetting("revanced_disable_auto_captions", FALSE, true);
|
||||
public static final BooleanSetting DISABLE_CHAPTER_SKIP_DOUBLE_TAP = new BooleanSetting("revanced_disable_chapter_skip_double_tap", FALSE);
|
||||
public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true);
|
||||
public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE);
|
||||
public static final EnumSetting<FullscreenMode> EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED);
|
||||
public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_captions_button", FALSE);
|
||||
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND = new BooleanSetting("revanced_hide_player_control_buttons_background", FALSE, true);
|
||||
public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE);
|
||||
public static final BooleanSetting HIDE_CHANNEL_MEMBER_SHELF = new BooleanSetting("revanced_hide_channel_member_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_community_guidelines", TRUE);
|
||||
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
|
||||
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
|
||||
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
|
||||
public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true);
|
||||
public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
|
||||
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
|
||||
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE);
|
||||
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
|
||||
public static final BooleanSetting HIDE_JOIN_MEMBERSHIP_BUTTON = new BooleanSetting("revanced_hide_join_membership_button", TRUE);
|
||||
public static final BooleanSetting HIDE_MEDICAL_PANELS = new BooleanSetting("revanced_hide_medical_panels", TRUE);
|
||||
public static final BooleanSetting HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND = new BooleanSetting("revanced_hide_player_control_buttons_background", FALSE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS = new BooleanSetting("revanced_hide_player_previous_next_buttons", FALSE, true);
|
||||
public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE);
|
||||
public static final BooleanSetting HIDE_RELATED_VIDEOS_OVERLAY = new BooleanSetting("revanced_hide_related_videos_overlay", FALSE, true);
|
||||
public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE);
|
||||
public static final BooleanSetting HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_subscribers_community_guidelines", TRUE);
|
||||
public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE);
|
||||
@@ -163,6 +173,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE);
|
||||
public static final IntegerSetting PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_player_overlay_opacity", 100, true);
|
||||
public static final BooleanSetting PLAYER_POPUP_PANELS = new BooleanSetting("revanced_hide_player_popup_panels", FALSE);
|
||||
|
||||
// Miniplayer
|
||||
public static final EnumSetting<MiniplayerType> MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.DEFAULT, true);
|
||||
private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4);
|
||||
@@ -175,20 +186,25 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN);
|
||||
public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN);
|
||||
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
||||
|
||||
// External downloader
|
||||
public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE);
|
||||
public static final BooleanSetting EXTERNAL_DOWNLOADER_ACTION_BUTTON = new BooleanSetting("revanced_external_downloader_action_button", FALSE);
|
||||
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME = new StringSetting("revanced_external_downloader_name",
|
||||
"org.schabi.newpipe" /* NewPipe */, parentsAny(EXTERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER_ACTION_BUTTON));
|
||||
"com.deniscerri.ytdl" /* YTDLnis */, parentsAny(EXTERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER_ACTION_BUTTON));
|
||||
|
||||
// Comments
|
||||
public static final BooleanSetting HIDE_COMMENTS_AI_CHAT_SUMMARY = new BooleanSetting("revanced_hide_comments_ai_chat_summary", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_AI_SUMMARY = new BooleanSetting("revanced_hide_comments_ai_summary", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_BY_MEMBERS_HEADER = new BooleanSetting("revanced_hide_comments_by_members_header", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_comments_channel_guidelines", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_comments_community_guidelines", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_CREATE_A_SHORT_BUTTON = new BooleanSetting("revanced_hide_comments_create_a_short_button", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_TIMESTAMP_BUTTON = new BooleanSetting("revanced_hide_comments_timestamp_button", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_comments_preview_comment", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_TIMESTAMP_BUTTON = new BooleanSetting("revanced_hide_comments_timestamp_button", FALSE);
|
||||
|
||||
// Description
|
||||
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
|
||||
public static final BooleanSetting HIDE_ASK_SECTION = new BooleanSetting("revanced_hide_ask_section", FALSE);
|
||||
@@ -199,6 +215,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_KEY_CONCEPTS_SECTION = new BooleanSetting("revanced_hide_key_concepts_section", FALSE);
|
||||
public static final BooleanSetting HIDE_PODCAST_SECTION = new BooleanSetting("revanced_hide_podcast_section", TRUE);
|
||||
public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", TRUE);
|
||||
|
||||
// Action buttons
|
||||
public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE);
|
||||
public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE);
|
||||
@@ -211,6 +228,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE);
|
||||
public static final BooleanSetting HIDE_STOP_ADS_BUTTON = new BooleanSetting("revanced_hide_stop_ads_button", TRUE);
|
||||
public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", TRUE);
|
||||
|
||||
// Player flyout menu items
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_additional_settings", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_AMBIENT_MODE = new BooleanSetting("revanced_hide_player_flyout_ambient_mode", FALSE);
|
||||
@@ -233,7 +251,8 @@ public class Settings extends BaseSettings {
|
||||
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
||||
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
||||
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
||||
public static final EnumSetting<SplashScreenAnimationStyle> SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true);
|
||||
public static final EnumSetting<SplashScreenAnimationStyle> SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("revanced_splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true);
|
||||
public static final EnumSetting<HeaderLogo> HEADER_LOGO = new EnumSetting<>("revanced_header_logo", HeaderLogo.DEFAULT, true);
|
||||
|
||||
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
|
||||
"revanced_remove_viewer_discretion_dialog_user_dialog_message");
|
||||
@@ -243,9 +262,11 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting CHANGE_START_PAGE_ALWAYS = new BooleanSetting("revanced_change_start_page_always", FALSE, true,
|
||||
new ChangeStartPageTypeAvailability());
|
||||
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "19.01.34", true, parent(SPOOF_APP_VERSION));
|
||||
|
||||
// Custom filter
|
||||
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
|
||||
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
|
||||
|
||||
// Navigation buttons
|
||||
public static final BooleanSetting HIDE_HOME_BUTTON = new BooleanSetting("revanced_hide_home_button", FALSE, true);
|
||||
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", TRUE, true);
|
||||
@@ -281,12 +302,12 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_SHORTS_LOCATION_LABEL = new BooleanSetting("revanced_hide_shorts_location_label", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true);
|
||||
public static final BooleanSetting HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_shorts_preview_comment", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_REMIX_BUTTON = new BooleanSetting("revanced_hide_shorts_remix_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SAVE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_save_sound_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SEARCH = new BooleanSetting("revanced_hide_shorts_search", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHARE_BUTTON = new BooleanSetting("revanced_hide_shorts_share_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_shorts_preview_comment", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_sound_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", FALSE);
|
||||
@@ -316,7 +337,7 @@ public class Settings extends BaseSettings {
|
||||
public static final StringSetting SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_primary", "#FF0033", true, parent(SEEKBAR_CUSTOM_COLOR));
|
||||
public static final StringSetting SEEKBAR_CUSTOM_COLOR_ACCENT = new StringSetting("revanced_seekbar_custom_color_accent", "#FF2791", true, parent(SEEKBAR_CUSTOM_COLOR));
|
||||
|
||||
// Misc
|
||||
// Miscellaneous
|
||||
public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE);
|
||||
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
|
||||
public static final BooleanSetting AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE);
|
||||
@@ -381,7 +402,11 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting SB_SQUARE_LAYOUT = new BooleanSetting("sb_square_layout", FALSE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_COMPACT_SKIP_BUTTON = new BooleanSetting("sb_compact_skip_button", FALSE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_AUTO_HIDE_SKIP_BUTTON = new BooleanSetting("sb_auto_hide_skip_button", TRUE, parent(SB_ENABLED));
|
||||
public static final EnumSetting<SponsorBlockDuration> SB_AUTO_HIDE_SKIP_BUTTON_DURATION = new EnumSetting<>("sb_auto_hide_skip_button_duration",
|
||||
SponsorBlockDuration.FOUR_SECONDS, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE, parent(SB_ENABLED));
|
||||
public static final EnumSetting<SponsorBlockDuration> SB_TOAST_ON_SKIP_DURATION = new EnumSetting<>("sb_toast_on_skip_duration",
|
||||
SponsorBlockDuration.FOUR_SECONDS, parentsAll(SB_ENABLED, SB_TOAST_ON_SKIP));
|
||||
public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", TRUE, parent(SB_ENABLED));
|
||||
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
|
||||
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
|
||||
|
||||
@@ -16,10 +16,8 @@ import app.revanced.extension.youtube.settings.Settings;
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public final class CustomVideoSpeedListPreference extends CustomDialogListPreference {
|
||||
|
||||
/**
|
||||
* Initialize a settings preference list with the available playback speeds.
|
||||
*/
|
||||
private void initializeEntryValues() {
|
||||
{
|
||||
// Initialize a settings preference list with the available playback speeds.
|
||||
float[] customPlaybackSpeeds = CustomPlaybackSpeedPatch.customPlaybackSpeeds;
|
||||
final int numberOfEntries = customPlaybackSpeeds.length + 1;
|
||||
String[] preferenceListEntries = new String[numberOfEntries];
|
||||
@@ -41,10 +39,6 @@ public final class CustomVideoSpeedListPreference extends CustomDialogListPrefer
|
||||
setEntryValues(preferenceListEntryValues);
|
||||
}
|
||||
|
||||
{
|
||||
initializeEntryValues();
|
||||
}
|
||||
|
||||
public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.sf;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Pair;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* A custom ListPreference for selecting an external downloader package with checkmarks and EditText for custom package names.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ExternalDownloaderPreference extends CustomDialogListPreference {
|
||||
|
||||
/**
|
||||
* Enum representing supported external downloaders with their display names, package names, and download URLs.
|
||||
*/
|
||||
private enum Downloader {
|
||||
YTDLNIS("YTDLnis",
|
||||
"com.deniscerri.ytdl",
|
||||
"https://ytdlnis.org",
|
||||
true),
|
||||
SEAL("Seal",
|
||||
"com.junkfood.seal",
|
||||
"https://github.com/JunkFood02/Seal/releases/latest",
|
||||
true),
|
||||
GRAYJAY("Grayjay",
|
||||
"com.futo.platformplayer",
|
||||
"https://grayjay.app"),
|
||||
LIBRETUBE("LibreTube",
|
||||
"com.github.libretube",
|
||||
"https://libretube.dev"),
|
||||
NEWPIPE("NewPipe",
|
||||
"org.schabi.newpipe",
|
||||
"https://newpipe.net"),
|
||||
PIPEPIPE("PipePipe",
|
||||
"InfinityLoop1309.NewPipeEnhanced",
|
||||
"https://pipepipe.dev"),
|
||||
TUBULAR("Tubular",
|
||||
"org.polymorphicshade.tubular",
|
||||
"https://github.com/polymorphicshade/Tubular/releases/latest"),
|
||||
OTHER(sf("revanced_external_downloader_other_item").toString(),
|
||||
null,
|
||||
null,
|
||||
true);
|
||||
|
||||
private static final Map<String, Downloader> PACKAGE_TO_ENUM = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (Downloader downloader : values()) {
|
||||
String packageName = downloader.packageName;
|
||||
if (packageName != null) {
|
||||
PACKAGE_TO_ENUM.put(packageName, downloader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a Downloader by its package name. This method can never return {@link #OTHER}.
|
||||
* @return The Downloader enum or null if not found.
|
||||
*/
|
||||
@Nullable
|
||||
public static Downloader findByPackageName(String packageName) {
|
||||
return PACKAGE_TO_ENUM.get(Objects.requireNonNull(packageName));
|
||||
}
|
||||
|
||||
public final String name;
|
||||
@Nullable
|
||||
public final String packageName;
|
||||
@Nullable
|
||||
public final String downloadUrl;
|
||||
/**
|
||||
* If a downloader app should be shown in the preference settings
|
||||
* if the app is not currently installed.
|
||||
*/
|
||||
public final boolean isPreferred;
|
||||
|
||||
Downloader(String name, String packageName, String downloadUrl) {
|
||||
this(name, packageName, downloadUrl, false);
|
||||
}
|
||||
|
||||
Downloader(String name, @Nullable String packageName, @Nullable String downloadUrl, boolean isPreferred) {
|
||||
this.name = name;
|
||||
this.packageName = packageName;
|
||||
this.downloadUrl = downloadUrl;
|
||||
this.isPreferred = isPreferred;
|
||||
}
|
||||
|
||||
public boolean isInstalled() {
|
||||
return packageName != null && isAppInstalledAndEnabled(packageName);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAppInstalledAndEnabled(String packageName) {
|
||||
try {
|
||||
if (Utils.getContext().getPackageManager().getApplicationInfo(packageName, 0).enabled) {
|
||||
Logger.printDebug(() -> "App installed: " + packageName);
|
||||
return true;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException error) {
|
||||
Logger.printDebug(() -> "App not installed: " + packageName);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private EditText editText;
|
||||
private CustomDialogListPreference.ListPreferenceArrayAdapter adapter;
|
||||
|
||||
public ExternalDownloaderPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public ExternalDownloaderPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public ExternalDownloaderPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ExternalDownloaderPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
private void updateEntries() {
|
||||
List<CharSequence> entries = new ArrayList<>();
|
||||
List<CharSequence> entryValues = new ArrayList<>();
|
||||
|
||||
for (Downloader downloader : Downloader.values()) {
|
||||
if (downloader.isPreferred || downloader.isInstalled()) {
|
||||
String packageName = downloader.packageName;
|
||||
|
||||
entries.add(downloader.name);
|
||||
entryValues.add(packageName != null
|
||||
? packageName
|
||||
: Downloader.OTHER.name);
|
||||
}
|
||||
}
|
||||
|
||||
setEntries(entries.toArray(new CharSequence[0]));
|
||||
setEntryValues(entryValues.toArray(new CharSequence[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the summary for this ListPreference.
|
||||
*/
|
||||
@Override
|
||||
public void setSummary(CharSequence summary) {
|
||||
// Ignore calls to set the summary.
|
||||
// Summary is always the description of the category.
|
||||
//
|
||||
// This is required otherwise the ReVanced preference fragment
|
||||
// sets all ListPreference summaries to show the current selection.
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a custom dialog with a ListView for predefined downloader packages and EditText for custom package input.
|
||||
*/
|
||||
@Override
|
||||
protected void showDialog(@Nullable Bundle state) {
|
||||
// Must set entries before showing the dialog, to handle if
|
||||
// an app is installed while the settings are open in the background.
|
||||
updateEntries();
|
||||
|
||||
Context context = getContext();
|
||||
String packageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get();
|
||||
|
||||
// Create the main layout for the dialog content.
|
||||
LinearLayout contentLayout = new LinearLayout(context);
|
||||
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
// Create ListView for predefined downloader apps.
|
||||
ListView listView = new ListView(context);
|
||||
listView.setId(android.R.id.list);
|
||||
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
|
||||
|
||||
// Create custom adapter for the ListView.
|
||||
final boolean usingCustomDownloader = Downloader.findByPackageName(packageName) == null;
|
||||
adapter = new CustomDialogListPreference.ListPreferenceArrayAdapter(
|
||||
context,
|
||||
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
|
||||
getEntries(),
|
||||
getEntryValues(),
|
||||
usingCustomDownloader
|
||||
? Downloader.OTHER.name
|
||||
: packageName
|
||||
);
|
||||
listView.setAdapter(adapter);
|
||||
|
||||
Function<String, Void> updateListViewSelection = (updatedPackageName) -> {
|
||||
String entryValueName = Downloader.findByPackageName(updatedPackageName) == null
|
||||
? Downloader.OTHER.name
|
||||
: updatedPackageName;
|
||||
CharSequence[] entryValues = getEntryValues();
|
||||
|
||||
for (int i = 0, length = entryValues.length; i < length; i++) {
|
||||
String entryString = entryValues[i].toString();
|
||||
if (entryString.equals(entryValueName)) {
|
||||
listView.setItemChecked(i, true);
|
||||
listView.setSelection(i);
|
||||
adapter.setSelectedValue(entryString);
|
||||
adapter.notifyDataSetChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
updateListViewSelection.apply(packageName);
|
||||
|
||||
// Handle item click to select value.
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
String selectedValue = getEntryValues()[position].toString();
|
||||
Downloader selectedApp = Downloader.findByPackageName(selectedValue);
|
||||
|
||||
if (selectedApp != null) {
|
||||
editText.setText(selectedApp.packageName);
|
||||
editText.setEnabled(false); // Disable editing for predefined options.
|
||||
} else {
|
||||
String savedPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get();
|
||||
editText.setText(Downloader.findByPackageName(savedPackageName) == null
|
||||
? savedPackageName // If the user is clicking thru options then retain existing other app.
|
||||
: ""
|
||||
);
|
||||
editText.setEnabled(true); // Enable editing for Custom.
|
||||
editText.requestFocus();
|
||||
}
|
||||
editText.setSelection(editText.getText().length());
|
||||
adapter.setSelectedValue(selectedValue);
|
||||
adapter.notifyDataSetChanged();
|
||||
});
|
||||
|
||||
// Add ListView to content layout with initial height.
|
||||
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0 // Initial height, will be updated.
|
||||
);
|
||||
listViewParams.bottomMargin = dipToPixels(16);
|
||||
contentLayout.addView(listView, listViewParams);
|
||||
|
||||
// Add EditText for custom package name.
|
||||
editText = new EditText(context);
|
||||
editText.setText(packageName);
|
||||
editText.setSelection(packageName.length());
|
||||
editText.setHint(str("revanced_external_downloader_other_item_hint"));
|
||||
editText.setSingleLine(true); // Restrict EditText to a single line.
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
|
||||
// Set initial EditText state based on selected downloader.
|
||||
editText.setEnabled(usingCustomDownloader);
|
||||
editText.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) {
|
||||
String updatedPackageName = edit.toString().trim();
|
||||
updateListViewSelection.apply(updatedPackageName);
|
||||
}
|
||||
});
|
||||
|
||||
ShapeDrawable editTextBackground = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.createCornerRadii(10), null, null));
|
||||
editTextBackground.getPaint().setColor(Utils.getEditTextBackground());
|
||||
final int dip8 = dipToPixels(8);
|
||||
editText.setPadding(dip8, dip8, dip8, dip8);
|
||||
editText.setBackground(editTextBackground);
|
||||
editText.setClipToOutline(true);
|
||||
contentLayout.addView(editText);
|
||||
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : "",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
() -> {
|
||||
String newValue = editText.getText().toString().trim();
|
||||
if (newValue.isEmpty()) {
|
||||
// Show dialog if EditText is empty.
|
||||
Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_external_downloader_name_title"),
|
||||
str("revanced_external_downloader_empty_warning"),
|
||||
null,
|
||||
null,
|
||||
() -> {}, // OK button does nothing (dismiss only).
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false
|
||||
).first.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (showDialogIfAppIsNotInstalled(getContext(), newValue)) {
|
||||
return; // Invalid package. Do not save.
|
||||
}
|
||||
|
||||
// Save custom package name.
|
||||
if (callChangeListener(newValue)) {
|
||||
setValue(newValue);
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_settings_reset"),
|
||||
() -> { // Reset action.
|
||||
String defaultValue = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.defaultValue;
|
||||
editText.setText(defaultValue);
|
||||
editText.setSelection(defaultValue.length());
|
||||
editText.setEnabled(false); // Disable editing on reset.
|
||||
updateListViewSelection.apply(defaultValue);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Add the content layout directly to the dialog's main layout.
|
||||
LinearLayout dialogMainLayout = dialogPair.second;
|
||||
dialogMainLayout.addView(contentLayout, dialogMainLayout.getChildCount() - 1);
|
||||
|
||||
// Update ListView height dynamically based on orientation.
|
||||
//noinspection ExtractMethodRecommender
|
||||
Runnable updateListViewHeight = () -> {
|
||||
int totalHeight = 0;
|
||||
ListAdapter listAdapter = listView.getAdapter();
|
||||
if (listAdapter != null) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
final int listAdapterCount = listAdapter.getCount();
|
||||
for (int i = 0; i < listAdapterCount; i++) {
|
||||
View item = listAdapter.getView(i, null, listView);
|
||||
item.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(metrics.widthPixels, View.MeasureSpec.AT_MOST),
|
||||
View.MeasureSpec.UNSPECIFIED
|
||||
);
|
||||
totalHeight += item.getMeasuredHeight();
|
||||
}
|
||||
totalHeight += listView.getDividerHeight() * (listAdapterCount - 1);
|
||||
}
|
||||
|
||||
final int orientation = context.getResources().getConfiguration().orientation;
|
||||
if (orientation == android.content.res.Configuration.ORIENTATION_PORTRAIT) {
|
||||
// In portrait orientation, use WRAP_CONTENT for ListView height.
|
||||
listViewParams.height = LinearLayout.LayoutParams.WRAP_CONTENT;
|
||||
} else {
|
||||
// In landscape orientation, limit ListView height to 30% of screen height.
|
||||
final int maxHeight = Utils.percentageHeightToPixels(30);
|
||||
listViewParams.height = Math.min(totalHeight, maxHeight);
|
||||
}
|
||||
listView.setLayoutParams(listViewParams);
|
||||
};
|
||||
|
||||
// Initial height calculation.
|
||||
updateListViewHeight.run();
|
||||
|
||||
// Listen for configuration changes (e.g., orientation).
|
||||
View dialogView = dialogPair.second;
|
||||
// Recalculate height when layout changes (e.g., orientation change).
|
||||
dialogView.getViewTreeObserver().addOnGlobalLayoutListener(updateListViewHeight::run);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the app is not installed and a dialog was shown.
|
||||
*/
|
||||
public static boolean showDialogIfAppIsNotInstalled(Context context, String packageName) {
|
||||
if (isAppInstalledAndEnabled(packageName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Downloader downloader = Downloader.findByPackageName(packageName);
|
||||
String downloadUrl = downloader != null
|
||||
? downloader.downloadUrl
|
||||
: null;
|
||||
String okButtonText = downloadUrl != null
|
||||
? str("gms_core_dialog_open_website_text") // Open website.
|
||||
: null; // Ok.
|
||||
// Show a dialog if the recommended app is not installed or if the custom package cannot be found.
|
||||
String message = downloader != null
|
||||
? str("revanced_external_downloader_not_installed_warning", downloader.name)
|
||||
: str("revanced_external_downloader_package_not_found_warning", packageName);
|
||||
|
||||
Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_external_downloader_not_found_title"),
|
||||
message,
|
||||
null,
|
||||
okButtonText,
|
||||
() -> {
|
||||
try {
|
||||
// OK button action: open the downloader's URL if available.
|
||||
if (downloadUrl != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(downloadUrl));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to open downloader URL: " + downloader, ex);
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
null,
|
||||
null,
|
||||
false
|
||||
).first.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import android.preference.SwitchPreference;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
@@ -248,7 +249,15 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
rootView.setOnApplyWindowInsetsListener((v, insets) -> {
|
||||
Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
|
||||
Insets navInsets = insets.getInsets(WindowInsets.Type.navigationBars());
|
||||
v.setPadding(0, statusInsets.top, 0, navInsets.bottom);
|
||||
Insets cutoutInsets = insets.getInsets(WindowInsets.Type.displayCutout());
|
||||
|
||||
// Apply padding for display cutout in landscape.
|
||||
int leftPadding = cutoutInsets.left;
|
||||
int rightPadding = cutoutInsets.right;
|
||||
int topPadding = statusInsets.top;
|
||||
int bottomPadding = navInsets.bottom;
|
||||
|
||||
v.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
|
||||
return insets;
|
||||
});
|
||||
}
|
||||
@@ -265,10 +274,16 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
true, TextView.class::isInstance);
|
||||
if (toolbarTextView != null) {
|
||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||
toolbarTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||
}
|
||||
|
||||
LicenseActivityHook.setToolbarLayoutParams(toolbar);
|
||||
|
||||
if (LicenseActivityHook.searchViewController != null
|
||||
&& LicenseActivityHook.searchViewController.isSearchActive()) {
|
||||
toolbar.post(() -> LicenseActivityHook.searchViewController.closeSearch());
|
||||
}
|
||||
|
||||
rootView.addView(toolbar, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
package app.revanced.extension.youtube.sponsorblock;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Range;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
@@ -23,6 +44,7 @@ import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||
import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment;
|
||||
import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
|
||||
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
|
||||
@@ -30,20 +52,37 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController
|
||||
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
|
||||
*/
|
||||
public class SegmentPlaybackController {
|
||||
|
||||
/**
|
||||
* Length of time to show a skip button for a highlight segment,
|
||||
* or a regular segment if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled.
|
||||
*
|
||||
* Effectively this value is rounded up to the next second.
|
||||
* Enum for configurable durations (1 to 10 seconds) for skip button and toast display.
|
||||
*/
|
||||
private static final long DURATION_TO_SHOW_SKIP_BUTTON = 3800;
|
||||
public enum SponsorBlockDuration {
|
||||
ONE_SECOND(1),
|
||||
TWO_SECONDS(2),
|
||||
THREE_SECONDS(3),
|
||||
FOUR_SECONDS(4),
|
||||
FIVE_SECONDS(5),
|
||||
SIX_SECONDS(6),
|
||||
SEVEN_SECONDS(7),
|
||||
EIGHT_SECONDS(8),
|
||||
NINE_SECONDS(9),
|
||||
TEN_SECONDS(10);
|
||||
|
||||
/**
|
||||
* Duration, minus 200ms to adjust for exclusive end time checking in scheduled show/hides.
|
||||
*/
|
||||
private final long adjustedDuration;
|
||||
|
||||
SponsorBlockDuration(int seconds) {
|
||||
adjustedDuration = seconds * 1000L - 200;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Highlight segments have zero length as they are a point in time.
|
||||
* Draw them on screen using a fixed width bar.
|
||||
* Value is independent of device dpi.
|
||||
*/
|
||||
private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = 7;
|
||||
private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = dipToPixels(7);
|
||||
|
||||
@Nullable
|
||||
private static String currentVideoId;
|
||||
@@ -59,7 +98,7 @@ public class SegmentPlaybackController {
|
||||
/**
|
||||
* Because loading can take time, show the skip to highlight for a few seconds after the segments load.
|
||||
* This is the system time (in milliseconds) to no longer show the initial display skip to highlight.
|
||||
* Value will be zero if no highlight segment exists, or if the system time to show the highlight has passed.
|
||||
* Value is zero if no highlight segment exists, or if the system time to show the highlight has passed.
|
||||
*/
|
||||
private static long highlightSegmentInitialShowEndTime;
|
||||
|
||||
@@ -70,7 +109,7 @@ public class SegmentPlaybackController {
|
||||
private static SponsorSegment segmentCurrentlyPlaying;
|
||||
/**
|
||||
* Currently playing manual skip segment that is scheduled to hide.
|
||||
* This will always be NULL or equal to {@link #segmentCurrentlyPlaying}.
|
||||
* This is always NULL or equal to {@link #segmentCurrentlyPlaying}.
|
||||
*/
|
||||
@Nullable
|
||||
private static SponsorSegment scheduledHideSegment;
|
||||
@@ -89,31 +128,95 @@ public class SegmentPlaybackController {
|
||||
*/
|
||||
private static final List<SponsorSegment> hiddenSkipSegmentsForCurrentVideoTime = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Current segments that have been auto skipped.
|
||||
* If field is non null then the range will always contain the current video time.
|
||||
* Range is used to prevent auto-skipping after undo.
|
||||
* Android Range object has inclusive end time, unlike {@link SponsorSegment}.
|
||||
*/
|
||||
@Nullable
|
||||
private static Range<Long> undoAutoSkipRange;
|
||||
/**
|
||||
* Range to undo if the toast is tapped.
|
||||
* Is always null or identical to the last non null value of {@link #undoAutoSkipRange}.
|
||||
*/
|
||||
@Nullable
|
||||
private static Range<Long> undoAutoSkipRangeToast;
|
||||
|
||||
/**
|
||||
* System time (in milliseconds) of when to hide the skip button of {@link #segmentCurrentlyPlaying}.
|
||||
* Value is zero if playback is not inside a segment ({@link #segmentCurrentlyPlaying} is null),
|
||||
* or if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is not enabled.
|
||||
*/
|
||||
private static long skipSegmentButtonEndTime;
|
||||
|
||||
@Nullable
|
||||
private static String timeWithoutSegments;
|
||||
|
||||
private static int sponsorBarAbsoluteLeft;
|
||||
private static int sponsorAbsoluteBarRight;
|
||||
private static int sponsorBarThickness;
|
||||
|
||||
@Nullable
|
||||
private static SponsorSegment lastSegmentSkipped;
|
||||
private static long lastSegmentSkippedTime;
|
||||
|
||||
@Nullable
|
||||
private static SponsorSegment toastSegmentSkipped;
|
||||
private static int toastNumberOfSegmentsSkipped;
|
||||
|
||||
/**
|
||||
* The last toast dialog showing on screen.
|
||||
*/
|
||||
private static WeakReference<Dialog> toastDialogRef = new WeakReference<>(null);
|
||||
|
||||
static {
|
||||
// Dismiss toast if app changes to PiP while undo skip is shown.
|
||||
PlayerType.getOnChange().addObserver((PlayerType type) -> {
|
||||
if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE && dismissUndoToast()) {
|
||||
Logger.printDebug(() -> "Dismissed undo toast as playback is PiP");
|
||||
}
|
||||
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the toast was on screen and is now dismissed.
|
||||
*/
|
||||
private static boolean dismissUndoToast() {
|
||||
Dialog toastDialog = toastDialogRef.get();
|
||||
if (toastDialog != null && toastDialog.isShowing()) {
|
||||
toastDialog.dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The adjusted duration to show the skip button, in milliseconds.
|
||||
*/
|
||||
private static long getSkipButtonDuration() {
|
||||
return Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().adjustedDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The adjusted duration to show the skipped toast, in milliseconds.
|
||||
*/
|
||||
private static long getToastDuration() {
|
||||
return Settings.SB_TOAST_ON_SKIP_DURATION.get().adjustedDuration;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static SponsorSegment[] getSegments() {
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static void setSegments(@NonNull SponsorSegment[] videoSegments) {
|
||||
private static void setSegments(SponsorSegment[] videoSegments) {
|
||||
Arrays.sort(videoSegments);
|
||||
segments = videoSegments;
|
||||
calculateTimeWithoutSegments();
|
||||
|
||||
if (SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY
|
||||
if (SegmentCategory.HIGHLIGHT.behaviour == SKIP_AUTOMATICALLY
|
||||
|| SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.MANUAL_SKIP) {
|
||||
for (SponsorSegment segment : videoSegments) {
|
||||
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||
@@ -125,7 +228,7 @@ public class SegmentPlaybackController {
|
||||
highlightSegment = null;
|
||||
}
|
||||
|
||||
static void addUnsubmittedSegment(@NonNull SponsorSegment segment) {
|
||||
static void addUnsubmittedSegment(SponsorSegment segment) {
|
||||
Objects.requireNonNull(segment);
|
||||
if (segments == null) {
|
||||
segments = new SponsorSegment[1];
|
||||
@@ -140,6 +243,7 @@ public class SegmentPlaybackController {
|
||||
if (segments == null || segments.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<SponsorSegment> replacement = new ArrayList<>();
|
||||
for (SponsorSegment segment : segments) {
|
||||
if (segment.category != SegmentCategory.UNSUBMITTED) {
|
||||
@@ -156,7 +260,7 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all downloaded data.
|
||||
* Clear all data.
|
||||
*/
|
||||
private static void clearData() {
|
||||
currentVideoId = null;
|
||||
@@ -170,6 +274,8 @@ public class SegmentPlaybackController {
|
||||
skipSegmentButtonEndTime = 0;
|
||||
toastSegmentSkipped = null;
|
||||
toastNumberOfSegmentsSkipped = 0;
|
||||
undoAutoSkipRange = null;
|
||||
undoAutoSkipRangeToast = null;
|
||||
hiddenSkipSegmentsForCurrentVideoTime.clear();
|
||||
}
|
||||
|
||||
@@ -186,7 +292,7 @@ public class SegmentPlaybackController {
|
||||
SponsorBlockUtils.clearUnsubmittedSegmentTimes();
|
||||
Logger.printDebug(() -> "Initialized SponsorBlock");
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to initialize SponsorBlock", ex);
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +309,7 @@ public class SegmentPlaybackController {
|
||||
return;
|
||||
}
|
||||
if (PlayerType.getCurrent().isNoneOrHidden()) {
|
||||
Logger.printDebug(() -> "ignoring Short");
|
||||
Logger.printDebug(() -> "Ignoring Short");
|
||||
return;
|
||||
}
|
||||
if (!Utils.isNetworkConnected()) {
|
||||
@@ -212,7 +318,7 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
|
||||
currentVideoId = videoId;
|
||||
Logger.printDebug(() -> "setCurrentVideoId: " + videoId);
|
||||
Logger.printDebug(() -> "New video ID: " + videoId);
|
||||
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
@@ -227,42 +333,39 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called off main thread
|
||||
* Must be called off main thread.
|
||||
*/
|
||||
static void executeDownloadSegments(@NonNull String videoId) {
|
||||
static void executeDownloadSegments(String videoId) {
|
||||
Objects.requireNonNull(videoId);
|
||||
try {
|
||||
SponsorSegment[] segments = SBRequester.getSegments(videoId);
|
||||
|
||||
Utils.runOnMainThread(()-> {
|
||||
if (!videoId.equals(currentVideoId)) {
|
||||
// user changed videos before get segments network call could complete
|
||||
Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId);
|
||||
return;
|
||||
}
|
||||
setSegments(segments);
|
||||
SponsorSegment[] segments = SBRequester.getSegments(videoId);
|
||||
|
||||
final long videoTime = VideoInformation.getVideoTime();
|
||||
if (highlightSegment != null) {
|
||||
// If the current video time is before the highlight.
|
||||
final long timeUntilHighlight = highlightSegment.start - videoTime;
|
||||
if (timeUntilHighlight > 0) {
|
||||
if (highlightSegment.shouldAutoSkip()) {
|
||||
skipSegment(highlightSegment, false);
|
||||
return;
|
||||
}
|
||||
highlightSegmentInitialShowEndTime = System.currentTimeMillis() + Math.min(
|
||||
(long) (timeUntilHighlight / VideoInformation.getPlaybackSpeed()),
|
||||
DURATION_TO_SHOW_SKIP_BUTTON);
|
||||
Utils.runOnMainThread(() -> {
|
||||
if (!videoId.equals(currentVideoId)) {
|
||||
// user changed videos before get segments network call could complete
|
||||
Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId);
|
||||
return;
|
||||
}
|
||||
setSegments(segments);
|
||||
|
||||
final long videoTime = VideoInformation.getVideoTime();
|
||||
if (highlightSegment != null) {
|
||||
// If the current video time is before the highlight.
|
||||
final long timeUntilHighlight = highlightSegment.start - videoTime;
|
||||
if (timeUntilHighlight > 0) {
|
||||
if (highlightSegment.shouldAutoSkip()) {
|
||||
skipSegment(highlightSegment, false);
|
||||
return;
|
||||
}
|
||||
highlightSegmentInitialShowEndTime = System.currentTimeMillis() + Math.min(
|
||||
(long) (timeUntilHighlight / VideoInformation.getPlaybackSpeed()),
|
||||
getSkipButtonDuration());
|
||||
}
|
||||
}
|
||||
|
||||
// check for any skips now, instead of waiting for the next update to setVideoTime()
|
||||
setVideoTime(videoTime);
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "executeDownloadSegments failure", ex);
|
||||
}
|
||||
// check for any skips now, instead of waiting for the next update to setVideoTime()
|
||||
setVideoTime(videoTime);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,8 +376,8 @@ public class SegmentPlaybackController {
|
||||
public static void setVideoTime(long millis) {
|
||||
try {
|
||||
if (!Settings.SB_ENABLED.get()
|
||||
|| PlayerType.getCurrent().isNoneOrHidden() // Shorts playback.
|
||||
|| segments == null || segments.length == 0) {
|
||||
|| PlayerType.getCurrent().isNoneOrHidden() // Shorts playback.
|
||||
|| segments == null || segments.length == 0) {
|
||||
return;
|
||||
}
|
||||
Logger.printDebug(() -> "setVideoTime: " + millis);
|
||||
@@ -290,7 +393,7 @@ public class SegmentPlaybackController {
|
||||
//
|
||||
// To debug the stale skip logic, set this to a very large value (5000 or more)
|
||||
// then try manually seeking just before playback reaches a segment skip.
|
||||
final long speedAdjustedTimeThreshold = (long)(playbackSpeed * 1200);
|
||||
final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 1200);
|
||||
final long startTimerLookAheadThreshold = millis + speedAdjustedTimeThreshold;
|
||||
|
||||
SponsorSegment foundSegmentCurrentlyPlaying = null;
|
||||
@@ -298,22 +401,24 @@ public class SegmentPlaybackController {
|
||||
|
||||
for (final SponsorSegment segment : segments) {
|
||||
if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR
|
||||
|| segment.category.behaviour == CategoryBehaviour.IGNORE
|
||||
|| segment.category == SegmentCategory.HIGHLIGHT) {
|
||||
|| segment.category.behaviour == CategoryBehaviour.IGNORE
|
||||
|| segment.category == SegmentCategory.HIGHLIGHT) {
|
||||
continue;
|
||||
}
|
||||
if (segment.end <= millis) {
|
||||
continue; // past this segment
|
||||
continue; // Past this segment.
|
||||
}
|
||||
|
||||
final boolean segmentShouldAutoSkip = shouldAutoSkipAndUndoSkipNotActive(segment, millis);
|
||||
|
||||
if (segment.start <= millis) {
|
||||
// we are in the segment!
|
||||
if (segment.shouldAutoSkip()) {
|
||||
// We are in the segment!
|
||||
if (segmentShouldAutoSkip) {
|
||||
skipSegment(segment, false);
|
||||
return; // must return, as skipping causes a recursive call back into this method
|
||||
return; // Must return, as skipping causes a recursive call back into this method.
|
||||
}
|
||||
|
||||
// first found segment, or it's an embedded segment and fully inside the outer segment
|
||||
// First found segment, or it's an embedded segment and fully inside the outer segment.
|
||||
if (foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) {
|
||||
// If the found segment is not currently displayed, then do not show if the segment is nearly over.
|
||||
// This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time.
|
||||
@@ -327,25 +432,27 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
}
|
||||
// Keep iterating and looking. There may be an upcoming autoskip,
|
||||
// or there may be another smaller segment nested inside this segment
|
||||
// or there may be another smaller segment nested inside this segment.
|
||||
continue;
|
||||
}
|
||||
|
||||
// segment is upcoming
|
||||
// Segment is upcoming.
|
||||
if (startTimerLookAheadThreshold < segment.start) {
|
||||
break; // segment is not close enough to schedule, and no segments after this are of interest
|
||||
// Segment is not close enough to schedule, and no segments after this are of interest.
|
||||
break;
|
||||
}
|
||||
if (segment.shouldAutoSkip()) { // upcoming autoskip
|
||||
|
||||
if (segmentShouldAutoSkip) {
|
||||
foundUpcomingSegment = segment;
|
||||
break; // must stop here
|
||||
break; // Must stop here.
|
||||
}
|
||||
|
||||
// upcoming manual skip
|
||||
// Upcoming manual skip.
|
||||
|
||||
// do not schedule upcoming segment, if it is not fully contained inside the current segment
|
||||
// Do not schedule upcoming segment, if it is not fully contained inside the current segment.
|
||||
if ((foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment))
|
||||
// use the most inner upcoming segment
|
||||
&& (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) {
|
||||
// Use the most inner upcoming segment.
|
||||
&& (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) {
|
||||
|
||||
// Only schedule, if the segment start time is not near the end time of the current segment.
|
||||
// This check is needed to prevent scheduled hide and show from clashing with each other.
|
||||
@@ -361,8 +468,8 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
|
||||
if (highlightSegment != null) {
|
||||
if (millis < DURATION_TO_SHOW_SKIP_BUTTON || (highlightSegmentInitialShowEndTime != 0
|
||||
&& System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) {
|
||||
if (millis < getSkipButtonDuration() || (highlightSegmentInitialShowEndTime != 0
|
||||
&& System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) {
|
||||
SponsorBlockViewController.showSkipHighlightButton(highlightSegment);
|
||||
} else {
|
||||
highlightSegmentInitialShowEndTime = 0;
|
||||
@@ -373,16 +480,17 @@ public class SegmentPlaybackController {
|
||||
if (segmentCurrentlyPlaying != foundSegmentCurrentlyPlaying) {
|
||||
setSegmentCurrentlyPlaying(foundSegmentCurrentlyPlaying);
|
||||
} else if (foundSegmentCurrentlyPlaying != null
|
||||
&& skipSegmentButtonEndTime != 0 && skipSegmentButtonEndTime <= System.currentTimeMillis()) {
|
||||
&& skipSegmentButtonEndTime != 0
|
||||
&& skipSegmentButtonEndTime <= System.currentTimeMillis()) {
|
||||
Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying);
|
||||
skipSegmentButtonEndTime = 0;
|
||||
hiddenSkipSegmentsForCurrentVideoTime.add(foundSegmentCurrentlyPlaying);
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
}
|
||||
|
||||
// schedule a hide, only if the segment end is near
|
||||
final SponsorSegment segmentToHide =
|
||||
(foundSegmentCurrentlyPlaying != null && foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold))
|
||||
// Schedule a hide, but only if the segment end is near.
|
||||
final SponsorSegment segmentToHide = (foundSegmentCurrentlyPlaying != null &&
|
||||
foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold))
|
||||
? foundSegmentCurrentlyPlaying
|
||||
: null;
|
||||
|
||||
@@ -407,7 +515,7 @@ public class SegmentPlaybackController {
|
||||
|
||||
final long videoTime = VideoInformation.getVideoTime();
|
||||
if (!segmentToHide.endIsNear(videoTime, speedAdjustedTimeThreshold)) {
|
||||
// current video time is not what's expected. User paused playback
|
||||
// Current video time is not what's expected. User paused playback.
|
||||
Logger.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide
|
||||
+ " videoInformation time: " + videoTime);
|
||||
return;
|
||||
@@ -416,7 +524,7 @@ public class SegmentPlaybackController {
|
||||
// Need more than just hide the skip button, as this may have been an embedded segment
|
||||
// Instead call back into setVideoTime to check everything again.
|
||||
// Should not use VideoInformation time as it is less accurate,
|
||||
// but this scheduled handler was scheduled precisely so we can just use the segment end time
|
||||
// but this scheduled handler was scheduled precisely so we can just use the segment end time.
|
||||
setSegmentCurrentlyPlaying(null);
|
||||
setVideoTime(segmentToHide.end);
|
||||
}, delayUntilHide);
|
||||
@@ -446,12 +554,12 @@ public class SegmentPlaybackController {
|
||||
|
||||
final long videoTime = VideoInformation.getVideoTime();
|
||||
if (!segmentToSkip.startIsNear(videoTime, speedAdjustedTimeThreshold)) {
|
||||
// current video time is not what's expected. User paused playback
|
||||
// Current video time is not what's expected. User paused playback.
|
||||
Logger.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip
|
||||
+ " videoInformation time: " + videoTime);
|
||||
return;
|
||||
}
|
||||
if (segmentToSkip.shouldAutoSkip()) {
|
||||
if (shouldAutoSkipAndUndoSkipNotActive(segmentToSkip, videoTime)) {
|
||||
Logger.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip);
|
||||
skipSegment(segmentToSkip, false);
|
||||
} else {
|
||||
@@ -461,6 +569,12 @@ public class SegmentPlaybackController {
|
||||
}, delayUntilSkip);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear undo range if video time is outside the segment. Must check last.
|
||||
if (undoAutoSkipRange != null && !undoAutoSkipRange.contains(millis)) {
|
||||
Logger.printDebug(() -> "Clearing undo range as current time is now outside range: " + undoAutoSkipRange);
|
||||
undoAutoSkipRange = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "setVideoTime failure", e);
|
||||
}
|
||||
@@ -470,14 +584,13 @@ public class SegmentPlaybackController {
|
||||
* Removes all previously hidden segments that are not longer contained in the given video time.
|
||||
*/
|
||||
private static void updateHiddenSegments(long currentVideoTime) {
|
||||
Iterator<SponsorSegment> i = hiddenSkipSegmentsForCurrentVideoTime.iterator();
|
||||
while (i.hasNext()) {
|
||||
SponsorSegment hiddenSegment = i.next();
|
||||
hiddenSkipSegmentsForCurrentVideoTime.removeIf((hiddenSegment) -> {
|
||||
if (!hiddenSegment.containsTime(currentVideoTime)) {
|
||||
Logger.printDebug(() -> "Resetting hide skip button: " + hiddenSegment);
|
||||
i.remove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) {
|
||||
@@ -488,8 +601,10 @@ public class SegmentPlaybackController {
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
return;
|
||||
}
|
||||
|
||||
segmentCurrentlyPlaying = segment;
|
||||
skipSegmentButtonEndTime = 0;
|
||||
|
||||
if (Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()) {
|
||||
if (hiddenSkipSegmentsForCurrentVideoTime.contains(segment)) {
|
||||
// Playback exited a nested segment and the outer segment skip button was previously hidden.
|
||||
@@ -497,16 +612,13 @@ public class SegmentPlaybackController {
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
return;
|
||||
}
|
||||
skipSegmentButtonEndTime = System.currentTimeMillis() + DURATION_TO_SHOW_SKIP_BUTTON;
|
||||
skipSegmentButtonEndTime = System.currentTimeMillis() + getSkipButtonDuration();
|
||||
}
|
||||
Logger.printDebug(() -> "Showing segment: " + segment);
|
||||
SponsorBlockViewController.showSkipSegmentButton(segment);
|
||||
}
|
||||
|
||||
private static SponsorSegment lastSegmentSkipped;
|
||||
private static long lastSegmentSkippedTime;
|
||||
|
||||
private static void skipSegment(@NonNull SponsorSegment segmentToSkip, boolean userManuallySkipped) {
|
||||
private static void skipSegment(SponsorSegment segmentToSkip, boolean userManuallySkipped) {
|
||||
try {
|
||||
SponsorBlockViewController.hideSkipHighlightButton();
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
@@ -525,7 +637,7 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Skipping segment: " + segmentToSkip);
|
||||
Logger.printDebug(() -> "Skipping segment: " + segmentToSkip + " videoState: " + VideoState.getCurrent());
|
||||
lastSegmentSkipped = segmentToSkip;
|
||||
lastSegmentSkippedTime = now;
|
||||
setSegmentCurrentlyPlaying(null);
|
||||
@@ -535,29 +647,39 @@ public class SegmentPlaybackController {
|
||||
highlightSegmentInitialShowEndTime = 0;
|
||||
}
|
||||
|
||||
// Set or update undo skip range.
|
||||
Range<Long> range = segmentToSkip.getUndoRange();
|
||||
if (undoAutoSkipRange == null) {
|
||||
Logger.printDebug(() -> "Setting new undo range to: " + range);
|
||||
undoAutoSkipRange = range;
|
||||
} else {
|
||||
Range<Long> extendedRange = undoAutoSkipRange.extend(range);
|
||||
Logger.printDebug(() -> "Extending undo range from: " + undoAutoSkipRange +
|
||||
" to: " + extendedRange);
|
||||
undoAutoSkipRange = extendedRange;
|
||||
}
|
||||
undoAutoSkipRangeToast = undoAutoSkipRange;
|
||||
|
||||
// If the seek is successful, then the seek causes a recursive call back into this class.
|
||||
final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end);
|
||||
if (!seekSuccessful) {
|
||||
// can happen when switching videos and is normal
|
||||
// Can happen when switching videos and is normal.
|
||||
Logger.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean videoIsPaused = VideoState.getCurrent() == VideoState.PAUSED;
|
||||
if (!userManuallySkipped) {
|
||||
// check for any smaller embedded segments, and count those as autoskipped
|
||||
// Check for any smaller embedded segments, and count those as auto-skipped.
|
||||
final boolean showSkipToast = Settings.SB_TOAST_ON_SKIP.get();
|
||||
for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) {
|
||||
for (SponsorSegment otherSegment : Objects.requireNonNull(segments)) {
|
||||
if (segmentToSkip.end < otherSegment.start) {
|
||||
break; // no other segments can be contained
|
||||
break; // No other segments can be contained.
|
||||
}
|
||||
|
||||
if (otherSegment == segmentToSkip ||
|
||||
(otherSegment.category != SegmentCategory.HIGHLIGHT && segmentToSkip.containsSegment(otherSegment))) {
|
||||
otherSegment.didAutoSkipped = true;
|
||||
// Do not show a toast if the user is scrubbing thru a paused video.
|
||||
// Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date.
|
||||
// So instead, only hide toasts because all other skip logic done while paused causes no harm.
|
||||
if (showSkipToast && !videoIsPaused) {
|
||||
if (showSkipToast) {
|
||||
showSkippedSegmentToast(otherSegment);
|
||||
}
|
||||
}
|
||||
@@ -567,7 +689,7 @@ public class SegmentPlaybackController {
|
||||
if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) {
|
||||
removeUnsubmittedSegments();
|
||||
SponsorBlockUtils.setNewSponsorSegmentPreviewed();
|
||||
} else if (!videoIsPaused) {
|
||||
} else if (VideoState.getCurrent() != VideoState.PAUSED) {
|
||||
SponsorBlockUtils.sendViewRequestAsync(segmentToSkip);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@@ -575,31 +697,49 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the segment should be auto-skipped _and_ if undo autoskip is not active.
|
||||
*/
|
||||
private static boolean shouldAutoSkipAndUndoSkipNotActive(SponsorSegment segment, long currentVideoTime) {
|
||||
return segment.shouldAutoSkip() && (undoAutoSkipRange == null
|
||||
|| !undoAutoSkipRange.contains(currentVideoTime));
|
||||
}
|
||||
|
||||
private static int toastNumberOfSegmentsSkipped;
|
||||
@Nullable
|
||||
private static SponsorSegment toastSegmentSkipped;
|
||||
|
||||
private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) {
|
||||
private static void showSkippedSegmentToast(SponsorSegment segment) {
|
||||
Utils.verifyOnMainThread();
|
||||
toastNumberOfSegmentsSkipped++;
|
||||
if (toastNumberOfSegmentsSkipped > 1) {
|
||||
return; // toast already scheduled
|
||||
}
|
||||
toastSegmentSkipped = segment;
|
||||
if (toastNumberOfSegmentsSkipped++ > 0) {
|
||||
return; // Toast is already scheduled.
|
||||
}
|
||||
|
||||
final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments
|
||||
// Maximum time between skips to be considered skipping multiple segments.
|
||||
final long delayToToastMilliseconds = 250;
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
try {
|
||||
if (toastSegmentSkipped == null) { // video was changed just after skipping segment
|
||||
// Do not show a toast if the user is scrubbing thru a paused video.
|
||||
// Cannot do this video state check in setTime or before calling this this method,
|
||||
// as the video state may not be up to date. So instead, only ignore the toast
|
||||
// just before it's about to show since the video state is up to date.
|
||||
if (VideoState.getCurrent() == VideoState.PAUSED) {
|
||||
Logger.printDebug(() -> "Ignoring scheduled toast as video state is paused");
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
|
||||
Logger.printDebug(() -> "Not showing autoskip toast as playback is PiP");
|
||||
return;
|
||||
}
|
||||
|
||||
if (toastSegmentSkipped == null || undoAutoSkipRangeToast == null) {
|
||||
// Video was changed immediately after skipping segment.
|
||||
Logger.printDebug(() -> "Ignoring old scheduled show toast");
|
||||
return;
|
||||
}
|
||||
Utils.showToastShort(toastNumberOfSegmentsSkipped == 1
|
||||
String message = toastNumberOfSegmentsSkipped == 1
|
||||
? toastSegmentSkipped.getSkippedToastText()
|
||||
: str("revanced_sb_skipped_multiple_segments"));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showSkippedSegmentToast failure", ex);
|
||||
: str("revanced_sb_skipped_multiple_segments");
|
||||
|
||||
showAutoSkipToast(message, undoAutoSkipRangeToast);
|
||||
} finally {
|
||||
toastNumberOfSegmentsSkipped = 0;
|
||||
toastSegmentSkipped = null;
|
||||
@@ -607,13 +747,125 @@ public class SegmentPlaybackController {
|
||||
}, delayToToastMilliseconds);
|
||||
}
|
||||
|
||||
private static void showAutoSkipToast(String messageToToast, Range<Long> rangeToUndo) {
|
||||
Objects.requireNonNull(messageToToast);
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
Context currentContext = SponsorBlockViewController.getOverLaysViewGroupContext();
|
||||
if (currentContext == null) {
|
||||
Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||
|
||||
Dialog dialog = new Dialog(currentContext);
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
// Do not dismiss dialog if tapped outside the dialog bounds.
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
|
||||
LinearLayout mainLayout = new LinearLayout(currentContext);
|
||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
final int dip8 = dipToPixels(8);
|
||||
final int dip16 = dipToPixels(16);
|
||||
mainLayout.setPadding(dip16, dip8, dip16, dip8);
|
||||
mainLayout.setGravity(Gravity.CENTER);
|
||||
mainLayout.setMinimumHeight(dipToPixels(48));
|
||||
|
||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.createCornerRadii(20), null, null));
|
||||
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||
mainLayout.setBackground(background);
|
||||
|
||||
TextView textView = new TextView(currentContext);
|
||||
textView.setText(messageToToast);
|
||||
textView.setTextSize(14);
|
||||
textView.setTextColor(Utils.getAppForegroundColor());
|
||||
textView.setGravity(Gravity.CENTER);
|
||||
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
textParams.gravity = Gravity.CENTER;
|
||||
textView.setLayoutParams(textParams);
|
||||
mainLayout.addView(textView);
|
||||
mainLayout.setAlpha(0.8f); // Opacity for the entire dialog.
|
||||
|
||||
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
|
||||
Animation fadeIn = Utils.getResourceAnimation("fade_in");
|
||||
Animation fadeOut = Utils.getResourceAnimation("fade_out");
|
||||
fadeIn.setDuration(fadeDurationFast);
|
||||
fadeOut.setDuration(fadeDurationFast);
|
||||
fadeOut.setAnimationListener(new Animation.AnimationListener() {
|
||||
public void onAnimationStart(Animation animation) { }
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
if (dialog.isShowing()) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
public void onAnimationRepeat(Animation animation) { }
|
||||
});
|
||||
|
||||
mainLayout.setOnClickListener(v -> {
|
||||
try {
|
||||
Logger.printDebug(() -> "Undoing autoskip using range: " + rangeToUndo);
|
||||
// Restore undo autoskip range since it's already cleared by now.
|
||||
undoAutoSkipRange = rangeToUndo;
|
||||
VideoInformation.seekTo(rangeToUndo.getLower());
|
||||
|
||||
mainLayout.startAnimation(fadeOut);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showToastShortWithTapAction setOnClickListener failure", ex);
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
mainLayout.setClickable(true);
|
||||
dialog.setContentView(mainLayout);
|
||||
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
// Remove window animations and use custom fade animation.
|
||||
window.setWindowAnimations(0);
|
||||
|
||||
WindowManager.LayoutParams params = window.getAttributes();
|
||||
params.gravity = Gravity.BOTTOM;
|
||||
params.y = dipToPixels(72);
|
||||
int portraitWidth = Utils.percentageWidthToPixels(60); // 60% of the screen width.
|
||||
|
||||
if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
portraitWidth = Math.min(portraitWidth, Utils.percentageHeightToPixels(60)); // 60% of the screen height.
|
||||
}
|
||||
params.width = portraitWidth;
|
||||
params.dimAmount = 0.0f;
|
||||
window.setAttributes(params);
|
||||
window.setBackgroundDrawable(null);
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
|
||||
}
|
||||
|
||||
if (dismissUndoToast()) {
|
||||
Logger.printDebug(() -> "Dismissed previous skip toast that was still on screen");
|
||||
}
|
||||
toastDialogRef = new WeakReference<>(dialog);
|
||||
|
||||
mainLayout.startAnimation(fadeIn);
|
||||
dialog.show();
|
||||
|
||||
// Fade out and dismiss the dialog if the user does not undo the skip.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
if (dialog.isShowing()) {
|
||||
mainLayout.startAnimation(fadeOut);
|
||||
}
|
||||
}, getToastDuration());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param segment can be either a highlight or a regular manual skip segment.
|
||||
*/
|
||||
public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) {
|
||||
public static void onSkipSegmentClicked(SponsorSegment segment) {
|
||||
try {
|
||||
if (segment != highlightSegment && segment != segmentCurrentlyPlaying) {
|
||||
Logger.printException(() -> "error: segment not available to skip"); // should never happen
|
||||
Logger.printException(() -> "error: segment not available to skip"); // Should never happen.
|
||||
SponsorBlockViewController.hideSkipSegmentButton();
|
||||
SponsorBlockViewController.hideSkipHighlightButton();
|
||||
return;
|
||||
@@ -628,7 +880,7 @@ public class SegmentPlaybackController {
|
||||
* Injection point
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void setSponsorBarRect(final Object self) {
|
||||
public static void setSponsorBarRect(Object self) {
|
||||
try {
|
||||
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
|
||||
field.setAccessible(true);
|
||||
@@ -651,7 +903,7 @@ public class SegmentPlaybackController {
|
||||
private static void setSponsorBarAbsoluteRight(Rect rect) {
|
||||
final int right = rect.right;
|
||||
if (sponsorAbsoluteBarRight != right) {
|
||||
Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right);
|
||||
Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right);
|
||||
sponsorAbsoluteBarRight = right;
|
||||
}
|
||||
}
|
||||
@@ -726,12 +978,6 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actual screen pixel width to use for the highlight segment time bar.
|
||||
*/
|
||||
private static final int highlightSegmentTimeBarScreenWidth
|
||||
= Utils.dipToPixels(HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH);
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@@ -752,9 +998,9 @@ public class SegmentPlaybackController {
|
||||
final float left = leftPadding + segment.start * videoMillisecondsToPixels;
|
||||
final float right;
|
||||
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||
right = left + highlightSegmentTimeBarScreenWidth;
|
||||
right = left + HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH;
|
||||
} else {
|
||||
right = leftPadding + segment.end * videoMillisecondsToPixels;
|
||||
right = leftPadding + segment.end * videoMillisecondsToPixels;
|
||||
}
|
||||
canvas.drawRect(left, top, right, bottom, segment.category.paint);
|
||||
}
|
||||
@@ -762,5 +1008,4 @@ public class SegmentPlaybackController {
|
||||
Logger.printException(() -> "drawSponsorTimeBars failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -223,13 +223,18 @@ public class SponsorBlockUtils {
|
||||
Logger.printException(() -> "invalid parameters");
|
||||
return;
|
||||
}
|
||||
|
||||
clearUnsubmittedSegmentTimes();
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
SBRequester.submitSegments(videoId, segmentCategory.keyValue, start, end, videoLength);
|
||||
SegmentPlaybackController.executeDownloadSegments(videoId);
|
||||
try {
|
||||
SBRequester.submitSegments(videoId, segmentCategory.keyValue, start, end, videoLength);
|
||||
SegmentPlaybackController.executeDownloadSegments(videoId);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "submitNewSegment failure", ex);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "Unable to submit segment", e);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "submitNewSegment failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,7 +371,7 @@ public class SponsorBlockUtils {
|
||||
}
|
||||
|
||||
|
||||
static void sendViewRequestAsync(@NonNull SponsorSegment segment) {
|
||||
static void sendViewRequestAsync(SponsorSegment segment) {
|
||||
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
|
||||
return;
|
||||
}
|
||||
@@ -409,7 +414,7 @@ public class SponsorBlockUtils {
|
||||
return statsNumberFormatter.format(viewCount);
|
||||
}
|
||||
|
||||
private static long parseSegmentTime(@NonNull String time) {
|
||||
private static long parseSegmentTime(String time) {
|
||||
Matcher matcher = manualEditTimePattern.matcher(time);
|
||||
if (!matcher.matches()) {
|
||||
return -1;
|
||||
@@ -419,9 +424,12 @@ public class SponsorBlockUtils {
|
||||
String secondsStr = matcher.group(4);
|
||||
String millisecondsStr = matcher.group(6); // Milliseconds is optional.
|
||||
|
||||
|
||||
try {
|
||||
final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0;
|
||||
//noinspection ConstantConditions
|
||||
final int minutes = Integer.parseInt(minutesStr);
|
||||
//noinspection ConstantConditions
|
||||
final int seconds = Integer.parseInt(secondsStr);
|
||||
final int milliseconds;
|
||||
if (millisecondsStr != null) {
|
||||
@@ -468,32 +476,29 @@ public class SponsorBlockUtils {
|
||||
}
|
||||
|
||||
public static String getTimeSavedString(long totalSecondsSaved) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
Duration duration = Duration.ofSeconds(totalSecondsSaved);
|
||||
final long hours = duration.toHours();
|
||||
final long minutes = duration.toMinutes() % 60;
|
||||
Duration duration = Duration.ofSeconds(totalSecondsSaved);
|
||||
final long hours = duration.toHours();
|
||||
final long minutes = duration.toMinutes() % 60;
|
||||
|
||||
// Format all numbers so non-western numbers use a consistent appearance.
|
||||
String minutesFormatted = statsNumberFormatter.format(minutes);
|
||||
if (hours > 0) {
|
||||
String hoursFormatted = statsNumberFormatter.format(hours);
|
||||
return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted);
|
||||
}
|
||||
|
||||
final long seconds = duration.getSeconds() % 60;
|
||||
String secondsFormatted = statsNumberFormatter.format(seconds);
|
||||
if (minutes > 0) {
|
||||
return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted);
|
||||
}
|
||||
|
||||
return str("revanced_sb_stats_saved_second_format", secondsFormatted);
|
||||
// Format all numbers so non-western numbers use a consistent appearance.
|
||||
String minutesFormatted = statsNumberFormatter.format(minutes);
|
||||
if (hours > 0) {
|
||||
String hoursFormatted = statsNumberFormatter.format(hours);
|
||||
return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted);
|
||||
}
|
||||
return "error"; // will never be reached. YouTube requires Android O or greater
|
||||
|
||||
final long seconds = duration.getSeconds() % 60;
|
||||
String secondsFormatted = statsNumberFormatter.format(seconds);
|
||||
if (minutes > 0) {
|
||||
return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted);
|
||||
}
|
||||
|
||||
return str("revanced_sb_stats_saved_second_format", secondsFormatted);
|
||||
}
|
||||
|
||||
private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener {
|
||||
boolean settingStart;
|
||||
WeakReference<EditText> editTextRef = new WeakReference<>(null);
|
||||
private boolean settingStart;
|
||||
private WeakReference<EditText> editTextRef = new WeakReference<>(null);
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
@@ -512,10 +517,11 @@ public class SponsorBlockUtils {
|
||||
}
|
||||
}
|
||||
|
||||
if (settingStart)
|
||||
if (settingStart) {
|
||||
newSponsorSegmentStartMillis = Math.max(time, 0);
|
||||
else
|
||||
} else {
|
||||
newSponsorSegmentEndMillis = time;
|
||||
}
|
||||
|
||||
if (which == DialogInterface.BUTTON_NEUTRAL)
|
||||
editByHandDialogListener.onClick(dialog, settingStart ?
|
||||
|
||||
@@ -9,7 +9,10 @@ import java.util.Objects;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.sf;
|
||||
|
||||
import android.util.Range;
|
||||
|
||||
public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||
|
||||
public enum SegmentVote {
|
||||
UPVOTE(sf("revanced_sb_vote_upvote"), 1,false),
|
||||
DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true),
|
||||
@@ -38,7 +41,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||
@NonNull
|
||||
public final SegmentCategory category;
|
||||
/**
|
||||
* NULL if segment is unsubmitted
|
||||
* NULL if segment is unsubmitted.
|
||||
*/
|
||||
@Nullable
|
||||
public final String UUID;
|
||||
@@ -64,33 +67,54 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
|
||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number.
|
||||
*/
|
||||
public boolean startIsNear(long videoTime, long nearThreshold) {
|
||||
return Math.abs(start - videoTime) <= nearThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
|
||||
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number.
|
||||
*/
|
||||
public boolean endIsNear(long videoTime, long nearThreshold) {
|
||||
return Math.abs(end - videoTime) <= nearThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the time parameter is within this segment
|
||||
* @return if the time parameter is within this segment.
|
||||
*/
|
||||
public boolean containsTime(long videoTime) {
|
||||
return start <= videoTime && videoTime < end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the segment is completely contained inside this segment
|
||||
* @return if the segment is completely contained inside this segment.
|
||||
*/
|
||||
public boolean containsSegment(SponsorSegment other) {
|
||||
return start <= other.start && other.end <= end;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the range has any overlap with this segment.
|
||||
*/
|
||||
public boolean intersectsRange(Range<Long> range) {
|
||||
return range.getLower() < end && range.getUpper() >= start;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The start/end time in range form.
|
||||
* Range times are adjusted since it uses inclusive and Segments use exclusive.
|
||||
*
|
||||
* {@link SegmentCategory#HIGHLIGHT} is unique and
|
||||
* returns a range from the start of the video until the highlight.
|
||||
*/
|
||||
public Range<Long> getUndoRange() {
|
||||
final long undoStart = category == SegmentCategory.HIGHLIGHT
|
||||
? 0
|
||||
: start;
|
||||
return Range.create(undoStart, end - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the length of this segment, in milliseconds. Always a positive number.
|
||||
*/
|
||||
@@ -99,7 +123,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 'skip segment' UI overlay button text
|
||||
* @return 'skip segment' UI overlay button text.
|
||||
*/
|
||||
@NonNull
|
||||
public String getSkipButtonText() {
|
||||
@@ -107,7 +131,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 'skipped segment' toast message
|
||||
* @return 'skipped segment' toast message.
|
||||
*/
|
||||
@NonNull
|
||||
public String getSkippedToastText() {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class SBRequester {
|
||||
private SBRequester() {
|
||||
}
|
||||
|
||||
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
|
||||
private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
|
||||
if (Settings.SB_TOAST_ON_CONNECTION_ERROR.get()) {
|
||||
Utils.showToastShort(toastMessage);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class SBRequester {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SponsorSegment[] getSegments(@NonNull String videoId) {
|
||||
public static SponsorSegment[] getSegments(String videoId) {
|
||||
Utils.verifyOffMainThread();
|
||||
List<SponsorSegment> segments = new ArrayList<>();
|
||||
try {
|
||||
@@ -113,10 +113,10 @@ public class SBRequester {
|
||||
Logger.printException(() -> "getSegments failure", ex);
|
||||
}
|
||||
|
||||
// Crude debug tests to verify random features
|
||||
// Crude debug tests to verify random features.
|
||||
// Could benefit from:
|
||||
// 1) collection of YouTube videos with test segment times (verify client skip timing matches the video, verify seekbar draws correctly)
|
||||
// 2) unit tests (verify everything else)
|
||||
// 1) Collection of YouTube videos with test segment times (verify client skip timing matches the video, verify seekbar draws correctly).
|
||||
// 2) Unit tests (verify everything else).
|
||||
//noinspection ConstantValue
|
||||
if (false) {
|
||||
segments.clear();
|
||||
@@ -140,10 +140,30 @@ public class SBRequester {
|
||||
segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 200000, 330000, false));
|
||||
}
|
||||
|
||||
// Test undo skip functionality.
|
||||
// To test enable 'Autoskip always' for intro and self promo.
|
||||
//noinspection ConstantValue
|
||||
if (false) {
|
||||
// Should autoskip to 12 seconds.
|
||||
// Undoing skip should seek to 2 seconds.
|
||||
// Skip button should show at 2 seconds, and again at 8 seconds.
|
||||
// Self promo at 8 second time should not autoskip.
|
||||
segments.clear();
|
||||
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 2000, 12000, false));
|
||||
segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 8000, 15000, false));
|
||||
|
||||
// Test multiple autoskip dialogs rapidly showing.
|
||||
// Only one toast should be shown at anytime.
|
||||
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 16000, 17000, false));
|
||||
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 18000, 19000, false));
|
||||
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 20000, 21000, false));
|
||||
segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 22000, 23000, false));
|
||||
}
|
||||
|
||||
return segments.toArray(new SponsorSegment[0]);
|
||||
}
|
||||
|
||||
public static void submitSegments(@NonNull String videoId, @NonNull String category,
|
||||
public static void submitSegments(String videoId, String category,
|
||||
long startTime, long endTime, long videoLength) {
|
||||
Utils.verifyOffMainThread();
|
||||
|
||||
@@ -189,7 +209,7 @@ public class SBRequester {
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendSegmentSkippedViewedRequest(@NonNull SponsorSegment segment) {
|
||||
public static void sendSegmentSkippedViewedRequest(SponsorSegment segment) {
|
||||
Utils.verifyOffMainThread();
|
||||
try {
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID);
|
||||
@@ -208,13 +228,13 @@ public class SBRequester {
|
||||
}
|
||||
}
|
||||
|
||||
public static void voteForSegmentOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption) {
|
||||
public static void voteForSegmentOnBackgroundThread(SponsorSegment segment, SegmentVote voteOption) {
|
||||
voteOrRequestCategoryChange(segment, voteOption, null);
|
||||
}
|
||||
public static void voteToChangeCategoryOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentCategory categoryToVoteFor) {
|
||||
public static void voteToChangeCategoryOnBackgroundThread(SponsorSegment segment, SegmentCategory categoryToVoteFor) {
|
||||
voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor);
|
||||
}
|
||||
private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption, SegmentCategory categoryToVoteFor) {
|
||||
private static void voteOrRequestCategoryChange(SponsorSegment segment, SegmentVote voteOption, SegmentCategory categoryToVoteFor) {
|
||||
Utils.runOnBackgroundThread(() -> {
|
||||
try {
|
||||
String segmentUuid = segment.UUID;
|
||||
@@ -280,7 +300,7 @@ public class SBRequester {
|
||||
* @return NULL if the call was successful. If unsuccessful, an error message is returned.
|
||||
*/
|
||||
@Nullable
|
||||
public static String setUsername(@NonNull String username) {
|
||||
public static String setUsername(String username) {
|
||||
Utils.verifyOffMainThread();
|
||||
try {
|
||||
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.getSBPrivateUserID(), username);
|
||||
@@ -320,14 +340,14 @@ public class SBRequester {
|
||||
|
||||
// helpers
|
||||
|
||||
private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException {
|
||||
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
|
||||
HttpURLConnection connection = Requester.getConnectionFromRoute(Settings.SB_API_URL.get(), route, params);
|
||||
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
|
||||
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static JSONObject getJSONObject(@NonNull Route route, String... params) throws IOException, JSONException {
|
||||
private static JSONObject getJSONObject(Route route, String... params) throws IOException, JSONException {
|
||||
return Requester.parseJSONObject(getConnectionFromRoute(route, params));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.SponsorBlockDuration;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
@@ -28,6 +29,7 @@ import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
||||
@@ -62,6 +64,8 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
private SwitchPreference trackSkips;
|
||||
private SwitchPreference showTimeWithoutSegments;
|
||||
private SwitchPreference toastOnConnectionError;
|
||||
private CustomDialogListPreference autoHideSkipSegmentButtonDuration;
|
||||
private CustomDialogListPreference showSkipToastDuration;
|
||||
|
||||
private ResettableEditTextPreference newSegmentStep;
|
||||
private ResettableEditTextPreference minSegmentDuration;
|
||||
@@ -69,8 +73,8 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
private EditTextPreference importExport;
|
||||
private Preference apiUrl;
|
||||
|
||||
private final List<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
|
||||
private PreferenceCategory segmentCategory;
|
||||
private final List<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
|
||||
|
||||
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
@@ -114,17 +118,23 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
||||
votingEnabled.setEnabled(enabled);
|
||||
|
||||
autoHideSkipSegmentButton.setEnabled(enabled);
|
||||
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
||||
autoHideSkipSegmentButton.setEnabled(enabled);
|
||||
|
||||
autoHideSkipSegmentButtonDuration.setValue(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().toString());
|
||||
autoHideSkipSegmentButtonDuration.setEnabled(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.isAvailable());
|
||||
|
||||
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
||||
compactSkipButton.setEnabled(enabled);
|
||||
|
||||
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
||||
showSkipToast.setEnabled(enabled);
|
||||
|
||||
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
||||
squareLayout.setEnabled(enabled);
|
||||
|
||||
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
||||
showSkipToast.setEnabled(enabled);
|
||||
showSkipToastDuration.setValue(Settings.SB_TOAST_ON_SKIP_DURATION.get().toString());
|
||||
showSkipToastDuration.setEnabled(Settings.SB_TOAST_ON_SKIP_DURATION.isAvailable());
|
||||
|
||||
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
||||
toastOnConnectionError.setEnabled(enabled);
|
||||
@@ -166,7 +176,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
try {
|
||||
super.onAttachedToActivity();
|
||||
|
||||
if (preferencesInitialized) {
|
||||
if (preferencesInitialized) {
|
||||
if (settingsImported) {
|
||||
settingsImported = false;
|
||||
updateUI();
|
||||
@@ -205,17 +215,6 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
});
|
||||
appearanceCategory.addPreference(votingEnabled);
|
||||
|
||||
autoHideSkipSegmentButton = new SwitchPreference(context);
|
||||
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
||||
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
||||
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
||||
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
||||
|
||||
compactSkipButton = new SwitchPreference(context);
|
||||
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
||||
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
||||
@@ -227,25 +226,38 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
});
|
||||
appearanceCategory.addPreference(compactSkipButton);
|
||||
|
||||
squareLayout = new SwitchPreference(context);
|
||||
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
||||
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
||||
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
||||
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
||||
autoHideSkipSegmentButton = new SwitchPreference(context);
|
||||
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
||||
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
||||
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
||||
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
appearanceCategory.addPreference(squareLayout);
|
||||
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
||||
|
||||
String[] durationEntries = Utils.getResourceStringArray("revanced_sb_duration_entries");
|
||||
String[] durationEntryValues = Utils.getResourceStringArray("revanced_sb_duration_entry_values");
|
||||
|
||||
autoHideSkipSegmentButtonDuration = new CustomDialogListPreference(context);
|
||||
autoHideSkipSegmentButtonDuration.setTitle(str("revanced_sb_auto_hide_skip_button_duration"));
|
||||
autoHideSkipSegmentButtonDuration.setSummary(str("revanced_sb_auto_hide_skip_button_duration_sum"));
|
||||
autoHideSkipSegmentButtonDuration.setEntries(durationEntries);
|
||||
autoHideSkipSegmentButtonDuration.setEntryValues(durationEntryValues);
|
||||
autoHideSkipSegmentButtonDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.save(
|
||||
SponsorBlockDuration.valueOf((String) newValue)
|
||||
);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
appearanceCategory.addPreference(autoHideSkipSegmentButtonDuration);
|
||||
|
||||
showSkipToast = new SwitchPreference(context);
|
||||
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
||||
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
||||
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
||||
showSkipToast.setOnPreferenceClickListener(preference1 -> {
|
||||
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
|
||||
return false;
|
||||
});
|
||||
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
||||
updateUI();
|
||||
@@ -253,6 +265,20 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
});
|
||||
appearanceCategory.addPreference(showSkipToast);
|
||||
|
||||
showSkipToastDuration = new CustomDialogListPreference(context);
|
||||
showSkipToastDuration.setTitle(str("revanced_sb_toast_on_skip_duration"));
|
||||
showSkipToastDuration.setSummary(str("revanced_sb_toast_on_skip_duration_sum"));
|
||||
showSkipToastDuration.setEntries(durationEntries);
|
||||
showSkipToastDuration.setEntryValues(durationEntryValues);
|
||||
showSkipToastDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
Settings.SB_TOAST_ON_SKIP_DURATION.save(
|
||||
SponsorBlockDuration.valueOf((String) newValue)
|
||||
);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
appearanceCategory.addPreference(showSkipToastDuration);
|
||||
|
||||
showTimeWithoutSegments = new SwitchPreference(context);
|
||||
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
||||
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
||||
@@ -264,6 +290,17 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
});
|
||||
appearanceCategory.addPreference(showTimeWithoutSegments);
|
||||
|
||||
squareLayout = new SwitchPreference(context);
|
||||
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
||||
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
||||
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
||||
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
||||
updateUI();
|
||||
return true;
|
||||
});
|
||||
appearanceCategory.addPreference(squareLayout);
|
||||
|
||||
segmentCategory = new PreferenceCategory(context);
|
||||
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
||||
addPreference(segmentCategory);
|
||||
|
||||
@@ -203,7 +203,7 @@ public class SponsorBlockViewController {
|
||||
setSkipButtonMargins(skipSponsorButton, isWatchFullScreen);
|
||||
setViewVisibility(skipSponsorButton, skipSegment != null);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Player type changed failure", ex);
|
||||
Logger.printException(() -> "playerTypeChanged failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
||||
org.gradle.parallel = true
|
||||
android.useAndroidX = true
|
||||
kotlin.code.style = official
|
||||
version = 5.30.0-dev.1
|
||||
version = 5.32.0-dev.5
|
||||
|
||||
1318
package-lock.json
generated
1318
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,6 @@
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.10.1",
|
||||
"semantic-release": "^24.2.5"
|
||||
"semantic-release": "^24.2.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +116,8 @@ public final class app/revanced/patches/all/misc/shortcut/sharetargets/RemoveSha
|
||||
public static final fun getRemoveShareTargetsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/spoof/SignatureSpoofPatchKt {
|
||||
public static final fun getSignatureSpoofPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
public final class app/revanced/patches/all/misc/spoof/EnableRomSignatureSpoofingKt {
|
||||
public static final fun getEnableRomSignatureSpoofing ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/targetSdk/SetTargetSdkVersion34Kt {
|
||||
@@ -168,6 +168,10 @@ public final class app/revanced/patches/cricbuzz/ads/DisableAdsPatchKt {
|
||||
public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/cricbuzz/misc/extension/ExtensionPatchKt {
|
||||
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/crunchyroll/ads/HideAdsPatchKt {
|
||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -472,6 +476,10 @@ public final class app/revanced/patches/primevideo/misc/permissions/RenamePermis
|
||||
public static final fun getRenamePermissionsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/primevideo/video/speed/PlaybackSpeedPatchKt {
|
||||
public static final fun getPlaybackSpeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/protonmail/account/RemoveFreeAccountsLimitPatchKt {
|
||||
public static final fun getRemoveFreeAccountsLimitPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
@@ -645,10 +653,10 @@ public final class app/revanced/patches/shared/misc/extension/ExtensionHook {
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/shared/misc/extension/SharedExtensionPatchKt {
|
||||
public static final fun extensionHook (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static final fun extensionHook (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lapp/revanced/patcher/Fingerprint;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static final fun extensionHook (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static final fun extensionHook (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lapp/revanced/patcher/Fingerprint;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/extension/ExtensionHook;
|
||||
public static final fun sharedExtensionPatch (Ljava/lang/String;[Lapp/revanced/patches/shared/misc/extension/ExtensionHook;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static final fun sharedExtensionPatch ([Lapp/revanced/patches/shared/misc/extension/ExtensionHook;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -900,6 +908,10 @@ public final class app/revanced/patches/shared/misc/spoof/UserAgentClientSpoofPa
|
||||
public static final fun userAgentClientSpoofPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/shared/misc/string/ReplaceStringPatchKt {
|
||||
public static final fun replaceStringPatch (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatchKt {
|
||||
public static final fun getRemoveFileSizeLimitPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -1228,6 +1240,11 @@ public final class app/revanced/patches/youtube/interaction/dialog/RemoveViewerD
|
||||
public static final fun getRemoveViewerDiscretionDialogPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/interaction/doubletap/DisableChapterSkipDoubleTapPatchKt {
|
||||
public static final fun getDisableChapterSkipDoubleTapPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static final fun getDisableDoubleTapActionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/interaction/downloads/DownloadsPatchKt {
|
||||
public static final fun getDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package app.revanced.patches.all.misc.spoof
|
||||
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.util.getNode
|
||||
import com.android.apksig.ApkVerifier
|
||||
import com.android.apksig.apk.ApkFormatException
|
||||
import org.w3c.dom.Element
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.util.*
|
||||
|
||||
@Suppress("unused")
|
||||
val enableRomSignatureSpoofing = resourcePatch(
|
||||
name = "Enable ROM signature spoofing",
|
||||
description = "Spoofs the signature via the manifest meta-data \"fake-signature\". " +
|
||||
"This patch only works with ROMs that support signature spoofing.",
|
||||
use = false,
|
||||
) {
|
||||
val signatureOrPath by stringOption(
|
||||
key = "signatureOrApkFilePath",
|
||||
title = "Signature or APK file path",
|
||||
validator = validator@{ signature ->
|
||||
signature ?: return@validator false
|
||||
|
||||
parseSignature(signature) != null
|
||||
},
|
||||
description = "The hex-encoded signature or path to an APK file with the desired signature.",
|
||||
required = true,
|
||||
)
|
||||
execute {
|
||||
document("AndroidManifest.xml").use { document ->
|
||||
val permission = document.createElement("uses-permission").apply {
|
||||
setAttribute("android:name", "android.permission.FAKE_PACKAGE_SIGNATURE")
|
||||
}
|
||||
val manifest = document.getNode("manifest").appendChild(permission)
|
||||
|
||||
|
||||
val fakeSignatureMetadata = document.createElement("meta-data").apply {
|
||||
setAttribute("android:name", "fake-signature")
|
||||
setAttribute("android:value", parseSignature(signatureOrPath!!))
|
||||
}
|
||||
document.getNode("application").appendChild(fakeSignatureMetadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSignature(optionValue: String): String? {
|
||||
// Parse as a hex-encoded signature.
|
||||
try {
|
||||
// TODO: Replace with signature.hexToByteArray when stable in kotlin
|
||||
val signatureBytes = HexFormat.of().parseHex(optionValue)
|
||||
CertificateFactory.getInstance("X.509").generateCertificate(signatureBytes.inputStream())
|
||||
|
||||
return optionValue
|
||||
} catch (_: IllegalArgumentException) {
|
||||
} catch (_: CertificateException) {
|
||||
}
|
||||
|
||||
// Parse as a path to an APK file.
|
||||
try {
|
||||
val apkFile = File(optionValue)
|
||||
if (!apkFile.isFile) return null
|
||||
|
||||
val result = ApkVerifier.Builder(apkFile).build().verify()
|
||||
|
||||
val hexFormat = HexFormat.of()
|
||||
|
||||
val signature = (if (result.isVerifiedUsingV3Scheme) {
|
||||
result.v3SchemeSigners[0].certificate
|
||||
} else if (result.isVerifiedUsingV2Scheme) {
|
||||
result.v2SchemeSigners[0].certificate
|
||||
} else if (result.isVerifiedUsingV1Scheme) {
|
||||
result.v1SchemeSigners[0].certificate
|
||||
} else {
|
||||
return null
|
||||
}).encoded
|
||||
|
||||
return hexFormat.formatHex(signature)
|
||||
} catch (_: IOException) {
|
||||
} catch (_: InvalidPathException) {
|
||||
} catch (_: ApkFormatException) {
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package app.revanced.patches.all.misc.spoof
|
||||
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.util.getNode
|
||||
import com.android.apksig.ApkVerifier
|
||||
import com.android.apksig.apk.ApkFormatException
|
||||
import org.w3c.dom.Element
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.util.*
|
||||
import kotlin.io.path.Path
|
||||
|
||||
val signatureSpoofPatch = resourcePatch(
|
||||
name = "Spoof app signature",
|
||||
description = "Spoofs the app signature via the \"fake-signature\" meta key. " +
|
||||
"This patch only works with patched device roms.",
|
||||
use = false,
|
||||
) {
|
||||
val signature by stringOption(
|
||||
key = "spoofedAppSignature",
|
||||
title = "Signature",
|
||||
validator = { signature ->
|
||||
optionToSignature(signature) != null
|
||||
},
|
||||
description = "The hex-encoded signature or path to an apk file with the desired signature",
|
||||
required = true,
|
||||
)
|
||||
execute {
|
||||
document("AndroidManifest.xml").use { document ->
|
||||
val manifest = document.getNode("manifest") as Element
|
||||
|
||||
val fakeSignaturePermission = document.createElement("uses-permission")
|
||||
fakeSignaturePermission.setAttribute("android:name", "android.permission.FAKE_PACKAGE_SIGNATURE")
|
||||
manifest.appendChild(fakeSignaturePermission)
|
||||
|
||||
val application = document.getNode("application") ?: {
|
||||
val child = document.createElement("application")
|
||||
manifest.appendChild(child)
|
||||
child
|
||||
} as Element;
|
||||
|
||||
val fakeSignatureMetadata = document.createElement("meta-data")
|
||||
fakeSignatureMetadata.setAttribute("android:name", "fake-signature")
|
||||
fakeSignatureMetadata.setAttribute("android:value", optionToSignature(signature))
|
||||
application.appendChild(fakeSignatureMetadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun optionToSignature(signature: String?): String? {
|
||||
if (signature == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// TODO: Replace with signature.hexToByteArray when stable in kotlin
|
||||
val signatureBytes = HexFormat.of()
|
||||
.parseHex(signature)
|
||||
val factory = CertificateFactory.getInstance("X.509")
|
||||
factory.generateCertificate(ByteArrayInputStream(signatureBytes))
|
||||
return signature;
|
||||
} catch (_: IllegalArgumentException) {
|
||||
} catch (_: CertificateException) {
|
||||
}
|
||||
try {
|
||||
val signaturePath = Path(signature)
|
||||
if (!Files.readAttributes(signaturePath, BasicFileAttributes::class.java).isRegularFile) {
|
||||
return null;
|
||||
}
|
||||
val verifier = ApkVerifier.Builder(signaturePath.toFile())
|
||||
.build()
|
||||
|
||||
val result = verifier.verify()
|
||||
if (result.isVerifiedUsingV3Scheme) {
|
||||
return HexFormat.of().formatHex(result.v3SchemeSigners[0].certificate.encoded)
|
||||
} else if (result.isVerifiedUsingV2Scheme) {
|
||||
return HexFormat.of().formatHex(result.v2SchemeSigners[0].certificate.encoded)
|
||||
} else if (result.isVerifiedUsingV1Scheme) {
|
||||
return HexFormat.of().formatHex(result.v1SchemeSigners[0].certificate.encoded)
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (_: IOException) {
|
||||
} catch (_: InvalidPathException) {
|
||||
} catch (_: ApkFormatException) {
|
||||
} catch (_: NoSuchAlgorithmException) {
|
||||
} catch (_: IllegalArgumentException) {}
|
||||
return null;
|
||||
}
|
||||
@@ -3,24 +3,38 @@ package app.revanced.patches.cricbuzz.ads
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.cricbuzz.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.returnEarly
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/cricbuzz/ads/HideAdsPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val disableAdsPatch = bytecodePatch (
|
||||
name = "Hide ads",
|
||||
) {
|
||||
compatibleWith("com.cricbuzz.android"("6.23.02"))
|
||||
compatibleWith("com.cricbuzz.android"("6.24.01"))
|
||||
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
userStateSwitchFingerprint.method.apply {
|
||||
val opcodeIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_OBJECT)
|
||||
val register = getInstruction<OneRegisterInstruction>(opcodeIndex).registerA
|
||||
userStateSwitchFingerprint.method.returnEarly(true)
|
||||
|
||||
addInstruction(
|
||||
opcodeIndex + 1,
|
||||
"const-string v$register, \"ACTIVE\""
|
||||
// Remove region-specific Cricbuzz11 elements.
|
||||
cb11ConstructorFingerprint.method.addInstruction(0, "const/4 p7, 0x0")
|
||||
getBottomBarFingerprint.method.apply {
|
||||
val getIndex = indexOfFirstInstructionOrThrow() {
|
||||
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "bottomBar"
|
||||
}
|
||||
val getRegister = getInstruction<TwoRegisterInstruction>(getIndex).registerA
|
||||
|
||||
addInstruction(getIndex + 1,
|
||||
"invoke-static { v$getRegister }, $EXTENSION_CLASS_DESCRIPTOR->filterCb11(Ljava/util/List;)V"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,30 @@ import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal val userStateSwitchFingerprint = fingerprint {
|
||||
strings("key.user.state", "NA")
|
||||
opcodes(Opcode.SPARSE_SWITCH)
|
||||
strings("key.user.state", "NA")
|
||||
}
|
||||
|
||||
internal val cb11ConstructorFingerprint = fingerprint {
|
||||
parameters(
|
||||
"Ljava/lang/String;",
|
||||
"Ljava/lang/String;",
|
||||
"Ljava/lang/String;",
|
||||
"I",
|
||||
"Ljava/lang/String;",
|
||||
"Ljava/lang/String;",
|
||||
"Z",
|
||||
"Ljava/lang/String;",
|
||||
"Ljava/lang/String;",
|
||||
"L"
|
||||
)
|
||||
custom { _, classDef ->
|
||||
classDef.endsWith("CB11Details;")
|
||||
}
|
||||
}
|
||||
|
||||
internal val getBottomBarFingerprint = fingerprint {
|
||||
custom { method, classDef ->
|
||||
method.name == "getBottomBar" && classDef.endsWith("HomeMenu;")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package app.revanced.patches.cricbuzz.misc.extension
|
||||
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
|
||||
val sharedExtensionPatch = sharedExtensionPatch("cricbuzz", applicationInitHook)
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.patches.cricbuzz.misc.extension
|
||||
|
||||
import app.revanced.patches.shared.misc.extension.extensionHook
|
||||
|
||||
internal val applicationInitHook = extensionHook {
|
||||
custom { method, classDef ->
|
||||
method.name == "onCreate" && classDef.endsWith("/NyitoActivity;")
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
@Suppress("unused")
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide Ads"
|
||||
name = "Hide ads"
|
||||
) {
|
||||
compatibleWith("com.crunchyroll.crunchyroid")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
val disableMandatoryLoginPatch = bytecodePatch(
|
||||
name = "Disable mandatory login",
|
||||
) {
|
||||
compatibleWith("com.adobe.lrmobile"("10.0.2"))
|
||||
compatibleWith("com.adobe.lrmobile"("9.3.0"))
|
||||
|
||||
execute {
|
||||
isLoggedInFingerprint.method.apply {
|
||||
|
||||
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
val unlockPremiumPatch = bytecodePatch(
|
||||
name = "Unlock Premium",
|
||||
) {
|
||||
compatibleWith("com.adobe.lrmobile"("10.0.2"))
|
||||
compatibleWith("com.adobe.lrmobile"("9.3.0"))
|
||||
|
||||
execute {
|
||||
// Set hasPremium = true.
|
||||
|
||||
@@ -23,7 +23,6 @@ internal val createInboxSubTabsFingerprint = fingerprint {
|
||||
}
|
||||
|
||||
internal val loadInboxAdsFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("V")
|
||||
strings(
|
||||
"ads_load_begin",
|
||||
|
||||
@@ -8,12 +8,7 @@ val hideVideoAdsPatch = bytecodePatch(
|
||||
name = "Hide music video ads",
|
||||
description = "Hides ads that appear while listening to or streaming music videos, podcasts, or songs.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
navigate(showVideoAdsParentFingerprint.originalMethod)
|
||||
|
||||
@@ -8,12 +8,7 @@ val enableExclusiveAudioPlaybackPatch = bytecodePatch(
|
||||
name = "Enable exclusive audio playback",
|
||||
description = "Enables the option to play audio without video.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
allowExclusiveAudioPlaybackFingerprint.method.returnEarly(true)
|
||||
|
||||
@@ -11,12 +11,7 @@ val permanentRepeatPatch = bytecodePatch(
|
||||
description = "Permanently remember your repeating preference even if the playlist ends or another track is played.",
|
||||
use = false,
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
val startIndex = repeatTrackFingerprint.patternMatch!!.endIndex
|
||||
|
||||
@@ -7,18 +7,9 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
@Suppress("unused")
|
||||
val permanentShufflePatch = bytecodePatch(
|
||||
description = "Permanently remember your shuffle preference " +
|
||||
"even if the playlist ends or another track is played.",
|
||||
use = false,
|
||||
"even if the playlist ends or another track is played."
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"6.45.54",
|
||||
"6.51.53",
|
||||
"7.01.53",
|
||||
"7.02.52",
|
||||
"7.03.52",
|
||||
),
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
disableShuffleFingerprint.method.addInstruction(0, "return-void")
|
||||
|
||||
@@ -11,12 +11,7 @@ val hideCategoryBar = bytecodePatch(
|
||||
description = "Hides the category bar at the top of the homepage.",
|
||||
use = false,
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
constructCategoryBarFingerprint.method.apply {
|
||||
|
||||
@@ -11,12 +11,7 @@ val hideGetPremiumPatch = bytecodePatch(
|
||||
name = "Hide 'Get Music Premium' label",
|
||||
description = "Hides the \"Get Music Premium\" label from the account menu and settings.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
hideGetPremiumFingerprint.method.apply {
|
||||
|
||||
@@ -18,12 +18,7 @@ val removeUpgradeButtonPatch = bytecodePatch(
|
||||
name = "Remove upgrade button",
|
||||
description = "Removes the upgrade tab from the pivot bar.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
pivotBarConstructorFingerprint.method.apply {
|
||||
|
||||
@@ -8,12 +8,7 @@ val bypassCertificateChecksPatch = bytecodePatch(
|
||||
name = "Bypass certificate checks",
|
||||
description = "Bypasses certificate checks which prevent YouTube Music from working on Android Auto.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
checkCertificateFingerprint.method.returnEarly(true)
|
||||
|
||||
@@ -8,12 +8,7 @@ val backgroundPlaybackPatch = bytecodePatch(
|
||||
name = "Remove background playback restrictions",
|
||||
description = "Removes restrictions on background playback, including playing kids videos in the background.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
execute {
|
||||
kidsBackgroundPlaybackPolicyControllerFingerprint.method.addInstruction(
|
||||
|
||||
@@ -25,12 +25,7 @@ val spoofClientPatch = bytecodePatch(
|
||||
name = "Spoof client",
|
||||
description = "Spoofs the client to fix playback.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.apps.youtube.music"(
|
||||
"7.16.53",
|
||||
"8.05.51"
|
||||
)
|
||||
)
|
||||
compatibleWith("com.google.android.apps.youtube.music")
|
||||
|
||||
dependsOn(
|
||||
sharedExtensionPatch,
|
||||
|
||||
@@ -12,7 +12,7 @@ val skipAdsPatch = bytecodePatch(
|
||||
name = "Skip ads",
|
||||
description = "Automatically skips video stream ads.",
|
||||
) {
|
||||
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.403.257"))
|
||||
compatibleWith("com.amazon.avod.thirdpartyclient"("3.0.412.2947"))
|
||||
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.patches.primevideo.video.speed
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val playbackUserControlsInitializeFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC)
|
||||
parameters("Lcom/amazon/avod/playbackclient/PlaybackInitializationContext;")
|
||||
returns("V")
|
||||
custom { method, classDef ->
|
||||
method.name == "initialize" && classDef.type == "Lcom/amazon/avod/playbackclient/activity/feature/PlaybackUserControlsFeature;"
|
||||
}
|
||||
}
|
||||
|
||||
internal val playbackUserControlsPrepareForPlaybackFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC)
|
||||
parameters("Lcom/amazon/avod/playbackclient/PlaybackContext;")
|
||||
returns("V")
|
||||
custom { method, classDef ->
|
||||
method.name == "prepareForPlayback" &&
|
||||
classDef.type == "Lcom/amazon/avod/playbackclient/activity/feature/PlaybackUserControlsFeature;"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package app.revanced.patches.primevideo.video.speed
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.primevideo.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch;"
|
||||
|
||||
val playbackSpeedPatch = bytecodePatch(
|
||||
name = "Playback speed",
|
||||
description = "Adds playback speed controls to the video player.",
|
||||
) {
|
||||
dependsOn(
|
||||
sharedExtensionPatch,
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.amazon.avod.thirdpartyclient"("3.0.412.2947")
|
||||
)
|
||||
|
||||
execute {
|
||||
playbackUserControlsInitializeFingerprint.method.apply {
|
||||
val getIndex = indexOfFirstInstructionOrThrow {
|
||||
opcode == Opcode.IPUT_OBJECT &&
|
||||
getReference<FieldReference>()?.name == "mUserControls"
|
||||
}
|
||||
|
||||
val getRegister = getInstruction<OneRegisterInstruction>(getIndex).registerA
|
||||
|
||||
addInstructions(
|
||||
getIndex + 1,
|
||||
"""
|
||||
invoke-static { v$getRegister }, $EXTENSION_CLASS_DESCRIPTOR->initializeSpeedOverlay(Landroid/view/View;)V
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
playbackUserControlsPrepareForPlaybackFingerprint.method.apply {
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-virtual { p1 }, Lcom/amazon/avod/playbackclient/PlaybackContext;->getPlayer()Lcom/amazon/video/sdk/player/Player;
|
||||
move-result-object v0
|
||||
invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->setPlayer(Lcom/amazon/video/sdk/player/Player;)V
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,16 @@ import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.reddit.customclients.spoofClientPatch
|
||||
import app.revanced.patches.shared.misc.string.replaceStringPatch
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
val spoofClientPatch = spoofClientPatch(redirectUri = "http://baconreader.com/auth") { clientIdOption ->
|
||||
dependsOn(
|
||||
// Redirects from SSL to WWW domain are bugged causing auth problems.
|
||||
// Manually rewrite the URLs to fix this.
|
||||
replaceStringPatch("ssl.reddit.com", "www.reddit.com")
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.onelouder.baconreader",
|
||||
"com.onelouder.baconreader.premium",
|
||||
|
||||
@@ -61,7 +61,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "redditisfun://auth") { cl
|
||||
// region Patch miscellaneous.
|
||||
|
||||
// Reddit messed up and does not append a redirect uri to the authorization url to old.reddit.com/login.
|
||||
// Replace old.reddit.com with ssl.reddit.com to fix this.
|
||||
// Replace old.reddit.com with www.reddit.com to fix this.
|
||||
buildAuthorizationStringFingerprint.method.apply {
|
||||
val index = indexOfFirstInstructionOrThrow {
|
||||
getReference<StringReference>()?.contains("old.reddit.com") == true
|
||||
@@ -70,7 +70,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "redditisfun://auth") { cl
|
||||
val targetRegister = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
replaceInstruction(
|
||||
index,
|
||||
"const-string v$targetRegister, \"https://ssl.reddit.com/api/v1/authorize.compact\"",
|
||||
"const-string v$targetRegister, \"https://www.reddit.com/api/v1/authorize.compact\"",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.reddit.customclients.spoofClientPatch
|
||||
import app.revanced.patches.reddit.customclients.sync.detection.piracy.disablePiracyDetectionPatch
|
||||
import app.revanced.patches.shared.misc.string.replaceStringPatch
|
||||
import app.revanced.util.returnEarly
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
@@ -13,7 +14,12 @@ import java.util.Base64
|
||||
val spoofClientPatch = spoofClientPatch(
|
||||
redirectUri = "http://redditsync/auth",
|
||||
) { clientIdOption ->
|
||||
dependsOn(disablePiracyDetectionPatch)
|
||||
dependsOn(
|
||||
disablePiracyDetectionPatch,
|
||||
// Redirects from SSL to WWW domain are bugged causing auth problems.
|
||||
// Manually rewrite the URLs to fix this.
|
||||
replaceStringPatch("ssl.reddit.com", "www.reddit.com")
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.laurencedawson.reddit_sync",
|
||||
|
||||
@@ -87,8 +87,8 @@ fun sharedExtensionPatch(
|
||||
|
||||
class ExtensionHook internal constructor(
|
||||
internal val fingerprint: Fingerprint,
|
||||
private val insertIndexResolver: ((Method) -> Int),
|
||||
private val contextRegisterResolver: (Method) -> String,
|
||||
private val insertIndexResolver: BytecodePatchContext.(Method) -> Int,
|
||||
private val contextRegisterResolver: BytecodePatchContext.(Method) -> String,
|
||||
) {
|
||||
context(BytecodePatchContext)
|
||||
operator fun invoke(extensionClassDescriptor: String) {
|
||||
@@ -98,19 +98,19 @@ class ExtensionHook internal constructor(
|
||||
fingerprint.method.addInstruction(
|
||||
insertIndex,
|
||||
"invoke-static/range { $contextRegister .. $contextRegister }, " +
|
||||
"$extensionClassDescriptor->setContext(Landroid/content/Context;)V",
|
||||
"$extensionClassDescriptor->setContext(Landroid/content/Context;)V",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun extensionHook(
|
||||
insertIndexResolver: ((Method) -> Int) = { 0 },
|
||||
contextRegisterResolver: (Method) -> String = { "p0" },
|
||||
insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 },
|
||||
contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" },
|
||||
fingerprint: Fingerprint,
|
||||
) = ExtensionHook(fingerprint, insertIndexResolver, contextRegisterResolver)
|
||||
|
||||
fun extensionHook(
|
||||
insertIndexResolver: ((Method) -> Int) = { 0 },
|
||||
contextRegisterResolver: (Method) -> String = { "p0" },
|
||||
insertIndexResolver: BytecodePatchContext.(Method) -> Int = { 0 },
|
||||
contextRegisterResolver: BytecodePatchContext.(Method) -> String = { "p0" },
|
||||
fingerprintBuilderBlock: FingerprintBuilder.() -> Unit,
|
||||
) = extensionHook(insertIndexResolver, contextRegisterResolver, fingerprint(block = fingerprintBuilderBlock))
|
||||
|
||||
@@ -148,7 +148,7 @@ fun gmsCoreSupportPatch(
|
||||
|
||||
fun packageNameTransform(fromPackageName: String, toPackageName: String): (String) -> String? = { string ->
|
||||
when (string) {
|
||||
"$fromPackageName.SuggestionsProvider",
|
||||
"$fromPackageName.SuggestionProvider",
|
||||
"$fromPackageName.fileprovider",
|
||||
-> string.replace(fromPackageName, toPackageName)
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package app.revanced.patches.shared.misc.string
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
|
||||
import app.revanced.util.getReference
|
||||
import com.android.tools.smali.dexlib2.ReferenceType
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
import kotlin.text.contains
|
||||
|
||||
fun replaceStringPatch(
|
||||
from: String,
|
||||
to: String
|
||||
) = bytecodePatch(
|
||||
description = "Replaces occurrences of '$from' with '$to' in string references.",
|
||||
) {
|
||||
dependsOn(
|
||||
transformInstructionsPatch(
|
||||
filterMap = filterMap@{ _, _, instruction, instructionIndex ->
|
||||
if (instruction.opcode.referenceType != ReferenceType.STRING) return@filterMap null
|
||||
|
||||
val stringReference = instruction.getReference<StringReference>()!!.string
|
||||
if (from !in stringReference) return@filterMap null
|
||||
|
||||
Triple(instructionIndex, instruction as OneRegisterInstruction, stringReference)
|
||||
},
|
||||
transform = transform@{ mutableMethod, entry ->
|
||||
val (instructionIndex, instruction, stringReference) = entry
|
||||
|
||||
val newString = stringReference.replace(from, to)
|
||||
mutableMethod.replaceInstruction(
|
||||
instructionIndex,
|
||||
"${instruction.opcode.name} v${instruction.registerA}, \"$newString\"",
|
||||
)
|
||||
},
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import app.revanced.patches.soundcloud.shared.featureConstructorFingerprint
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide ads",
|
||||
) {
|
||||
compatibleWith("com.soundcloud.android")
|
||||
compatibleWith("com.soundcloud.android"("2025.05.27-release"))
|
||||
|
||||
execute {
|
||||
// Enable a preset feature to disable audio ads by modifying the JSON server response.
|
||||
|
||||
@@ -8,7 +8,7 @@ val disableTelemetryPatch = bytecodePatch(
|
||||
name = "Disable telemetry",
|
||||
description = "Disables SoundCloud's telemetry system.",
|
||||
) {
|
||||
compatibleWith("com.soundcloud.android")
|
||||
compatibleWith("com.soundcloud.android"("2025.05.27-release"))
|
||||
|
||||
execute {
|
||||
// Empty the "backend" argument to abort the initializer.
|
||||
|
||||
@@ -17,7 +17,7 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
val enableOfflineSync = bytecodePatch(
|
||||
name = "Enable offline sync",
|
||||
) {
|
||||
compatibleWith("com.soundcloud.android")
|
||||
compatibleWith("com.soundcloud.android"("2025.05.27-release"))
|
||||
|
||||
execute {
|
||||
// Enable the feature to allow offline track syncing by modifying the JSON server response.
|
||||
|
||||
@@ -6,12 +6,10 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
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
|
||||
import java.util.logging.Logger
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch;"
|
||||
@@ -26,13 +24,6 @@ val hideCreateButtonPatch = bytecodePatch(
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
Logger.getLogger(this::class.java.name).warning(
|
||||
"Create button does not exist in legacy app target. No changes applied."
|
||||
)
|
||||
return@execute
|
||||
}
|
||||
|
||||
val oldNavigationBarAddItemMethod = oldNavigationBarAddItemFingerprint.originalMethodOrNull
|
||||
// Only throw the fingerprint error when oldNavigationBarAddItemMethod does not exist.
|
||||
val navigationBarItemSetClassDef = if (oldNavigationBarAddItemMethod == null) {
|
||||
|
||||
@@ -7,8 +7,8 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.util.*
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import org.w3c.dom.Element
|
||||
@@ -19,12 +19,6 @@ private val customThemeBytecodePatch = bytecodePatch {
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
// Bytecode changes are not needed for legacy app target.
|
||||
// Player background color is changed with existing resource patch.
|
||||
return@execute
|
||||
}
|
||||
|
||||
val colorSpaceUtilsClassDef = colorSpaceUtilsClassFingerprint.originalClassDef
|
||||
|
||||
// Hook a util method that converts ARGB to RGBA in the sRGB color space to replace hardcoded accent colors.
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package app.revanced.patches.spotify.lite.ondemand
|
||||
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val onDemandFingerprint = fingerprint(fuzzyPatternScanThreshold = 2) {
|
||||
returns("L")
|
||||
parameters()
|
||||
opcodes(
|
||||
Opcode.INVOKE_STATIC,
|
||||
Opcode.MOVE_RESULT,
|
||||
Opcode.INVOKE_STATIC,
|
||||
Opcode.MOVE_RESULT_OBJECT,
|
||||
Opcode.IF_EQZ,
|
||||
Opcode.SGET_OBJECT,
|
||||
Opcode.GOTO,
|
||||
Opcode.SGET_OBJECT,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.MOVE_RESULT,
|
||||
Opcode.IPUT,
|
||||
Opcode.RETURN_OBJECT,
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,9 @@
|
||||
package app.revanced.patches.spotify.lite.ondemand
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
@Deprecated("Patch no longer works and will be deleted soon")
|
||||
@Suppress("unused")
|
||||
val onDemandPatch = bytecodePatch(
|
||||
description = "Enables listening to songs on-demand, allowing to play any song from playlists, albums or artists without limitations. This does not remove ads.",
|
||||
) {
|
||||
compatibleWith("com.spotify.lite")
|
||||
|
||||
execute {
|
||||
// Spoof a premium account
|
||||
|
||||
onDemandFingerprint.method.addInstruction(
|
||||
onDemandFingerprint.patternMatch!!.endIndex - 1,
|
||||
"const/4 v0, 0x2",
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@ package app.revanced.patches.spotify.misc
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
@@ -13,25 +12,13 @@ import com.android.tools.smali.dexlib2.iface.reference.TypeReference
|
||||
|
||||
context(BytecodePatchContext)
|
||||
internal val accountAttributeFingerprint get() = fingerprint {
|
||||
custom { _, classDef ->
|
||||
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
"Lcom/spotify/useraccount/v1/AccountAttribute;"
|
||||
} else {
|
||||
"Lcom/spotify/remoteconfig/internal/AccountAttribute;"
|
||||
}
|
||||
}
|
||||
custom { _, classDef -> classDef.type == "Lcom/spotify/remoteconfig/internal/AccountAttribute;" }
|
||||
}
|
||||
|
||||
context(BytecodePatchContext)
|
||||
internal val productStateProtoGetMapFingerprint get() = fingerprint {
|
||||
returns("Ljava/util/Map;")
|
||||
custom { _, classDef ->
|
||||
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
"Lcom/spotify/ucs/proto/v0/UcsResponseWrapper${'$'}AccountAttributesResponse;"
|
||||
} else {
|
||||
"Lcom/spotify/remoteconfig/internal/ProductStateProto;"
|
||||
}
|
||||
}
|
||||
custom { _, classDef -> classDef.type == "Lcom/spotify/remoteconfig/internal/ProductStateProto;" }
|
||||
}
|
||||
|
||||
internal val buildQueryParametersFingerprint = fingerprint {
|
||||
@@ -42,7 +29,10 @@ internal val contextMenuViewModelClassFingerprint = fingerprint {
|
||||
strings("ContextMenuViewModel(header=")
|
||||
}
|
||||
|
||||
internal val contextMenuViewModelAddItemFingerprint = fingerprint {
|
||||
/**
|
||||
* Used in versions older than "9.0.60.128".
|
||||
*/
|
||||
internal val oldContextMenuViewModelAddItemFingerprint = fingerprint {
|
||||
parameters("L")
|
||||
returns("V")
|
||||
custom { method, _ ->
|
||||
@@ -52,6 +42,27 @@ internal val contextMenuViewModelAddItemFingerprint = fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
internal val contextMenuViewModelConstructorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to find the interface name of a context menu item.
|
||||
*/
|
||||
internal val removeAdsContextMenuItemClassFingerprint = fingerprint {
|
||||
strings("remove_ads_item", "ui_navigate")
|
||||
}
|
||||
|
||||
internal const val CONTEXT_MENU_ITEM_CLASS_DESCRIPTOR_PLACEHOLDER = "Lapp/revanced/ContextMenuItemPlaceholder;"
|
||||
internal val extensionFilterContextMenuItemsFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("Ljava/util/List;")
|
||||
parameters("Ljava/util/List;")
|
||||
custom { method, classDef ->
|
||||
method.name == "filterContextMenuItems" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
|
||||
}
|
||||
}
|
||||
|
||||
internal val getViewModelFingerprint = fingerprint {
|
||||
custom { method, _ -> method.name == "getViewModel" }
|
||||
}
|
||||
@@ -66,14 +77,14 @@ internal val contextFromJsonFingerprint = fingerprint {
|
||||
)
|
||||
custom { method, classDef ->
|
||||
method.name == "fromJson" &&
|
||||
classDef.endsWith("voiceassistants/playermodels/ContextJsonAdapter;")
|
||||
classDef.type.endsWith("voiceassistants/playermodels/ContextJsonAdapter;")
|
||||
}
|
||||
}
|
||||
|
||||
internal val readPlayerOptionOverridesFingerprint = fingerprint {
|
||||
custom { method, classDef ->
|
||||
method.name == "readPlayerOptionOverrides" &&
|
||||
classDef.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;")
|
||||
classDef.type.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,23 +104,23 @@ internal val abstractProtobufListEnsureIsMutableFingerprint = fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
private fun structureGetSectionsFingerprint(className: String) = fingerprint {
|
||||
internal fun structureGetSectionsFingerprint(className: String) = fingerprint {
|
||||
custom { method, classDef ->
|
||||
classDef.endsWith(className) && method.indexOfFirstInstruction {
|
||||
classDef.type.endsWith(className) && method.indexOfFirstInstruction {
|
||||
opcode == Opcode.IGET_OBJECT && getReference<FieldReference>()?.name == "sections_"
|
||||
} >= 0
|
||||
}
|
||||
}
|
||||
|
||||
internal val homeSectionFingerprint = fingerprint {
|
||||
custom { _, classDef -> classDef.endsWith("homeapi/proto/Section;") }
|
||||
custom { _, classDef -> classDef.type.endsWith("homeapi/proto/Section;") }
|
||||
}
|
||||
|
||||
internal val homeStructureGetSectionsFingerprint =
|
||||
structureGetSectionsFingerprint("homeapi/proto/HomeStructure;")
|
||||
|
||||
internal val browseSectionFingerprint = fingerprint {
|
||||
custom { _, classDef -> classDef.endsWith("browsita/v1/resolved/Section;") }
|
||||
custom { _, classDef-> classDef.type.endsWith("browsita/v1/resolved/Section;") }
|
||||
}
|
||||
|
||||
internal val browseStructureGetSectionsFingerprint =
|
||||
|
||||
@@ -2,4 +2,8 @@ package app.revanced.patches.spotify.misc.extension
|
||||
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
|
||||
val sharedExtensionPatch = sharedExtensionPatch("spotify", mainActivityOnCreateHook)
|
||||
val sharedExtensionPatch = sharedExtensionPatch(
|
||||
"spotify",
|
||||
mainActivityOnCreateHook,
|
||||
loadOrbitLibraryHook
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user