Compare commits

..

91 Commits

Author SHA1 Message Date
semantic-release-bot
5d2c21540c chore: Release v5.29.1-dev.1 [skip ci]
## [5.29.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.0...v5.29.1-dev.1) (2025-06-26)

### Bug Fixes

* **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)) ([1a8aacd](1a8aacdff6))
2025-06-26 17:56:10 +00:00
oSumAtrIX
1a8aacdff6 fix(Spotify): Add Spoof client patch to fix various issues by using a web platform access token (#5173)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: brosssh <tiabroch@gmail.com>
Co-authored-by: Dawid Krajcarz <80264606+drobotk@users.noreply.github.com>
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-06-26 19:51:18 +02:00
semantic-release-bot
1804bd9bfc chore: Release v5.29.0 [skip ci]
# [5.29.0](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.29.0) (2025-06-26)

### Bug Fixes

* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([2b62fc2](2b62fc2224))
* **Google Photos:** Resolve startup crash for Android 5.0 devices ([7be3741](7be374100b))
* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([b656976](b65697603d))
* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([978c244](978c24458b))
* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([a678f17](a678f178e1))
* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([957bece](957bece3e9))
* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([ccd1691](ccd169121a))
* **YouTube:** Fix refactoring app startup exception ([0dbd058](0dbd058099))

### Features

* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([fb83e58](fb83e58f79))
* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([558bf8b](558bf8bca8))
* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([d338989](d338989cb4))
* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([29c86ac](29c86ac6a3))
* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([0e63f49](0e63f49e13))
* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([6b719df](6b719dfcd7))
* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([439ca37](439ca37e99))
2025-06-26 08:06:08 +00:00
LisoUseInAIKyrios
7eb4e62762 chore: Merge branch dev to main (#5227) 2025-06-26 12:02:33 +04:00
LisoUseInAIKyrios
b8e10b5c1f chore: Fix api dump 2025-06-26 11:51:54 +04:00
github-actions[bot]
a7c11b9b08 chore: Sync translations (#5258) 2025-06-26 11:50:50 +04:00
semantic-release-bot
443c0a74d5 chore: Release v5.29.0-dev.11 [skip ci]
# [5.29.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.10...v5.29.0-dev.11) (2025-06-26)

### Features

* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([558bf8b](558bf8bca8))
2025-06-26 07:13:07 +00:00
github-actions[bot]
84a0f7f7d7 chore: Sync translations (#5257) 2025-06-26 11:10:05 +04:00
Sourav Agrawal
558bf8bca8 feat(Cricbuzz): Add Hide ads patch (#4998) 2025-06-26 11:08:22 +04:00
semantic-release-bot
e22d4e6a4b chore: Release v5.29.0-dev.10 [skip ci]
# [5.29.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.9...v5.29.0-dev.10) (2025-06-25)

### Features

* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([29c86ac](29c86ac6a3))
2025-06-25 08:59:45 +00:00
LisoUseInAIKyrios
a07f946633 chore: Fix typo 2025-06-25 12:56:42 +04:00
LisoUseInAIKyrios
29c86ac6a3 feat(YouTube - Hide Shorts components): Add Hide Effects button (#5255) 2025-06-25 12:54:59 +04:00
semantic-release-bot
19cf5667d8 chore: Release v5.29.0-dev.9 [skip ci]
# [5.29.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.8...v5.29.0-dev.9) (2025-06-25)

### Features

* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([fb83e58](fb83e58f79))
2025-06-25 07:16:33 +00:00
Markus Probst
fb83e58f79 feat: Add Spoof app signature patch (#5158) 2025-06-25 11:13:32 +04:00
semantic-release-bot
9844081d04 chore: Release v5.29.0-dev.8 [skip ci]
# [5.29.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.7...v5.29.0-dev.8) (2025-06-25)

### Features

* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([439ca37](439ca37e99))
2025-06-25 07:06:01 +00:00
LisoUseInAIKyrios
439ca37e99 feat(YouTube): Support version 20.13.41 (#5253) 2025-06-25 11:03:25 +04:00
semantic-release-bot
113a3d9f19 chore: Release v5.29.0-dev.7 [skip ci]
# [5.29.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.6...v5.29.0-dev.7) (2025-06-24)

### Bug Fixes

* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([978c244](978c24458b))
* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([957bece](957bece3e9))
2025-06-24 18:50:18 +00:00
ILoveOpenSourceApplications
978c24458b fix(YouTube - Hide layout components): Fix "Hide video description attributes" (#5250) 2025-06-24 22:47:46 +04:00
ILoveOpenSourceApplications
957bece3e9 fix(YouTube - Hide Shorts components): Fix "Hide Use this template button" (#5249) 2025-06-24 22:47:05 +04:00
github-actions[bot]
d32c3ac51d chore: Sync translations (#5251) 2025-06-24 22:46:50 +04:00
LisoUseInAIKyrios
26102a70a2 chore: Fix compile warning 2025-06-24 22:43:25 +04:00
semantic-release-bot
2b44bf4c23 chore: Release v5.29.0-dev.6 [skip ci]
# [5.29.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.5...v5.29.0-dev.6) (2025-06-24)

### Features

* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([0e63f49](0e63f49e13))
2025-06-24 11:14:05 +00:00
ILoveOpenSourceApplications
0e63f49e13 feat(YouTube - Hide video action buttons): Add Hide Stop ads (#5245) 2025-06-24 15:10:23 +04:00
semantic-release-bot
674a5b8d29 chore: Release v5.29.0-dev.5 [skip ci]
# [5.29.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.4...v5.29.0-dev.5) (2025-06-23)

### Bug Fixes

* **Google Photos:** Resolve startup crash for Android 5.0 devices ([7be3741](7be374100b))
2025-06-23 12:28:49 +00:00
LisoUseInAIKyrios
7be374100b fix(Google Photos): Resolve startup crash for Android 5.0 devices 2025-06-23 16:24:42 +04:00
semantic-release-bot
e48c152b95 chore: Release v5.29.0-dev.4 [skip ci]
# [5.29.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.3...v5.29.0-dev.4) (2025-06-23)

### Bug Fixes

* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([a678f17](a678f178e1))
2025-06-23 09:33:13 +00:00
ILoveOpenSourceApplications
a678f178e1 fix(YouTube - Hide Shorts components): Fix "Hide Use this sound button" (#5233) 2025-06-23 13:29:53 +04:00
semantic-release-bot
2d8f5641f9 chore: Release v5.29.0-dev.3 [skip ci]
# [5.29.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.2...v5.29.0-dev.3) (2025-06-23)

### Bug Fixes

* **YouTube:** Fix refactoring app startup exception ([0dbd058](0dbd058099))
2025-06-23 09:18:27 +00:00
LisoUseInAIKyrios
0dbd058099 fix(YouTube): Fix refactoring app startup exception 2025-06-23 13:15:43 +04:00
semantic-release-bot
c1a8fd0766 chore: Release v5.29.0-dev.2 [skip ci]
# [5.29.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.1...v5.29.0-dev.2) (2025-06-23)

### Features

* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([d338989](d338989cb4))
2025-06-23 08:47:04 +00:00
hoodles
d338989cb4 feat(Crunchyroll): Add Hide ads patch (#5201) 2025-06-23 12:44:07 +04:00
github-actions[bot]
b94daacf01 chore: Sync translations (#5236) 2025-06-23 12:43:45 +04:00
semantic-release-bot
c764c4f197 chore: Release v5.29.0-dev.1 [skip ci]
# [5.29.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.2...v5.29.0-dev.1) (2025-06-23)

### Bug Fixes

* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([ccd1691](ccd169121a))

### Features

* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([6b719df](6b719dfcd7))
2025-06-23 08:24:13 +00:00
MarcaD
6b719dfcd7 feat(YouTube): Add an option to disable toasts when changing default playback speed or quality (#5230) 2025-06-23 12:20:37 +04:00
LisoUseInAIKyrios
ccd169121a fix(YouTube): Always use single threaded layout to resolve layout bugs in unpatched YouTube (#5226) 2025-06-23 12:19:07 +04:00
semantic-release-bot
dcfbd8bf93 chore: Release v5.28.1-dev.2 [skip ci]
## [5.28.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.1...v5.28.1-dev.2) (2025-06-23)

### Bug Fixes

* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([b656976](b65697603d))
2025-06-23 07:19:38 +00:00
ILoveOpenSourceApplications
b65697603d fix(YouTube - Hide ads): Hide new type of product ad in video description (#5225) 2025-06-23 11:17:08 +04:00
semantic-release-bot
25da5cca8b chore: Release v5.28.1-dev.1 [skip ci]
## [5.28.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.28.1-dev.1) (2025-06-22)

### Bug Fixes

* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([2b62fc2](2b62fc2224))
2025-06-22 11:23:52 +00:00
MarcaD
2b62fc2224 fix: Add scrollable content to modern style settings dialogs (#5211) 2025-06-22 15:21:00 +04:00
semantic-release-bot
a9e9456b6b chore: Release v5.28.0 [skip ci]
# [5.28.0](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0) (2025-06-20)

### Bug Fixes

* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([1cea6bf](1cea6bfdff))
* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([a8d2a1e](a8d2a1e028))
* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([90868ff](90868ff025))
* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([5540136](55401368b8))
* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([e138501](e138501657))
* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([e790cfb](e790cfbf59))

### Features

* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([7bbaca7](7bbaca77ad))
* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([a426e2a](a426e2af50))
2025-06-20 08:17:57 +00:00
LisoUseInAIKyrios
b01523e97d chore: Merge branch dev to main (#5160) 2025-06-20 12:14:29 +04:00
github-actions[bot]
b8afb4e821 chore: Sync translations (#5210) 2025-06-20 12:13:39 +04:00
github-actions[bot]
0d2198faed chore: Sync translations (#5207) 2025-06-20 01:33:20 +04:00
semantic-release-bot
5c7c407b82 chore: Release v5.28.0-dev.8 [skip ci]
# [5.28.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.7...v5.28.0-dev.8) (2025-06-19)

### Bug Fixes

* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([a8d2a1e](a8d2a1e028))
2025-06-19 06:10:06 +00:00
Dawid Krajcarz
a8d2a1e028 fix(Messenger - Remove Meta AI): Improve patch logic (#5153) 2025-06-19 10:06:46 +04:00
semantic-release-bot
d31624cae8 chore: Release v5.28.0-dev.7 [skip ci]
# [5.28.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.6...v5.28.0-dev.7) (2025-06-18)

### Bug Fixes

* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([e790cfb](e790cfbf59))
2025-06-18 10:38:43 +00:00
LisoUseInAIKyrios
e790cfbf59 fix(YouTube): Remove old app targets that are no longer supported by YouTube (#5192) 2025-06-18 12:35:43 +02:00
LisoUseInAIKyrios
a54d408d3e chore: Fix string typos, fix missing long/wide returnEarly/returnLate 2025-06-18 11:06:48 +02:00
semantic-release-bot
5d3769e921 chore: Release v5.28.0-dev.6 [skip ci]
# [5.28.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.5...v5.28.0-dev.6) (2025-06-17)

### Bug Fixes

* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([e138501](e138501657))
2025-06-17 21:07:00 +00:00
LisoUseInAIKyrios
e138501657 fix(Threads - Hide ads): Constrain patch to the last working app target (#5189) 2025-06-17 23:03:35 +02:00
semantic-release-bot
a9235d6b62 chore: Release v5.28.0-dev.5 [skip ci]
# [5.28.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.4...v5.28.0-dev.5) (2025-06-17)

### Bug Fixes

* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([90868ff](90868ff025))
2025-06-17 06:47:11 +00:00
hoodles
90868ff025 fix(Pandora - Disable ads): Support latest app target (#5185) 2025-06-17 08:44:19 +02:00
github-actions[bot]
345ec5c430 chore: Sync translations (#5188) 2025-06-17 08:43:55 +02:00
semantic-release-bot
c8b95d475c chore: Release v5.28.0-dev.4 [skip ci]
# [5.28.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.3...v5.28.0-dev.4) (2025-06-13)

### Features

* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([a426e2a](a426e2af50))
2025-06-13 07:33:27 +00:00
github-actions[bot]
0a93f44a5e chore: Sync translations (#5167) 2025-06-13 09:30:30 +02:00
MarcaD
a426e2af50 feat: Use modern style settings dialogs (#5109)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-06-13 09:29:13 +02:00
semantic-release-bot
9e30c34e74 chore: Release v5.28.0-dev.3 [skip ci]
# [5.28.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.2...v5.28.0-dev.3) (2025-06-11)

### Bug Fixes

* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([5540136](55401368b8))
2025-06-11 20:23:04 +00:00
Nuckyz
55401368b8 fix(Spotify): Fix Hide Create button and Sanitize sharing links for older but supported app targets (#5159) 2025-06-11 17:20:19 -03:00
LisoUseInAIKyrios
c0c56fef23 chore: Fix debug logging if context is not set 2025-06-11 19:57:37 +02:00
semantic-release-bot
69df47602f chore: Release v5.28.0-dev.2 [skip ci]
# [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11)

### Bug Fixes

* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([1cea6bf](1cea6bfdff))
2025-06-11 17:44:07 +00:00
LisoUseInAIKyrios
1cea6bfdff fix(Google Photos): Resolve startup crash if MicroG GmsCore does not already have granted permissions 2025-06-11 19:40:37 +02:00
semantic-release-bot
e2a9552f91 chore: Release v5.28.0-dev.1 [skip ci]
# [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11)

### Features

* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([7bbaca7](7bbaca77ad))
2025-06-11 08:28:44 +00:00
brosssh
7bbaca77ad feat(Spotify): Add Change lyrics provider patch (#4937) 2025-06-11 10:25:58 +02:00
semantic-release-bot
246f3efe55 chore: Release v5.27.0 [skip ci]
# [5.27.0](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.27.0) (2025-06-09)

### Bug Fixes

* **Bandcamp - Remove play limits:** Support latest app version ([#5124](https://github.com/ReVanced/revanced-patches/issues/5124)) ([c0448de](c0448dece4))
* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([7a432e5](7a432e5741))
* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([65fc6b4](65fc6b43f5))
* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([4b8499f](4b8499ff2c))
* **YouTube - Hide Shorts components:** Disable A/B player flags that prevents hiding buttons ([9d10ab6](9d10ab6c00))
* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([6aff8e8](6aff8e8ca4))

### Features

* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([6127f48](6127f48a9e))
* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([af827e2](af827e2f1a))
* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([ed0d807](ed0d807d70))
* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([adfac8a](adfac8a1f2))
* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([f8e31c8](f8e31c820a))
* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([6d5380d](6d5380d44d))
2025-06-09 18:37:59 +00:00
LisoUseInAIKyrios
0fca3e8fb1 chore: Merge branch dev to main (#5115) 2025-06-09 20:35:14 +02:00
semantic-release-bot
c09255eaed chore: Release v5.27.0-dev.9 [skip ci]
# [5.27.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.8...v5.27.0-dev.9) (2025-06-09)

### Features

* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([ed0d807](ed0d807d70))
2025-06-09 18:18:19 +00:00
github-actions[bot]
e78d6240ea chore: Sync translations (#5151) 2025-06-09 20:15:28 +02:00
Dawid Krajcarz
ed0d807d70 feat(Messenger): Add Hide Facebook button patch (#5057) 2025-06-09 20:13:05 +02:00
semantic-release-bot
1c39004350 chore: Release v5.27.0-dev.8 [skip ci]
# [5.27.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.7...v5.27.0-dev.8) (2025-06-09)

### Features

* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([6127f48](6127f48a9e))
2025-06-09 14:11:00 +00:00
ILoveOpenSourceApplications
6127f48a9e feat: Add Hide app icon patch (#4977) 2025-06-09 11:07:43 -03:00
semantic-release-bot
ad416f4aa7 chore: Release v5.27.0-dev.7 [skip ci]
# [5.27.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.6...v5.27.0-dev.7) (2025-06-08)

### Features

* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([adfac8a](adfac8a1f2))
2025-06-08 18:07:53 +00:00
Nuckyz
adfac8a1f2 feat(YouTube - Hide player overlay buttons): Add in app setting for "Hide player control buttons background" (#5147) 2025-06-08 15:04:58 -03:00
semantic-release-bot
498488d45b chore: Release v5.27.0-dev.6 [skip ci]
# [5.27.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.5...v5.27.0-dev.6) (2025-06-08)

### Features

* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([f8e31c8](f8e31c820a))
2025-06-08 12:06:28 +00:00
LisoUseInAIKyrios
f8e31c820a feat(YouTube - Hide Shorts components): Add hide 'New posts' button 2025-06-08 14:03:00 +02:00
semantic-release-bot
826a391591 chore: Release v5.27.0-dev.5 [skip ci]
# [5.27.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.4...v5.27.0-dev.5) (2025-06-08)

### Features

* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([af827e2](af827e2f1a))
2025-06-08 07:17:04 +00:00
Nuckyz
af827e2f1a feat(Google Photos): Add Enable DCIM folders backup control patch (#5138) 2025-06-08 09:13:53 +02:00
semantic-release-bot
97cd31509e chore: Release v5.27.0-dev.4 [skip ci]
# [5.27.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.3...v5.27.0-dev.4) (2025-06-06)

### Bug Fixes

* **Bandcamp - Remove play limits:** Support latest app version ([#5124](https://github.com/ReVanced/revanced-patches/issues/5124)) ([c0448de](c0448dece4))
2025-06-06 21:22:31 +00:00
hoodles
c0448dece4 fix(Bandcamp - Remove play limits): Support latest app version (#5124) 2025-06-06 23:20:00 +02:00
semantic-release-bot
f00a95c0d8 chore: Release v5.27.0-dev.3 [skip ci]
# [5.27.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.2...v5.27.0-dev.3) (2025-06-06)

### Bug Fixes

* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([7a432e5](7a432e5741))
2025-06-06 21:14:18 +00:00
Nuckyz
7a432e5741 fix(Spotify): Hide Create button patch failing in edge cases (#5131) 2025-06-06 18:11:32 -03:00
semantic-release-bot
966a78bd81 chore: Release v5.27.0-dev.2 [skip ci]
# [5.27.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.1...v5.27.0-dev.2) (2025-06-06)

### Bug Fixes

* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([6aff8e8](6aff8e8ca4))
2025-06-06 07:06:49 +00:00
LisoUseInAIKyrios
6aff8e8ca4 fix(YouTube - Video quality): Remove non-functional Shorts 144p default quality 2025-06-06 09:04:07 +02:00
github-actions[bot]
11aa463fa6 chore: Sync translations (#5125) 2025-06-06 09:03:38 +02:00
semantic-release-bot
bf1b639a2f chore: Release v5.27.0-dev.1 [skip ci]
# [5.27.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.3...v5.27.0-dev.1) (2025-06-05)

### Features

* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([6d5380d](6d5380d44d))
2025-06-05 20:08:58 +00:00
LisoUseInAIKyrios
6d5380d44d feat(YouTube - Theme): Add option for black and white splash screen animation (#5119) 2025-06-05 22:06:12 +02:00
github-actions[bot]
7e1547b5b9 chore: Sync translations (#5123) 2025-06-05 22:05:56 +02:00
semantic-release-bot
c790b45cc5 chore: Release v5.26.1-dev.3 [skip ci]
## [5.26.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.2...v5.26.1-dev.3) (2025-06-05)

### Bug Fixes

* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([65fc6b4](65fc6b43f5))
2025-06-05 19:57:52 +00:00
Nuckyz
65fc6b43f5 fix(Spotify): Prevent hiding all navigation bar buttons (#5122) 2025-06-05 16:55:33 -03:00
semantic-release-bot
2257dd90aa chore: Release v5.26.1-dev.2 [skip ci]
## [5.26.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.1...v5.26.1-dev.2) (2025-06-05)

### Bug Fixes

* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([4b8499f](4b8499ff2c))
2025-06-05 19:12:08 +00:00
LisoUseInAIKyrios
4b8499ff2c fix(YouTube - Hide layout components): Remove broken option 'Hide comments emoji picker' (#5121) 2025-06-05 21:09:25 +02:00
Nuckyz
bde3fda972 refactor(Spotify): Add extensions debug logging (#5110) 2025-06-05 16:04:02 -03:00
313 changed files with 7358 additions and 4700 deletions

View File

@@ -1,3 +1,305 @@
## [5.29.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.0...v5.29.1-dev.1) (2025-06-26)
### Bug Fixes
* **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))
# [5.29.0](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.29.0) (2025-06-26)
### Bug Fixes
* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([e6876d5](https://github.com/ReVanced/revanced-patches/commit/e6876d510d28f6a3a41ec1722a033b3e30a22c65))
* **Google Photos:** Resolve startup crash for Android 5.0 devices ([0294533](https://github.com/ReVanced/revanced-patches/commit/0294533c4d9a321aea086eedb4e46385ae9a026e))
* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([1e2efad](https://github.com/ReVanced/revanced-patches/commit/1e2efad7b2714c395ed6b0a77cbbf8a2265df520))
* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([2f22d45](https://github.com/ReVanced/revanced-patches/commit/2f22d45eb80745ac64fbea44c8055ebe7925a586))
* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([5d6ec9e](https://github.com/ReVanced/revanced-patches/commit/5d6ec9e94a6221a0f32762d5bede893e9e7457fc))
* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([b399ecb](https://github.com/ReVanced/revanced-patches/commit/b399ecbb6a222d82dd5e4b3417c9f7eff4324adb))
* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([1f539b1](https://github.com/ReVanced/revanced-patches/commit/1f539b1396526b2c767d77a804bd0308ee4a42ec))
* **YouTube:** Fix refactoring app startup exception ([1b00c90](https://github.com/ReVanced/revanced-patches/commit/1b00c907f4b90f4659afb4a54ba61ac2835b460d))
### Features
* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([78b25aa](https://github.com/ReVanced/revanced-patches/commit/78b25aa4e87ec3f9df1d57831b48a39029969416))
* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([83ccfa8](https://github.com/ReVanced/revanced-patches/commit/83ccfa8e1b5d5a44c55ef659484acf3cc08d3346))
* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([46b4398](https://github.com/ReVanced/revanced-patches/commit/46b4398fd6ca223391ed8f497a8347c2313421b7))
* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([240897a](https://github.com/ReVanced/revanced-patches/commit/240897a94008ce9a148c87bb41b978d553d5a6f5))
* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([274dcc6](https://github.com/ReVanced/revanced-patches/commit/274dcc676e009be63eb6970de33abacd34dc6560))
* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([c68cde3](https://github.com/ReVanced/revanced-patches/commit/c68cde3a896450874cc571be5c4723387db96032))
* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([d284c3d](https://github.com/ReVanced/revanced-patches/commit/d284c3dd3277430b6885e7c27ee02d062dcefc85))
# [5.29.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.10...v5.29.0-dev.11) (2025-06-26)
### Features
* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([83ccfa8](https://github.com/ReVanced/revanced-patches/commit/83ccfa8e1b5d5a44c55ef659484acf3cc08d3346))
# [5.29.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.9...v5.29.0-dev.10) (2025-06-25)
### Features
* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([240897a](https://github.com/ReVanced/revanced-patches/commit/240897a94008ce9a148c87bb41b978d553d5a6f5))
# [5.29.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.8...v5.29.0-dev.9) (2025-06-25)
### Features
* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([78b25aa](https://github.com/ReVanced/revanced-patches/commit/78b25aa4e87ec3f9df1d57831b48a39029969416))
# [5.29.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.7...v5.29.0-dev.8) (2025-06-25)
### Features
* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([d284c3d](https://github.com/ReVanced/revanced-patches/commit/d284c3dd3277430b6885e7c27ee02d062dcefc85))
# [5.29.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.6...v5.29.0-dev.7) (2025-06-24)
### Bug Fixes
* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([2f22d45](https://github.com/ReVanced/revanced-patches/commit/2f22d45eb80745ac64fbea44c8055ebe7925a586))
* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([b399ecb](https://github.com/ReVanced/revanced-patches/commit/b399ecbb6a222d82dd5e4b3417c9f7eff4324adb))
# [5.29.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.5...v5.29.0-dev.6) (2025-06-24)
### Features
* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([274dcc6](https://github.com/ReVanced/revanced-patches/commit/274dcc676e009be63eb6970de33abacd34dc6560))
# [5.29.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.4...v5.29.0-dev.5) (2025-06-23)
### Bug Fixes
* **Google Photos:** Resolve startup crash for Android 5.0 devices ([0294533](https://github.com/ReVanced/revanced-patches/commit/0294533c4d9a321aea086eedb4e46385ae9a026e))
# [5.29.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.3...v5.29.0-dev.4) (2025-06-23)
### Bug Fixes
* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([5d6ec9e](https://github.com/ReVanced/revanced-patches/commit/5d6ec9e94a6221a0f32762d5bede893e9e7457fc))
# [5.29.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.2...v5.29.0-dev.3) (2025-06-23)
### Bug Fixes
* **YouTube:** Fix refactoring app startup exception ([1b00c90](https://github.com/ReVanced/revanced-patches/commit/1b00c907f4b90f4659afb4a54ba61ac2835b460d))
# [5.29.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.1...v5.29.0-dev.2) (2025-06-23)
### Features
* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([46b4398](https://github.com/ReVanced/revanced-patches/commit/46b4398fd6ca223391ed8f497a8347c2313421b7))
# [5.29.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.2...v5.29.0-dev.1) (2025-06-23)
### Bug Fixes
* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([1f539b1](https://github.com/ReVanced/revanced-patches/commit/1f539b1396526b2c767d77a804bd0308ee4a42ec))
### Features
* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([c68cde3](https://github.com/ReVanced/revanced-patches/commit/c68cde3a896450874cc571be5c4723387db96032))
## [5.28.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.1...v5.28.1-dev.2) (2025-06-23)
### Bug Fixes
* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([1e2efad](https://github.com/ReVanced/revanced-patches/commit/1e2efad7b2714c395ed6b0a77cbbf8a2265df520))
## [5.28.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.28.1-dev.1) (2025-06-22)
### Bug Fixes
* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([e6876d5](https://github.com/ReVanced/revanced-patches/commit/e6876d510d28f6a3a41ec1722a033b3e30a22c65))
# [5.28.0](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0) (2025-06-20)
### Bug Fixes
* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59))
* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([4ad4887](https://github.com/ReVanced/revanced-patches/commit/4ad488744d87543c31e453dc7b6d8182b3a7f440))
* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([ca83047](https://github.com/ReVanced/revanced-patches/commit/ca83047f5c4acbb267d5b98db80ad111999086e0))
* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2))
* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([3558c44](https://github.com/ReVanced/revanced-patches/commit/3558c44a05c13f19fefdbbf14b364181a79f17c0))
* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([c9e54e1](https://github.com/ReVanced/revanced-patches/commit/c9e54e1d36243945ac1ec3108fe38edf0e15d772))
### Features
* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd))
* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([312b6dc](https://github.com/ReVanced/revanced-patches/commit/312b6dc04e01c2758cd304ca8606306027aa2f01))
# [5.28.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.7...v5.28.0-dev.8) (2025-06-19)
### Bug Fixes
* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([4ad4887](https://github.com/ReVanced/revanced-patches/commit/4ad488744d87543c31e453dc7b6d8182b3a7f440))
# [5.28.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.6...v5.28.0-dev.7) (2025-06-18)
### Bug Fixes
* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([c9e54e1](https://github.com/ReVanced/revanced-patches/commit/c9e54e1d36243945ac1ec3108fe38edf0e15d772))
# [5.28.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.5...v5.28.0-dev.6) (2025-06-17)
### Bug Fixes
* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([3558c44](https://github.com/ReVanced/revanced-patches/commit/3558c44a05c13f19fefdbbf14b364181a79f17c0))
# [5.28.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.4...v5.28.0-dev.5) (2025-06-17)
### Bug Fixes
* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([ca83047](https://github.com/ReVanced/revanced-patches/commit/ca83047f5c4acbb267d5b98db80ad111999086e0))
# [5.28.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.3...v5.28.0-dev.4) (2025-06-13)
### Features
* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([312b6dc](https://github.com/ReVanced/revanced-patches/commit/312b6dc04e01c2758cd304ca8606306027aa2f01))
# [5.28.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.2...v5.28.0-dev.3) (2025-06-11)
### Bug Fixes
* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2))
# [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11)
### Bug Fixes
* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59))
# [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11)
### Features
* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd))
# [5.27.0](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.27.0) (2025-06-09)
### Bug Fixes
* **Bandcamp - Remove play limits:** Support latest app version ([#5124](https://github.com/ReVanced/revanced-patches/issues/5124)) ([863e92b](https://github.com/ReVanced/revanced-patches/commit/863e92b20ad6682f10524e475ed18f879048ecae))
* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([0923600](https://github.com/ReVanced/revanced-patches/commit/0923600739a126329fc62100b500216860d7005e))
* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([8afbef0](https://github.com/ReVanced/revanced-patches/commit/8afbef01343c1e3e6e7e4a4cec6319aebfa4b11c))
* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([9a6a639](https://github.com/ReVanced/revanced-patches/commit/9a6a639c4905b00d6dffb0923c839c8e3ae54d0c))
* **YouTube - Hide Shorts components:** Disable A/B player flags that prevents hiding buttons ([bef0dac](https://github.com/ReVanced/revanced-patches/commit/bef0dacac54caf1ca9511d7bc19b19140ccb4eaf))
* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([3113cd6](https://github.com/ReVanced/revanced-patches/commit/3113cd6d092952c8657454452f34c0ae85358ec9))
### Features
* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([92311b8](https://github.com/ReVanced/revanced-patches/commit/92311b8e5675f3d4b80ed690d34b699fb847e3cd))
* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([328d232](https://github.com/ReVanced/revanced-patches/commit/328d232fe77406fa93a14768fc66e7b998506fba))
* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([9175b23](https://github.com/ReVanced/revanced-patches/commit/9175b23e8360d13c8c1c9c8602ca0b5931d13627))
* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([dd8afa2](https://github.com/ReVanced/revanced-patches/commit/dd8afa2b07b50be24d764c0f6ddc9e1bbdb91bf1))
* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([ac6b916](https://github.com/ReVanced/revanced-patches/commit/ac6b916c0c212167c4645e2110500dc811b3e54a))
* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([42db0c2](https://github.com/ReVanced/revanced-patches/commit/42db0c2e36fefccdbeaa072edcec48b1e05b6270))
# [5.27.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.8...v5.27.0-dev.9) (2025-06-09)
### Features
* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([9175b23](https://github.com/ReVanced/revanced-patches/commit/9175b23e8360d13c8c1c9c8602ca0b5931d13627))
# [5.27.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.7...v5.27.0-dev.8) (2025-06-09)
### Features
* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([92311b8](https://github.com/ReVanced/revanced-patches/commit/92311b8e5675f3d4b80ed690d34b699fb847e3cd))
# [5.27.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.6...v5.27.0-dev.7) (2025-06-08)
### Features
* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([dd8afa2](https://github.com/ReVanced/revanced-patches/commit/dd8afa2b07b50be24d764c0f6ddc9e1bbdb91bf1))
# [5.27.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.5...v5.27.0-dev.6) (2025-06-08)
### Features
* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([ac6b916](https://github.com/ReVanced/revanced-patches/commit/ac6b916c0c212167c4645e2110500dc811b3e54a))
# [5.27.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.4...v5.27.0-dev.5) (2025-06-08)
### Features
* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([328d232](https://github.com/ReVanced/revanced-patches/commit/328d232fe77406fa93a14768fc66e7b998506fba))
# [5.27.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.3...v5.27.0-dev.4) (2025-06-06)
### Bug Fixes
* **Bandcamp - Remove play limits:** Support latest app version ([#5124](https://github.com/ReVanced/revanced-patches/issues/5124)) ([863e92b](https://github.com/ReVanced/revanced-patches/commit/863e92b20ad6682f10524e475ed18f879048ecae))
# [5.27.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.2...v5.27.0-dev.3) (2025-06-06)
### Bug Fixes
* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([0923600](https://github.com/ReVanced/revanced-patches/commit/0923600739a126329fc62100b500216860d7005e))
# [5.27.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.1...v5.27.0-dev.2) (2025-06-06)
### Bug Fixes
* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([3113cd6](https://github.com/ReVanced/revanced-patches/commit/3113cd6d092952c8657454452f34c0ae85358ec9))
# [5.27.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.3...v5.27.0-dev.1) (2025-06-05)
### Features
* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([42db0c2](https://github.com/ReVanced/revanced-patches/commit/42db0c2e36fefccdbeaa072edcec48b1e05b6270))
## [5.26.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.2...v5.26.1-dev.3) (2025-06-05)
### Bug Fixes
* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([8afbef0](https://github.com/ReVanced/revanced-patches/commit/8afbef01343c1e3e6e7e4a4cec6319aebfa4b11c))
## [5.26.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.1...v5.26.1-dev.2) (2025-06-05)
### Bug Fixes
* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([9a6a639](https://github.com/ReVanced/revanced-patches/commit/9a6a639c4905b00d6dffb0923c839c8e3ae54d0c))
## [5.26.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.26.1-dev.1) (2025-06-05)

3
build.gradle.kts Normal file
View File

@@ -0,0 +1,3 @@
plugins {
alias(libs.plugins.android.library) apply false
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,14 +1,24 @@
package app.revanced.extension.messenger.metaai;
import java.util.*;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public class RemoveMetaAIPatch {
public static boolean overrideConfigBool(long id, boolean value) {
// It seems like all configs starting with 363219 are related to Meta AI.
// A list of specific ones that need disabling would probably be better,
// but these config numbers seem to change slightly with each update.
// These first 6 digits don't though.
if (Long.toString(id).startsWith("363219"))
return false;
private static final Set<Long> loggedIDs = Collections.synchronizedSet(new HashSet<>());
public static boolean overrideBooleanFlag(long id, boolean value) {
try {
if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) {
if (loggedIDs.add(id))
Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false");
return false;
}
} catch (Exception ex) {
Logger.printException(() -> "overrideBooleanFlag failure", ex);
}
return value;
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id("com.android.library")
alias(libs.plugins.android.library)
}
android {

View File

@@ -5,7 +5,7 @@ import static app.revanced.extension.shared.requests.Route.Method.GET;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.SearchManager;
import android.content.Context;
import android.content.DialogInterface;
@@ -15,9 +15,10 @@ import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Pair;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
@@ -78,13 +79,27 @@ public class GmsCoreSupport {
// Use a delay to allow the activity to finish initializing.
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("gms_core_dialog_title"), // Title.
str(dialogMessageRef), // Message.
null, // No EditText.
str(positiveButtonTextRef), // OK button text.
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
null, // No Cancel button action.
null, // No Neutral button text.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
Dialog dialog = dialogPair.first;
// Do not set cancelable to false, to allow using back button to skip the action,
// just in case the battery change can never be satisfied.
var dialog = new AlertDialog.Builder(context)
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageRef))
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
.create();
dialog.setCancelable(true);
// Show the dialog
Utils.showDialog(context, dialog);
}, 100);
}
@@ -92,7 +107,6 @@ public class GmsCoreSupport {
/**
* Injection point.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public static void checkGmsCore(Activity context) {
try {
// Verify the user has not included GmsCore for a root installation.
@@ -140,7 +154,9 @@ public class GmsCoreSupport {
}
// Check if GmsCore is currently running in the background.
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER);
//noinspection TryFinallyCanBeTryWithResources
try {
if (client == null) {
Logger.printInfo(() -> "GmsCore is not running in the background");
checkIfDontKillMyAppSupportsManufacturer();
@@ -150,6 +166,8 @@ public class GmsCoreSupport {
"gms_core_dialog_open_website_text",
(dialog, id) -> openDontKillMyApp());
}
} finally {
if (client != null) client.close();
}
} catch (Exception ex) {
Logger.printException(() -> "checkGmsCore failure", ex);
@@ -209,6 +227,11 @@ public class GmsCoreSupport {
* @return If GmsCore is not whitelisted from battery optimizations.
*/
private static boolean batteryOptimizationsEnabled(Context context) {
//noinspection ObsoleteSdkInt
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Android 5.0 does not have battery optimization settings.
return false;
}
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
}

View File

@@ -19,7 +19,8 @@ import app.revanced.extension.shared.settings.preference.LogBufferManager;
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
* and additionally accessible thru {@link LogBufferManager}.
*
* All methods are thread safe.
* All methods are thread safe, and are safe to call even
* if {@link Utils#getContext()} is not available.
*/
public class Logger {
@@ -138,6 +139,20 @@ public class Logger {
}
}
private static boolean shouldLogDebug() {
// If the app is still starting up and the context is not yet set,
// then allow debug logging regardless what the debug setting actually is.
return Utils.context == null || DEBUG.get();
}
private static boolean shouldShowErrorToast() {
return Utils.context != null && DEBUG_TOAST_ON_ERROR.get();
}
private static boolean includeStackTrace() {
return Utils.context != null && DEBUG_STACKTRACE.get();
}
/**
* Logs debug messages under the outer class name of the code calling this method.
* <p>
@@ -157,8 +172,8 @@ public class Logger {
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) {
logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false);
if (shouldLogDebug()) {
logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
}
}
@@ -173,7 +188,7 @@ public class Logger {
* Logs information messages using the outer class name of the code calling this method.
*/
public static void printInfo(LogMessage message, @Nullable Exception ex) {
logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false);
logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
}
/**
@@ -194,22 +209,6 @@ public class Logger {
* @param ex exception (optional)
*/
public static void printException(LogMessage message, @Nullable Throwable ex) {
logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get());
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationInfo(LogMessage message) {
logInternal(LogLevel.INFO, message, null, false, false);
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationException(LogMessage message, @Nullable Exception ex) {
logInternal(LogLevel.ERROR, message, ex, false, false);
logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast());
}
}

View File

@@ -3,15 +3,21 @@ package app.revanced.extension.shared.checks;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.app.Dialog;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.text.Html;
import android.util.Pair;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
@@ -86,38 +92,58 @@ abstract class Check {
);
Utils.runOnMainThreadDelayed(() -> {
AlertDialog alert = new AlertDialog.Builder(activity)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("revanced_check_environment_failed_title"))
.setMessage(message)
.setPositiveButton(
" ",
(dialog, which) -> {
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
activity,
str("revanced_check_environment_failed_title"), // Title.
message, // Message.
null, // No EditText.
str("revanced_check_environment_dialog_open_official_source_button"), // OK button text.
() -> {
// Action for the OK (website) button.
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
// Shutdown to prevent the user from navigating back to this app,
// which is no longer showing a warning dialog.
activity.finishAffinity();
System.exit(0);
}
).setNegativeButton(
" ",
(dialog, which) -> {
// Cleanup data if the user incorrectly imported a huge negative number.
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
// Shutdown to prevent the user from navigating back to this app,
// which is no longer showing a warning dialog.
activity.finishAffinity();
System.exit(0);
},
null, // No cancel button.
str("revanced_check_environment_dialog_ignore_button"), // Neutral button text.
() -> {
// Neutral button action.
// Cleanup data if the user incorrectly imported a huge negative number.
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
},
true // Dismiss dialog when onNeutralClick.
);
dialog.dismiss();
}
).create();
// Get the dialog and main layout.
Dialog dialog = dialogPair.first;
LinearLayout mainLayout = dialogPair.second;
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
// Add icon to the dialog.
ImageView iconView = new ImageView(activity);
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
iconView.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
iconParams.gravity = Gravity.CENTER;
mainLayout.addView(iconView, 0); // Add icon at the top.
dialog.setCancelable(false);
// Show the dialog.
Utils.showDialog(activity, dialog, false, new DialogFragmentOnStartAction() {
boolean hasRun;
@Override
public void onStart(AlertDialog dialog) {
public void onStart(Dialog dialog) {
// Only run this once, otherwise if the user changes to a different app
// then changes back, this handler will run again and disable the buttons.
if (hasRun) {
@@ -125,19 +151,43 @@ abstract class Check {
}
hasRun = true;
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
// Get the button container to access buttons.
LinearLayout buttonContainer = (LinearLayout) mainLayout.getChildAt(mainLayout.getChildCount() - 1);
Button openWebsiteButton;
Button ignoreButton;
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
// Neutral button is the first child (index 0).
ignoreButton = (Button) rowContainer.getChildAt(0);
// OK button is the last child.
openWebsiteButton = (Button) rowContainer.getChildAt(rowContainer.getChildCount() - 1);
} else {
// Multi-row layout: buttons are in separate containers, ordered OK, Cancel, Neutral.
LinearLayout okContainer =
(LinearLayout) buttonContainer.getChildAt(0); // OK is first.
openWebsiteButton = (Button) okContainer.getChildAt(0);
LinearLayout neutralContainer =
(LinearLayout)buttonContainer.getChildAt(buttonContainer.getChildCount() - 1); // Neutral is last.
ignoreButton = (Button) neutralContainer.getChildAt(0);
}
// Initially set buttons to INVISIBLE and disabled.
openWebsiteButton.setVisibility(View.INVISIBLE);
openWebsiteButton.setEnabled(false);
ignoreButton.setVisibility(View.INVISIBLE);
ignoreButton.setEnabled(false);
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
dismissButton.setEnabled(false);
getCountdownRunnable(dismissButton, openWebsiteButton).run();
// Start the countdown for showing and enabling buttons.
getCountdownRunnable(ignoreButton, openWebsiteButton).run();
}
});
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
}
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) {
private static Runnable getCountdownRunnable(Button ignoreButton, Button openWebsiteButton) {
return new Runnable() {
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
@@ -146,17 +196,15 @@ abstract class Check {
Utils.verifyOnMainThread();
if (secondsRemaining > 0) {
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) {
openWebsiteButton.setVisibility(View.VISIBLE);
openWebsiteButton.setEnabled(true);
}
secondsRemaining--;
Utils.runOnMainThreadDelayed(this, 1000);
} else {
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
dismissButton.setEnabled(true);
ignoreButton.setVisibility(View.VISIBLE);
ignoreButton.setEnabled(true);
}
}
};

View File

@@ -52,7 +52,7 @@ public class Route {
private int countMatches(CharSequence seq, char c) {
int count = 0;
for (int i = 0; i < seq.length(); i++) {
for (int i = 0, length = seq.length(); i < length; i++) {
if (seq.charAt(i) == c)
count++;
}

View File

@@ -3,12 +3,20 @@ package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.*;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.util.Pair;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -44,7 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* Set by subclasses if Strings cannot be added as a resource.
*/
@Nullable
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle;
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle, restartDialogMessage;
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
@@ -76,7 +84,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
updatingPreference = true;
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
// Updating here can can cause a recursive call back into this same method.
// Updating here can cause a recursive call back into this same method.
updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different.
updateUIAvailability();
@@ -116,11 +124,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
showingUserDialogMessage = true;
new AlertDialog.Builder(context)
.setTitle(confirmDialogTitle)
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
// User confirmed, save to the Setting.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
confirmDialogTitle, // Title.
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
null, // No EditText.
null, // OK button text.
() -> {
// OK button action. User confirmed, save to the Setting.
updatePreference(pref, setting, true, false);
// Update availability of other preferences that may be changed.
@@ -129,23 +140,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
// Restore whatever the setting was before the change.
},
() -> {
// Cancel button action. Restore whatever the setting was before the change.
updatePreference(pref, setting, true, true);
})
.setOnDismissListener(dialog -> {
showingUserDialogMessage = false;
})
.setCancelable(false)
.show();
},
null, // No Neutral button.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false);
// Show the dialog.
dialogPair.first.show();
}
/**
* Updates all Preferences values and their availability using the current values in {@link Setting}.
*/
protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true,true);
updatePreferenceScreen(getPreferenceScreen(), true, true);
}
/**
@@ -280,17 +295,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
if (restartDialogTitle == null) {
restartDialogTitle = str("revanced_settings_restart_title");
}
if (restartDialogMessage == null) {
restartDialogMessage = str("revanced_settings_restart_dialog_message");
}
if (restartDialogButtonText == null) {
restartDialogButtonText = str("revanced_settings_restart");
}
new AlertDialog.Builder(context)
.setMessage(restartDialogTitle)
.setPositiveButton(restartDialogButtonText, (dialog, id)
-> Utils.restartApp(context))
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.show();
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(context,
restartDialogTitle, // Title.
restartDialogMessage, // Message.
null, // No EditText.
restartDialogButtonText, // OK button text.
() -> Utils.restartApp(context), // OK button action.
() -> {}, // Cancel button action (dismiss only).
null, // No Neutral button text.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
}
@SuppressLint("ResourceType")

View File

@@ -2,8 +2,9 @@ package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
@@ -18,14 +19,12 @@ import android.text.TextWatcher;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.*;
import androidx.annotation.ColorInt;
@@ -182,7 +181,7 @@ public class ColorPickerPreference extends EditTextPreference {
* @throws IllegalArgumentException If the color string is invalid.
*/
@Override
public final void setText(String colorString) {
public final void setText(String colorString) {
try {
Logger.printDebug(() -> "setText: " + colorString);
super.setText(colorString);
@@ -216,86 +215,6 @@ public class ColorPickerPreference extends EditTextPreference {
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
}
/**
* Creates a layout with a color preview and EditText for hex color input.
*
* @param context The context for creating the layout.
* @return A LinearLayout containing the color preview and EditText.
*/
private LinearLayout createDialogLayout(Context context) {
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(70, 0, 70, 0);
// Inflate color picker.
View colorPicker = LayoutInflater.from(context).inflate(
getResourceIdentifier("revanced_color_picker", "layout"), null);
dialogColorPickerView = colorPicker.findViewById(
getResourceIdentifier("color_picker_view", "id"));
dialogColorPickerView.setColor(currentColor);
layout.addView(colorPicker);
// Horizontal layout for preview and EditText.
LinearLayout inputLayout = new LinearLayout(context);
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
inputLayout.setPadding(0, 20, 0, 0);
dialogColorPreview = new TextView(context);
inputLayout.addView(dialogColorPreview);
updateColorPreview();
EditText editText = getEditText();
ViewParent parent = editText.getParent();
if (parent instanceof ViewGroup parentViewGroup) {
parentViewGroup.removeView(editText);
}
editText.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
String currentColorString = getColorString(currentColor);
editText.setText(currentColorString);
editText.setSelection(currentColorString.length());
editText.setTypeface(Typeface.MONOSPACE);
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
editText.addTextChangedListener(colorTextWatcher);
inputLayout.addView(editText);
// Add a dummy view to take up remaining horizontal space,
// otherwise it will show an oversize underlined text view.
View paddingView = new View(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.MATCH_PARENT,
1f
);
paddingView.setLayoutParams(params);
inputLayout.addView(paddingView);
layout.addView(inputLayout);
// Set up color picker listener with debouncing.
// Add listener last to prevent callbacks from set calls above.
dialogColorPickerView.setOnColorChangedListener(color -> {
// Check if it actually changed, since this callback
// can be caused by updates in afterTextChanged().
if (currentColor == color) {
return;
}
String updatedColorString = getColorString(color);
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
currentColor = color;
editText.setText(updatedColorString);
editText.setSelection(updatedColorString.length());
updateColorPreview();
updateWidgetColorDot();
});
return layout;
}
/**
* Updates the color preview TextView with a colored dot.
*/
@@ -360,65 +279,153 @@ public class ColorPickerPreference extends EditTextPreference {
}
/**
* Prepares the dialog builder with a custom view and reset button.
*
* @param builder The AlertDialog.Builder to configure.
* Creates a Dialog with a color preview and EditText for hex color input.
*/
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder);
LinearLayout dialogLayout = createDialogLayout(builder.getContext());
builder.setView(dialogLayout);
final int originalColor = currentColor;
builder.setNeutralButton(str("revanced_settings_reset_color"), null);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
try {
String colorString = getEditText().getText().toString();
if (colorString.length() != COLOR_STRING_LENGTH) {
Utils.showToastShort(str("revanced_settings_color_invalid"));
setText(getColorString(originalColor));
return;
}
setText(colorString);
} catch (Exception ex) {
// Should never happen due to a bad color string,
// since the text is validated and fixed while the user types.
Logger.printException(() -> "setPositiveButton failure", ex);
}
});
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
try {
// Restore the original color.
setText(getColorString(originalColor));
} catch (Exception ex) {
Logger.printException(() -> "setNegativeButton failure", ex);
}
});
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
Context context = getContext();
AlertDialog dialog = (AlertDialog) getDialog();
dialog.setCanceledOnTouchOutside(false);
// Inflate color picker view.
View colorPicker = LayoutInflater.from(context).inflate(
getResourceIdentifier("revanced_color_picker", "layout"), null);
dialogColorPickerView = colorPicker.findViewById(
getResourceIdentifier("revanced_color_picker_view", "id"));
dialogColorPickerView.setColor(currentColor);
// Do not close dialog when reset is pressed.
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
button.setOnClickListener(view -> {
try {
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
// Setting view color causes listener callback into this class.
dialogColorPickerView.setColor(defaultColor);
} catch (Exception ex) {
Logger.printException(() -> "setOnClickListener failure", ex);
// Horizontal layout for preview and EditText.
LinearLayout inputLayout = new LinearLayout(context);
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
dialogColorPreview = new TextView(context);
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
previewParams.setMargins(dipToPixels(15), 0, dipToPixels(10), 0); // text dot has its own indents so 15, instead 16.
dialogColorPreview.setLayoutParams(previewParams);
inputLayout.addView(dialogColorPreview);
updateColorPreview();
EditText editText = getEditText();
ViewParent parent = editText.getParent();
if (parent instanceof ViewGroup parentViewGroup) {
parentViewGroup.removeView(editText);
}
editText.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
String currentColorString = getColorString(currentColor);
editText.setText(currentColorString);
editText.setSelection(currentColorString.length());
editText.setTypeface(Typeface.MONOSPACE);
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
editText.addTextChangedListener(colorTextWatcher);
inputLayout.addView(editText);
// Add a dummy view to take up remaining horizontal space,
// otherwise it will show an oversize underlined text view.
View paddingView = new View(context);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.MATCH_PARENT,
1f
);
paddingView.setLayoutParams(params);
inputLayout.addView(paddingView);
// Create content container for color picker and input layout.
LinearLayout contentContainer = new LinearLayout(context);
contentContainer.setOrientation(LinearLayout.VERTICAL);
contentContainer.addView(colorPicker);
contentContainer.addView(inputLayout);
// Create ScrollView to wrap the content container.
ScrollView contentScrollView = new ScrollView(context);
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
1.0f
);
contentScrollView.setLayoutParams(scrollViewParams);
contentScrollView.addView(contentContainer);
// Create custom dialog.
final int originalColor = currentColor & 0x00FFFFFF;
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"), // Title.
null, // No message.
null, // No EditText.
null, // OK button text.
() -> {
// OK button action.
try {
String colorString = editText.getText().toString();
if (colorString.length() != COLOR_STRING_LENGTH) {
Utils.showToastShort(str("revanced_settings_color_invalid"));
setText(getColorString(originalColor));
return;
}
setText(colorString);
} catch (Exception ex) {
// Should never happen due to a bad color string,
// since the text is validated and fixed while the user types.
Logger.printException(() -> "OK button failure", ex);
}
},
() -> {
// Cancel button action.
try {
// Restore the original color.
setText(getColorString(originalColor));
} catch (Exception ex) {
Logger.printException(() -> "Cancel button failure", ex);
}
},
str("revanced_settings_reset_color"), // Neutral button text.
() -> {
// Neutral button action.
try {
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
// Setting view color causes listener callback into this class.
dialogColorPickerView.setColor(defaultColor);
} catch (Exception ex) {
Logger.printException(() -> "Reset button failure", ex);
}
},
false // Do not dismiss dialog when onNeutralClick.
);
// Add the ScrollView to the dialog's main layout.
LinearLayout dialogMainLayout = dialogPair.second;
dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
// Set up color picker listener with debouncing.
// Add listener last to prevent callbacks from set calls above.
dialogColorPickerView.setOnColorChangedListener(color -> {
// Check if it actually changed, since this callback
// can be caused by updates in afterTextChanged().
if (currentColor == color) {
return;
}
String updatedColorString = getColorString(color);
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
currentColor = color;
editText.setText(updatedColorString);
editText.setSelection(updatedColorString.length());
updateColorPreview();
updateWidgetColorDot();
});
// Configure and show the dialog.
Dialog dialog = dialogPair.first;
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
@Override

View File

@@ -29,8 +29,8 @@ import app.revanced.extension.shared.Utils;
* <p>
* This view displays two main components for color selection:
* <ul>
* <li><b>Hue Bar:</b> A vertical bar on the right that allows the user to select the hue component of the color.
* <li><b>Saturation-Value Selector:</b> A rectangular area that allows the user to select the saturation and value (brightness)
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the saturation and value (brightness)
* components of the color based on the selected hue.
* </ul>
*
@@ -63,12 +63,12 @@ public class ColorPickerView extends View {
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
private static final float VIEW_PADDING = dipToPixels(16);
private static final float HUE_BAR_WIDTH = dipToPixels(12);
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
private static final float SELECTOR_RADIUS = dipToPixels(12);
private static final float SELECTOR_STROKE_WIDTH = 8;
/**
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
*/
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
@@ -144,17 +144,17 @@ public class ColorPickerView extends View {
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
final int minWidth = Utils.dipToPixels(250);
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO);
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
int width = resolveSize(minWidth, widthMeasureSpec);
int height = resolveSize(minHeight, heightMeasureSpec);
// Ensure minimum dimensions for usability
// Ensure minimum dimensions for usability.
width = Math.max(width, minWidth);
height = Math.max(height, minHeight);
// Adjust height to maintain desired aspect ratio if possible
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO);
// Adjust height to maintain desired aspect ratio if possible.
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
height = desiredHeight;
}
@@ -171,22 +171,22 @@ public class ColorPickerView extends View {
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
// Calculate bounds with hue bar on the right
// Calculate bounds with hue bar at the bottom.
final float effectiveWidth = width - (2 * VIEW_PADDING);
final float selectorWidth = effectiveWidth - HUE_BAR_WIDTH - MARGIN_BETWEEN_AREAS;
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
// Adjust rectangles to account for padding and density-independent dimensions
// Adjust rectangles to account for padding and density-independent dimensions.
saturationValueRect.set(
VIEW_PADDING,
VIEW_PADDING,
VIEW_PADDING + selectorWidth,
height - VIEW_PADDING
VIEW_PADDING + effectiveWidth,
VIEW_PADDING + effectiveHeight
);
hueRect.set(
width - VIEW_PADDING - HUE_BAR_WIDTH,
VIEW_PADDING,
width - VIEW_PADDING,
height - VIEW_PADDING - HUE_BAR_HEIGHT,
VIEW_PADDING + effectiveWidth,
height - VIEW_PADDING
);
@@ -201,7 +201,7 @@ public class ColorPickerView extends View {
private void updateHueShader() {
LinearGradient hueShader = new LinearGradient(
hueRect.left, hueRect.top,
hueRect.left, hueRect.bottom,
hueRect.right, hueRect.top,
HUE_COLORS,
null,
Shader.TileMode.CLAMP
@@ -263,8 +263,8 @@ public class ColorPickerView extends View {
// Draw the hue bar.
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
final float hueSelectorX = hueRect.centerX();
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
final float hueSelectorY = hueRect.centerY();
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
@@ -316,17 +316,17 @@ public class ColorPickerView extends View {
// Define touch expansion for the hue bar.
RectF expandedHueRect = new RectF(
hueRect.left - TOUCH_EXPANSION,
hueRect.top,
hueRect.right + TOUCH_EXPANSION,
hueRect.bottom
hueRect.left,
hueRect.top - TOUCH_EXPANSION,
hueRect.right,
hueRect.bottom + TOUCH_EXPANSION
);
switch (action) {
case MotionEvent.ACTION_DOWN:
// Calculate current handle positions.
final float hueSelectorX = hueRect.centerX();
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
final float hueSelectorY = hueRect.centerY();
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
@@ -348,14 +348,14 @@ public class ColorPickerView extends View {
// Check if the touch started on a handle or within the expanded hue bar area.
if (hueHitRect.contains(x, y)) {
isDraggingHue = true;
updateHueFromTouch(y);
updateHueFromTouch(x);
} else if (satValHitRect.contains(x, y)) {
isDraggingSaturation = true;
updateSaturationValueFromTouch(x, y);
} else if (expandedHueRect.contains(x, y)) {
// Handle touch within the expanded hue bar area.
isDraggingHue = true;
updateHueFromTouch(y);
updateHueFromTouch(x);
} else if (saturationValueRect.contains(x, y)) {
isDraggingSaturation = true;
updateSaturationValueFromTouch(x, y);
@@ -365,7 +365,7 @@ public class ColorPickerView extends View {
case MotionEvent.ACTION_MOVE:
// Continue updating values even if touch moves outside the view.
if (isDraggingHue) {
updateHueFromTouch(y);
updateHueFromTouch(x);
} else if (isDraggingSaturation) {
updateSaturationValueFromTouch(x, y);
}
@@ -387,12 +387,12 @@ public class ColorPickerView extends View {
/**
* Updates the hue value based on touch position, clamping to valid range.
*
* @param y The y-coordinate of the touch position.
* @param x The x-coordinate of the touch position.
*/
private void updateHueFromTouch(float y) {
// Clamp y to the hue rectangle bounds.
final float clampedY = Utils.clamp(y, hueRect.top, hueRect.bottom);
final float updatedHue = ((clampedY - hueRect.top) / hueRect.height()) * 360f;
private void updateHueFromTouch(float x) {
// Clamp x to the hue rectangle bounds.
final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right);
final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f;
if (hue == updatedHue) {
return;
}

View File

@@ -0,0 +1,173 @@
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;
import android.preference.ListPreference;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Utils;
/**
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
*/
@SuppressWarnings({"unused", "deprecation"})
public class CustomDialogListPreference extends ListPreference {
/**
* Custom ArrayAdapter to handle checkmark visibility.
*/
private static class ListPreferenceArrayAdapter extends ArrayAdapter<CharSequence> {
private static class SubViewDataContainer {
ImageView checkIcon;
View placeholder;
TextView itemText;
}
final int layoutResourceId;
final CharSequence[] entryValues;
String selectedValue;
public ListPreferenceArrayAdapter(Context context, int resource, CharSequence[] entries,
CharSequence[] entryValues, String selectedValue) {
super(context, resource, entries);
this.layoutResourceId = resource;
this.entryValues = entryValues;
this.selectedValue = selectedValue;
}
@NonNull
@Override
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
View view = convertView;
SubViewDataContainer holder;
if (view == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(layoutResourceId, parent, false);
holder = new SubViewDataContainer();
holder.checkIcon = view.findViewById(Utils.getResourceIdentifier(
"revanced_check_icon", "id"));
holder.placeholder = view.findViewById(Utils.getResourceIdentifier(
"revanced_check_icon_placeholder", "id"));
holder.itemText = view.findViewById(Utils.getResourceIdentifier(
"revanced_item_text", "id"));
view.setTag(holder);
} else {
holder = (SubViewDataContainer) view.getTag();
}
// Set text.
holder.itemText.setText(getItem(position));
holder.itemText.setTextColor(Utils.getAppForegroundColor());
// Show or hide checkmark and placeholder.
String currentValue = entryValues[position].toString();
boolean isSelected = currentValue.equals(selectedValue);
holder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
holder.checkIcon.setColorFilter(Utils.getAppForegroundColor());
holder.placeholder.setVisibility(isSelected ? View.GONE : View.VISIBLE);
return view;
}
public void setSelectedValue(String value) {
this.selectedValue = value;
}
}
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public CustomDialogListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDialogListPreference(Context context) {
super(context);
}
@Override
protected void showDialog(Bundle state) {
Context context = getContext();
// Create ListView.
ListView listView = new ListView(context);
listView.setId(android.R.id.list);
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
// Create custom adapter for the ListView.
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
context,
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
getEntries(),
getEntryValues(),
getValue()
);
listView.setAdapter(adapter);
// Set checked item.
String currentValue = getValue();
if (currentValue != null) {
CharSequence[] entryValues = getEntryValues();
for (int i = 0, length = entryValues.length; i < length; i++) {
if (currentValue.equals(entryValues[i].toString())) {
listView.setItemChecked(i, true);
listView.setSelection(i);
break;
}
}
}
// Create the custom dialog without OK button.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
getTitle() != null ? getTitle().toString() : "",
null,
null,
null, // No OK button text.
null, // No OK button action.
() -> {}, // Cancel button action (just dismiss).
null,
null,
true
);
// Add the ListView to the main layout.
LinearLayout mainLayout = dialogPair.second;
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
1.0f
);
mainLayout.addView(listView, mainLayout.getChildCount() - 1, listViewParams);
// Handle item click to select value and dismiss dialog.
listView.setOnItemClickListener((parent, view, position, id) -> {
String selectedValue = getEntryValues()[position].toString();
if (callChangeListener(selectedValue)) {
setValue(selectedValue);
adapter.setSelectedValue(selectedValue);
adapter.notifyDataSetChanged();
}
dialogPair.first.dismiss();
});
// Show the dialog.
dialogPair.first.show();
}
}

View File

@@ -1,19 +1,30 @@
package app.revanced.extension.shared.settings.preference;
import android.app.AlertDialog;
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.os.Build;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import app.revanced.extension.shared.settings.Setting;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.graphics.Color;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import static app.revanced.extension.shared.StringRef.str;
import app.revanced.extension.shared.settings.Setting;
@SuppressWarnings({"unused", "deprecation"})
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
@@ -54,7 +65,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
@Override
public boolean onPreferenceClick(Preference preference) {
try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
// Must set text before showing dialog,
// otherwise text is non-selectable if this preference is later reopened.
existingSettings = Setting.exportToJson(getContext());
getEditText().setText(existingSettings);
} catch (Exception ex) {
@@ -64,18 +76,32 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
protected void showDialog(Bundle state) {
try {
Utils.setEditTextDialogTheme(builder);
Context context = getContext();
EditText editText = getEditText();
// Show the user the settings in JSON format.
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
Utils.setClipboard(getEditText().getText());
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
importSettings(builder.getContext(), getEditText().getText().toString());
});
// Create a custom dialog with the EditText.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("revanced_pref_import_export_title"), // Title.
null, // No message (EditText replaces it).
editText, // Pass the EditText.
str("revanced_settings_import"), // OK button text.
() -> importSettings(context, editText.getText().toString()), // OK button action.
() -> {}, // Cancel button action (dismiss only).
str("revanced_settings_import_copy"), // Neutral button (Copy) text.
() -> {
// Neutral button (Copy) action. Show the user the settings in JSON format.
Utils.setClipboard(editText.getText());
},
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
Logger.printException(() -> "showDialog failure", ex);
}
}
@@ -88,7 +114,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
if (rebootNeeded) {
AbstractPreferenceFragment.showRestartDialog(getContext());
AbstractPreferenceFragment.showRestartDialog(context);
}
} catch (Exception ex) {
Logger.printException(() -> "importSettings failure", ex);
@@ -96,5 +122,4 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
AbstractPreferenceFragment.settingImportInProgress = false;
}
}
}
}

View File

@@ -1,6 +1,7 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import android.annotation.SuppressLint;
@@ -8,16 +9,19 @@ import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.view.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -48,28 +52,6 @@ public class ReVancedAboutPreference extends Preference {
return text.replace("-", "&#8209;"); // #8209 = non breaking hyphen.
}
private static String getColorHexString(int color) {
return String.format("#%06X", (0x00FFFFFF & color));
}
protected boolean isDarkModeEnabled() {
return Utils.isDarkModeEnabled();
}
/**
* Subclasses can override this and provide a themed color.
*/
protected int getLightColor() {
return Color.WHITE;
}
/**
* Subclasses can override this and provide a themed color.
*/
protected int getDarkColor() {
return Color.BLACK;
}
/**
* Apps that do not support bundling resources must override this.
*
@@ -86,9 +68,8 @@ public class ReVancedAboutPreference extends Preference {
builder.append("<html>");
builder.append("<body style=\"text-align: center; padding: 10px;\">");
final boolean isDarkMode = isDarkModeEnabled();
String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor());
String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor());
String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor());
String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor());
// Apply light/dark mode colors.
builder.append(String.format(
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
@@ -220,14 +201,38 @@ class WebViewDialog extends Dialog {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
// Create main layout.
LinearLayout mainLayout = new LinearLayout(getContext());
mainLayout.setOrientation(LinearLayout.VERTICAL);
final int padding = dipToPixels(10);
mainLayout.setPadding(padding, padding, padding, padding);
// Set rounded rectangle background.
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
Utils.createCornerRadii(28), null, null));
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
mainLayout.setBackground(mainBackground);
// Create WebView.
WebView webView = new WebView(getContext());
webView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new OpenLinksExternallyWebClient());
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
setContentView(webView);
// Add WebView to layout.
mainLayout.addView(webView);
setContentView(mainLayout);
// Set dialog window attributes
Window window = getWindow();
if (window != null) {
Utils.setDialogWindowParameters(window);
}
}
private class OpenLinksExternallyWebClient extends WebViewClient {
@@ -315,7 +320,7 @@ class AboutLinksRoutes {
// Do not show an exception toast if the server is down
final int responseCode = connection.getResponseCode();
if (responseCode != 200) {
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
return NO_CONNECTION_STATIC_LINKS;
}

View File

@@ -1,14 +1,28 @@
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.AlertDialog;
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;
@@ -44,41 +58,61 @@ public class ResettableEditTextPreference extends EditTextPreference {
this.setting = setting;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
Utils.setEditTextDialogTheme(builder);
if (setting == null) {
String key = getKey();
if (key != null) {
setting = Setting.getSettingFromPath(key);
}
}
if (setting != null) {
builder.setNeutralButton(str("revanced_settings_reset"), null);
}
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
try {
Context context = getContext();
EditText editText = getEditText();
// Override the button click listener to prevent dismissing the dialog.
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
if (button == null) {
return;
}
button.setOnClickListener(v -> {
try {
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
EditText editText = getEditText();
editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
} catch (Exception ex) {
Logger.printException(() -> "reset failure", ex);
// Resolve setting if not already set.
if (setting == null) {
String key = getKey();
if (key != null) {
setting = Setting.getSettingFromPath(key);
}
}
});
// Set initial EditText value to the current persisted value or empty string.
String initialValue = getText() != null ? getText() : "";
editText.setText(initialValue);
editText.setSelection(initialValue.length()); // Move cursor to end.
// Create custom dialog.
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
getTitle() != null ? getTitle().toString() : "", // Title.
null, // Message is replaced by EditText.
editText, // Pass the EditText.
null, // OK button text.
() -> {
// OK button action. Persist the EditText value when OK is clicked.
String newValue = editText.getText().toString();
if (callChangeListener(newValue)) {
setText(newValue);
}
},
() -> {}, // Cancel button action (dismiss only).
neutralButtonText, // Neutral button text (Reset).
() -> {
// Neutral button action.
if (setting != null) {
try {
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // Move cursor to end of text.
} catch (Exception ex) {
Logger.printException(() -> "reset failure", ex);
}
}
},
false // Do not dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
}

View File

@@ -1,7 +1,6 @@
package app.revanced.extension.shared.settings.preference;
import android.content.Context;
import android.preference.ListPreference;
import android.util.AttributeSet;
import android.util.Pair;
@@ -24,12 +23,14 @@ import app.revanced.extension.shared.Utils;
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
*/
@SuppressWarnings({"unused", "deprecation"})
public class SortedListPreference extends ListPreference {
public class SortedListPreference extends CustomDialogListPreference {
/**
* Sorts the current list entries.
*
* @param firstEntriesToPreserve The number of entries to preserve in their original position.
* @param firstEntriesToPreserve The number of entries to preserve in their original position,
* or a negative value to not sort and leave entries
* as they current are.
*/
public void sortEntryAndValues(int firstEntriesToPreserve) {
CharSequence[] entries = getEntries();
@@ -44,6 +45,10 @@ public class SortedListPreference extends ListPreference {
throw new IllegalStateException();
}
if (firstEntriesToPreserve < 0) {
return; // Nothing to do.
}
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
// Android does not have a triple class like Kotlin, So instead use a nested pair.
@@ -85,10 +90,6 @@ public class SortedListPreference extends ListPreference {
super.setEntryValues(sortedEntryValues);
}
protected int getFirstEntriesToPreserve() {
return 1;
}
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
@@ -112,4 +113,12 @@ public class SortedListPreference extends ListPreference {
sortEntryAndValues(getFirstEntriesToPreserve());
}
/**
* @return The number of first entries to leave exactly where they are, and do not sort them.
* A negative value indicates do not sort any entries.
*/
protected int getFirstEntriesToPreserve() {
return 1;
}
}

View File

@@ -1,12 +1,20 @@
plugins {
alias(libs.plugins.protobuf)
}
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:spotify:stub"))
compileOnly(libs.annotation)
implementation(project(":extensions:spotify:utils"))
implementation(libs.nanohttpd)
implementation(libs.protobuf.javalite)
}
android {
defaultConfig {
minSdk = 24
minSdk = 21
}
compileOptions {
@@ -14,3 +22,19 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
}
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
}

View File

@@ -2,23 +2,30 @@ package app.revanced.extension.spotify.layout.hide.createbutton;
import java.util.List;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.spotify.shared.ComponentFilters.*;
@SuppressWarnings("unused")
public final class HideCreateButtonPatch {
/**
* A list of ids of resources which contain the Create button title.
* A list of component filters that match whether a navigation bar item is the Create button.
* The main approach used is matching the resource id for the Create button title.
*/
private static final List<String> CREATE_BUTTON_TITLE_RES_ID_LIST = List.of(
Integer.toString(Utils.getResourceIdentifier("navigationbar_musicappitems_create_title", "string"))
private static final List<ComponentFilter> CREATE_BUTTON_COMPONENT_FILTERS = List.of(
new ResourceIdComponentFilter("navigationbar_musicappitems_create_title", "string"),
// Temporary fallback and fix for APKs merged with AntiSplit-M not having resources properly encoded,
// and thus getting the resource identifier for the Create button title always return 0.
// FIXME: Remove this once the above issue is no longer relevant.
new StringComponentFilter("spotify:create-menu")
);
/**
* The old id of the resource which contained the Create button title. Used in older versions of the app.
* A component filter for the old id of the resource which contained the Create button title.
* Used in older versions of the app.
*/
private static final int OLD_CREATE_BUTTON_TITLE_RES_ID =
Utils.getResourceIdentifier("bottom_navigation_bar_create_tab_title", "string");
private static final ResourceIdComponentFilter OLD_CREATE_BUTTON_COMPONENT_FILTER =
new ResourceIdComponentFilter("bottom_navigation_bar_create_tab_title", "string");
/**
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.
@@ -30,12 +37,24 @@ public final class HideCreateButtonPatch {
return null;
}
String stringifiedNavigationBarItem = navigationBarItem.toString();
boolean isCreateButton = CREATE_BUTTON_TITLE_RES_ID_LIST.stream()
.anyMatch(stringifiedNavigationBarItem::contains);
try {
String stringifiedNavigationBarItem = navigationBarItem.toString();
if (isCreateButton) {
return null;
for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) {
if (componentFilter.filterUnavailable()) {
Logger.printInfo(() -> "returnNullIfIsCreateButton: Filter " +
componentFilter.getFilterRepresentation() + " not available, skipping");
continue;
}
if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) {
Logger.printInfo(() -> "Hiding Create button because the navigation bar item " +
navigationBarItem + " matched the filter " + componentFilter.getFilterRepresentation());
return null;
}
}
} catch (Exception ex) {
Logger.printException(() -> "returnNullIfIsCreateButton failure", ex);
}
return navigationBarItem;
@@ -46,6 +65,18 @@ public final class HideCreateButtonPatch {
* Create button.
*/
public static boolean isOldCreateButton(int oldNavigationBarItemTitleResId) {
return oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_TITLE_RES_ID;
if (OLD_CREATE_BUTTON_COMPONENT_FILTER.filterUnavailable()) {
Logger.printInfo(() -> "Skipping hiding old Create button because the resource id for " +
OLD_CREATE_BUTTON_COMPONENT_FILTER.resourceName + " is not available");
return false;
}
if (oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_COMPONENT_FILTER.getResourceId()) {
Logger.printInfo(() -> "Hiding old Create button because the navigation bar item title resource id" +
" matched " + OLD_CREATE_BUTTON_COMPONENT_FILTER.getFilterRepresentation());
return true;
}
return false;
}
}

View File

@@ -0,0 +1,153 @@
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);
}
}

View File

@@ -0,0 +1,124 @@
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 +
')';
}
}

View File

@@ -0,0 +1,42 @@
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;
/**
* Injection point.
* <br>
* Start login server.
*/
public static void listen(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);
} 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);
}
}
}

View File

@@ -0,0 +1,267 @@
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);
}
}
}

View File

@@ -33,10 +33,11 @@ public final class SanitizeSharingLinksPatch {
}
}
return builder.build().toString();
String sanitizedUrl = builder.build().toString();
Logger.printInfo(() -> "Sanitized url " + url + " to " + sanitizedUrl);
return sanitizedUrl;
} catch (Exception ex) {
Logger.printException(() -> "sanitizeUrl failure", ex);
Logger.printException(() -> "sanitizeUrl failure with " + url, ex);
return url;
}
}

View File

@@ -0,0 +1,85 @@
package app.revanced.extension.spotify.shared;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
public final class ComponentFilters {
public interface ComponentFilter {
@NonNull
String getFilterValue();
String getFilterRepresentation();
default boolean filterUnavailable() {
return false;
}
}
public static final class ResourceIdComponentFilter implements ComponentFilter {
public final String resourceName;
public final String resourceType;
// Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded.
// 0 is returned when a resource has not been found.
private int resourceId = -1;
@Nullable
private String stringfiedResourceId;
public ResourceIdComponentFilter(String resourceName, String resourceType) {
this.resourceName = resourceName;
this.resourceType = resourceType;
}
public int getResourceId() {
if (resourceId == -1) {
resourceId = Utils.getResourceIdentifier(resourceName, resourceType);
}
return resourceId;
}
@NonNull
@Override
public String getFilterValue() {
if (stringfiedResourceId == null) {
stringfiedResourceId = Integer.toString(getResourceId());
}
return stringfiedResourceId;
}
@Override
public String getFilterRepresentation() {
boolean resourceFound = getResourceId() != 0;
return (resourceFound ? getFilterValue() + " (" : "") + resourceName + (resourceFound ? ")" : "");
}
@Override
public boolean filterUnavailable() {
boolean resourceNotFound = getResourceId() == 0;
if (resourceNotFound) {
Logger.printInfo(() -> "Resource id for " + resourceName + " was not found");
}
return resourceNotFound;
}
}
public static final class StringComponentFilter implements ComponentFilter {
public final String string;
public StringComponentFilter(String string) {
this.string = string;
}
@NonNull
@Override
public String getFilterValue() {
return string;
}
@Override
public String getFilterRepresentation() {
return string;
}
}
}

View File

@@ -0,0 +1,43 @@
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;
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {
@@ -7,7 +7,7 @@ android {
compileSdk = 34
defaultConfig {
minSdk = 24
minSdk = 21
}
compileOptions {

View File

@@ -0,0 +1,19 @@
plugins {
java
antlr
}
dependencies {
antlr(libs.antlr4)
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
tasks {
generateGrammarSource {
arguments = listOf("-visitor")
}
}

View File

@@ -0,0 +1,35 @@
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]+
;

View File

@@ -0,0 +1,60 @@
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();
}
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -16,7 +16,7 @@ public class SpoofSimPatch {
return false;
}
Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
Logger.printException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
return true;
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,7 +1,7 @@
android.namespace = "app.revanced.extension"
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -1,179 +0,0 @@
package app.revanced.extension.youtube;
import static app.revanced.extension.shared.Utils.clamp;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Build;
import android.text.style.ReplacementSpan;
import android.text.TextPaint;
import android.view.Window;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
public class ThemeHelper {
@Nullable
private static Integer darkThemeColor, lightThemeColor;
private static int themeValue;
/**
* Injection point.
*/
@SuppressWarnings("unused")
public static void setTheme(Enum<?> value) {
final int newOrdinalValue = value.ordinal();
if (themeValue != newOrdinalValue) {
themeValue = newOrdinalValue;
Logger.printDebug(() -> "Theme value: " + newOrdinalValue);
}
}
public static boolean isDarkTheme() {
return themeValue == 1;
}
public static void setActivityTheme(Activity activity) {
final var theme = isDarkTheme()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
}
/**
* Injection point.
*/
@SuppressWarnings("SameReturnValue")
private static String darkThemeResourceName() {
// Value is changed by Theme patch, if included.
return "@color/yt_black3";
}
private static int getThemeColor(String resourceName, int defaultColor) {
try {
return Utils.getColorFromString(resourceName);
} catch (Exception ex) {
// User entered an invalid custom theme color.
// Normally this should never be reached, and no localized strings are needed.
Utils.showToastLong("Invalid custom theme color: " + resourceName);
return defaultColor;
}
}
/**
* @return The dark theme color as specified by the Theme patch (if included),
* or the dark mode background color unpatched YT uses.
*/
public static int getDarkThemeColor() {
if (darkThemeColor == null) {
darkThemeColor = getThemeColor(darkThemeResourceName(), Color.BLACK);
}
return darkThemeColor;
}
/**
* Injection point.
*/
@SuppressWarnings("SameReturnValue")
private static String lightThemeResourceName() {
// Value is changed by Theme patch, if included.
return "@color/yt_white1";
}
/**
* @return The light theme color as specified by the Theme patch (if included),
* or the non dark mode background color unpatched YT uses.
*/
public static int getLightThemeColor() {
if (lightThemeColor == null) {
lightThemeColor = getThemeColor(lightThemeResourceName(), Color.WHITE);
}
return lightThemeColor;
}
public static int getBackgroundColor() {
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
}
public static int getForegroundColor() {
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
}
public static int getDialogBackgroundColor() {
final String colorName = isDarkTheme()
? "yt_black1"
: "yt_white1";
return Utils.getColorFromString(colorName);
}
public static int getToolbarBackgroundColor() {
final String colorName = isDarkTheme()
? "yt_black3"
: "yt_white1";
return Utils.getColorFromString(colorName);
}
/**
* Sets the system navigation bar color for the activity.
* Applies the background color obtained from {@link #getBackgroundColor()} to the navigation bar.
* For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
*/
public static void setNavigationBarColor(@Nullable Window window) {
if (window == null) {
Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
return;
}
window.setNavigationBarColor(getBackgroundColor());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(true);
}
}
/**
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
* <p>
* If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF).
* If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000).
* The alpha channel remains unchanged.
*
* @param color The input color to adjust, in ARGB format.
* @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening)
* or values <= 1.0f to darken (e.g., 0.95f for slight darkening).
* @return The adjusted color in ARGB format.
*/
public static int adjustColorBrightness(int color, float factor) {
final int alpha = Color.alpha(color);
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
if (factor > 1.0f) {
// Lighten: Interpolate toward white (255)
final float t = 1.0f - (1.0f / factor); // Interpolation parameter
red = Math.round(red + (255 - red) * t);
green = Math.round(green + (255 - green) * t);
blue = Math.round(blue + (255 - blue) * t);
} else {
// Darken or no change: Scale toward black
red = (int) (red * factor);
green = (int) (green * factor);
blue = (int) (blue * factor);
}
// Ensure values are within [0, 255]
red = clamp(red, 0, 255);
green = clamp(green, 0, 255);
blue = clamp(blue, 0, 255);
return Color.argb(alpha, red, green, blue);
}
}

View File

@@ -3,7 +3,10 @@ package app.revanced.extension.youtube.patches;
import static app.revanced.extension.shared.StringRef.str;
import android.app.Activity;
import android.app.Dialog;
import android.text.Html;
import android.util.Pair;
import android.widget.LinearLayout;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -63,18 +66,28 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
}
Utils.runOnMainThread(() -> {
var alert = new android.app.AlertDialog.Builder(context)
.setTitle(str("revanced_check_watch_history_domain_name_dialog_title"))
.setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")))
.setIconAttribute(android.R.attr.alertDialogIcon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
dialog.dismiss();
}).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> {
Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false);
dialog.dismiss();
}).create();
try {
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
null, // No EditText.
null, // OK button text.
() -> {}, // OK button action (just dismiss).
() -> {}, // Cancel button action (just dismiss).
str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text.
() -> Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore).
true // Dismiss dialog on Neutral button click.
);
Utils.showDialog(context, alert, false, null);
// Show the dialog.
Dialog dialog = dialogPair.first;
Utils.showDialog(context, dialog, false, null);
} catch (Exception ex) {
Logger.printException(() -> "checkDnsResolver dialog creation failure", ex);
}
});
} catch (Exception ex) {
Logger.printException(() -> "checkDnsResolver failure", ex);

View File

@@ -1,6 +1,7 @@
package app.revanced.extension.youtube.patches;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import app.revanced.extension.shared.Logger;
@@ -58,6 +59,22 @@ public final class HidePlayerOverlayButtonsPatch {
});
}
/**
* Injection point.
*/
public static void hidePlayerControlButtonsBackground(View rootView) {
try {
if (!Settings.HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND.get()) {
return;
}
// Each button is an ImageView with a background set to another drawable.
removeImageViewsBackgroundRecursive(rootView);
} catch (Exception ex) {
Logger.printException(() -> "removePlayerControlButtonsBackground failure", ex);
}
}
private static void hideView(View parentView, int resourceId) {
View nextPreviousButton = parentView.findViewById(resourceId);
@@ -69,4 +86,16 @@ public final class HidePlayerOverlayButtonsPatch {
Logger.printDebug(() -> "Hiding previous/next button");
Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton);
}
private static void removeImageViewsBackgroundRecursive(View currentView) {
if (currentView instanceof ImageView imageView) {
imageView.setBackground(null);
}
if (currentView instanceof ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
removeImageViewsBackgroundRecursive(viewGroup.getChildAt(i));
}
}
}
}

View File

@@ -7,11 +7,17 @@ public class VersionCheckPatch {
return Utils.getAppVersionName().compareTo(version) >= 0;
}
@Deprecated
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
@Deprecated
public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
@Deprecated
public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
@Deprecated
public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
@Deprecated
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
@Deprecated
public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00");
}

View File

@@ -2,13 +2,16 @@ package app.revanced.extension.youtube.patches.announcements;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Pair;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.json.JSONArray;
@@ -120,25 +123,38 @@ public final class AnnouncementsPatch {
final Level finalLevel = level;
Utils.runOnMainThread(() -> {
// Show the announcement.
var alert = new AlertDialog.Builder(context)
.setTitle(finalTitle)
.setMessage(finalMessage)
.setIcon(finalLevel.icon)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
dialog.dismiss();
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
dialog.dismiss();
})
.setCancelable(false)
.create();
// Create the custom dialog and show the announcement.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
finalTitle, // Title.
finalMessage, // Message.
null, // No EditText.
null, // OK button text.
() -> Settings.ANNOUNCEMENT_LAST_ID.save(finalId), // OK button action.
() -> {}, // Cancel button action (dismiss only).
str("revanced_announcements_dialog_dismiss"), // Neutral button text.
() -> {}, // Neutral button action (dismiss only).
true // Dismiss dialog when onNeutralClick.
);
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
// Make links clickable.
((TextView) dialog.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
});
Dialog dialog = dialogPair.first;
LinearLayout mainLayout = dialogPair.second;
// Set the icon for the title TextView
for (int i = 0, childCould = mainLayout.getChildCount(); i < childCould; i++) {
View child = mainLayout.getChildAt(i);
if (child instanceof TextView childTextView && finalTitle.equals(childTextView.getText().toString())) {
childTextView.setCompoundDrawablesWithIntrinsicBounds(
finalLevel.icon, 0, 0, 0);
childTextView.setCompoundDrawablePadding(dipToPixels(8));
}
}
// Set dialog as non-cancelable.
dialog.setCancelable(false);
// Show the dialog.
Utils.showDialog(context, dialog);
});
} catch (Exception e) {
final var message = "Failed to get announcement";

View File

@@ -121,12 +121,14 @@ public final class AdsFilter extends Filter {
playerShoppingShelf = new StringFilterGroup(
Settings.HIDE_PLAYER_STORE_SHELF,
"expandable_list.eml",
"horizontal_shelf.eml"
);
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
null,
"shopping_item_card_list.eml"
"shopping_link_item",
"shopping_item_card_list"
);
channelProfile = new StringFilterGroup(

View File

@@ -6,7 +6,7 @@ import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQual
import app.revanced.extension.youtube.settings.Settings;
/**
* Abuse LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
* LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
*/
public final class AdvancedVideoQualityMenuFilter extends Filter {
// Must be volatile or synchronized, as litho filtering runs off main thread

View File

@@ -46,7 +46,7 @@ final class ButtonsFilter extends Filter {
"|download_button.eml"
),
new StringFilterGroup(
Settings.HIDE_PLAYLIST_BUTTON,
Settings.HIDE_SAVE_BUTTON,
"|save_to_playlist_button"
),
new StringFilterGroup(
@@ -76,6 +76,10 @@ final class ButtonsFilter extends Filter {
Settings.HIDE_ASK_BUTTON,
"yt_fill_spark"
),
new ByteArrayFilterGroup(
Settings.HIDE_STOP_ADS_BUTTON,
"yt_outline_slash_circle_left"
),
// Check for clip button both here and using a path filter,
// as there's a chance the path is a generic action button and won't contain 'clip_button'
new ByteArrayFilterGroup(

View File

@@ -7,11 +7,6 @@ import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
final class CommentsFilter extends Filter {
private static final String TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH
= "|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|";
private final StringFilterGroup commentComposer;
private final ByteArrayFilterGroup emojiPickerBufferGroup;
private final StringFilterGroup filterChipBar;
private final ByteArrayFilterGroup aiCommentsSummary;
@@ -50,14 +45,9 @@ final class CommentsFilter extends Filter {
"super_thanks_button.eml"
);
commentComposer = new StringFilterGroup(
Settings.HIDE_COMMENTS_TIMESTAMP_AND_EMOJI_BUTTONS,
"comment_composer.eml"
);
emojiPickerBufferGroup = new ByteArrayFilterGroup(
null,
"id.comment.quick_emoji.button"
StringFilterGroup timestampButton = new StringFilterGroup(
Settings.HIDE_COMMENTS_TIMESTAMP_BUTTON,
"composer_timestamp_button.eml"
);
filterChipBar = new StringFilterGroup(
@@ -77,7 +67,7 @@ final class CommentsFilter extends Filter {
createAShort,
previewComment,
thanksButton,
commentComposer,
timestampButton,
filterChipBar
);
}
@@ -85,14 +75,6 @@ final class CommentsFilter extends Filter {
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == commentComposer) {
// To completely hide the emoji buttons (and leave no empty space), the timestamp button is
// also hidden because the buffer is exactly the same and there's no way selectively hide.
return contentIndex == 0
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
&& emojiPickerBufferGroup.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == filterChipBar) {
return aiCommentsSummary.check(protobufBufferArray).isFiltered();
}

View File

@@ -14,6 +14,9 @@ final class DescriptionComponentsFilter extends Filter {
private final StringFilterGroup macroMarkersCarousel;
private final StringFilterGroup horizontalShelf;
private final ByteArrayFilterGroup cellVideoAttribute;
public DescriptionComponentsFilter() {
exceptions.addPatterns(
"compact_channel",
@@ -35,8 +38,7 @@ final class DescriptionComponentsFilter extends Filter {
final StringFilterGroup attributesSection = new StringFilterGroup(
Settings.HIDE_ATTRIBUTES_SECTION,
"gaming_section",
"music_section",
// "gaming_section", "music_section"
"video_attributes_section"
);
@@ -76,15 +78,26 @@ final class DescriptionComponentsFilter extends Filter {
)
);
horizontalShelf = new StringFilterGroup(
Settings.HIDE_ATTRIBUTES_SECTION,
"horizontal_shelf.eml"
);
cellVideoAttribute = new ByteArrayFilterGroup(
null,
"cell_video_attribute"
);
addPathCallbacks(
aiGeneratedVideoSummarySection,
askSection,
attributesSection,
infoCardsSection,
horizontalShelf,
howThisWasMadeSection,
macroMarkersCarousel,
podcastSection,
transcriptSection,
macroMarkersCarousel
transcriptSection
);
}
@@ -97,6 +110,10 @@ final class DescriptionComponentsFilter extends Filter {
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == horizontalShelf) {
return cellVideoAttribute.check(protobufBufferArray).isFiltered();
}
return true;
}
}

View File

@@ -74,6 +74,27 @@ public final class LithoFilterPatch {
}
}
/**
* Litho layout fixed thread pool size override.
* <p>
* Unpatched YouTube uses a layout fixed thread pool between 1 and 3 threads:
* <pre>
* 1 thread - > Device has less than 6 cores
* 2 threads -> Device has over 6 cores and less than 6GB of memory
* 3 threads -> Device has over 6 cores and more than 6GB of memory
* </pre>
*
* Using more than 1 thread causes layout issues such as the You tab watch/playlist shelf
* that is sometimes incorrectly hidden (ReVanced is not hiding it), and seems to
* fix a race issue if using the active navigation tab status with litho filtering.
*/
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
/**
* Placeholder for actual filters.
*/
private static final class DummyFilter extends Filter { }
private static final Filter[] filters = new Filter[] {
new DummyFilter() // Replaced by patch.
};
@@ -213,9 +234,28 @@ public final class LithoFilterPatch {
return false;
}
}
/**
* Placeholder for actual filters.
*/
final class DummyFilter extends Filter { }
/**
* Injection point.
*/
public static int getExecutorCorePoolSize(int originalCorePoolSize) {
if (originalCorePoolSize != LITHO_LAYOUT_THREAD_POOL_SIZE) {
Logger.printDebug(() -> "Overriding core thread pool size from: " + originalCorePoolSize
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
}
return LITHO_LAYOUT_THREAD_POOL_SIZE;
}
/**
* Injection point.
*/
public static int getExecutorMaxThreads(int originalMaxThreads) {
if (originalMaxThreads != LITHO_LAYOUT_THREAD_POOL_SIZE) {
Logger.printDebug(() -> "Overriding max thread pool size from: " + originalMaxThreads
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
}
return LITHO_LAYOUT_THREAD_POOL_SIZE;
}
}

View File

@@ -40,8 +40,10 @@ public final class ShortsFilter extends Filter {
private static WeakReference<PivotBar> pivotBarRef = new WeakReference<>(null);
private final StringFilterGroup shortsCompactFeedVideoPath;
private final StringFilterGroup shortsCompactFeedVideo;
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
private final StringFilterGroup useSoundButton;
private final ByteArrayFilterGroup useSoundButtonBuffer;
private final StringFilterGroup subscribeButton;
private final StringFilterGroup joinButton;
@@ -49,11 +51,11 @@ public final class ShortsFilter extends Filter {
private final StringFilterGroup shelfHeader;
private final StringFilterGroup suggestedAction;
private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList();
private final StringFilterGroup shortsActionBar;
private final StringFilterGroup actionButton;
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroup videoActionButton;
private final ByteArrayFilterGroupList videoActionButtonBuffer = new ByteArrayFilterGroupList();
public ShortsFilter() {
//
@@ -82,7 +84,7 @@ public final class ShortsFilter extends Filter {
// Path components.
//
shortsCompactFeedVideoPath = new StringFilterGroup(null,
shortsCompactFeedVideo = new StringFilterGroup(null,
// Shorts that appear in the feed/search when the device is using tablet layout.
"compact_video.eml",
// 'video_lockup_with_attachment.eml' is shown instead of 'compact_video.eml' for some users
@@ -174,7 +176,18 @@ public final class ShortsFilter extends Filter {
"reel_action_bar.eml"
);
actionButton = new StringFilterGroup(
useSoundButton = new StringFilterGroup(
Settings.HIDE_SHORTS_USE_SOUND_BUTTON,
"floating_action_button.eml",
REEL_METAPANEL_PATH
);
useSoundButtonBuffer = new ByteArrayFilterGroup(
null,
"yt_outline_camera_"
);
videoActionButton = new StringFilterGroup(
null,
// Can be simply 'button.eml', 'shorts_video_action_button.eml' or 'reel_action_button.eml'
"button.eml"
@@ -182,20 +195,21 @@ public final class ShortsFilter extends Filter {
suggestedAction = new StringFilterGroup(
null,
"suggested_action.eml"
"suggested_action.eml",
REEL_METAPANEL_PATH
);
addPathCallbacks(
shortsCompactFeedVideoPath, joinButton, subscribeButton, paidPromotionButton,
shortsCompactFeedVideo, joinButton, subscribeButton, paidPromotionButton,
shortsActionBar, suggestedAction, pausedOverlayButtons, channelBar,
fullVideoLinkLabel, videoTitle, reelSoundMetadata, soundButton, infoPanel,
fullVideoLinkLabel, videoTitle, useSoundButton, reelSoundMetadata, soundButton, infoPanel,
stickers, likeFountain, likeButton, dislikeButton
);
//
// All other action buttons.
//
videoActionButtonGroupList.addAll(
videoActionButtonBuffer.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
"reel_comment_button",
@@ -216,7 +230,7 @@ public final class ShortsFilter extends Filter {
//
// Suggested actions.
//
suggestedActionsGroupList.addAll(
suggestedActionsBuffer.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_PREVIEW_COMMENT,
// Preview comment that can popup while a Short is playing.
@@ -242,10 +256,7 @@ public final class ShortsFilter extends Filter {
"yt_outline_bookmark_",
// 'Save sound' button. It seems this has been removed and only 'Save music' is used.
// Still hide this in case it's still present.
"yt_outline_list_add_",
// 'Use this sound' button. It seems this has been removed and only 'Save music' is used.
// Still hide this in case it's still present.
"yt_outline_camera_"
"yt_outline_list_add_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS,
@@ -263,10 +274,19 @@ public final class ShortsFilter extends Filter {
Settings.HIDE_SHORTS_UPCOMING_BUTTON,
"yt_outline_bell_"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_EFFECT_BUTTON,
// https://www.gstatic.com/youtube/effects/xeno/arcade/effects/icons/
"/arcade/effects/icons/"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
"greenscreen_temp"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_NEW_POSTS_BUTTON,
"yt_outline_box_pencil"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_HASHTAG_BUTTON,
"yt_outline_hashtag_"
@@ -275,7 +295,7 @@ public final class ShortsFilter extends Filter {
}
private boolean isEverySuggestedActionFilterEnabled() {
for (ByteArrayFilterGroup group : suggestedActionsGroupList) {
for (ByteArrayFilterGroup group : suggestedActionsBuffer) {
if (!group.isEnabled()) {
return false;
}
@@ -293,15 +313,19 @@ public final class ShortsFilter extends Filter {
return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
}
if (matchedGroup == shortsCompactFeedVideoPath) {
if (matchedGroup == useSoundButton) {
return useSoundButtonBuffer.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == shortsCompactFeedVideo) {
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
}
// Video action buttons (comment, share, remix) have the same path.
// Like and dislike are separate path filters and don't require buffer searching.
if (matchedGroup == shortsActionBar) {
return actionButton.check(path).isFiltered()
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered();
return videoActionButton.check(path).isFiltered()
&& videoActionButtonBuffer.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == suggestedAction) {
@@ -312,7 +336,7 @@ public final class ShortsFilter extends Filter {
return true;
}
return suggestedActionsGroupList.check(protobufBufferArray).isFiltered();
return suggestedActionsBuffer.check(protobufBufferArray).isFiltered();
}
return true;

View File

@@ -13,8 +13,6 @@ import app.revanced.extension.youtube.settings.Settings;
/**
* This patch contains the logic to always open the advanced video quality menu.
* Two methods are required, because the quality menu is a RecyclerView in the new YouTube version
* and a ListView in the old one.
*/
@SuppressWarnings("unused")
public final class AdvancedVideoQualityMenuPatch {
@@ -76,7 +74,7 @@ public final class AdvancedVideoQualityMenuPatch {
/**
* Injection point.
*
* Used if spoofing to an old app version, and also used for the Shorts video quality flyout.
* Shorts video quality flyout.
*/
public static void showAdvancedVideoQualityMenu(ListView listView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
@@ -90,14 +88,12 @@ public final class AdvancedVideoQualityMenuPatch {
final var indexOfAdvancedQualityMenuItem = 4;
if (listView.indexOfChild(child) != indexOfAdvancedQualityMenuItem) return;
Logger.printDebug(() -> "Found advanced menu item in old type of quality menu");
listView.setSoundEffectsEnabled(false);
final var qualityItemMenuPosition = 4;
listView.performItemClick(null, qualityItemMenuPosition, 0);
} catch (Exception ex) {
Logger.printException(() -> "showOldVideoQualityMenu failure", ex);
Logger.printException(() -> "showAdvancedVideoQualityMenu failure", ex);
}
}

View File

@@ -64,10 +64,11 @@ public class RememberVideoQualityPatch {
else videoQualityWifi.save(defaultQuality);
networkTypeMessage = str("revanced_remember_video_quality_wifi");
}
Utils.showToastShort(str(
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast",
networkTypeMessage, (defaultQuality + "p")
));
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
Utils.showToastShort(str(
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast",
networkTypeMessage, (defaultQuality + "p")
));
}
/**

View File

@@ -19,13 +19,15 @@ import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.icu.text.NumberFormat;
import android.support.v7.widget.RecyclerView;
import android.view.animation.Animation;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.GridLayout;
@@ -39,7 +41,6 @@ import java.util.function.Function;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch;
import app.revanced.extension.youtube.settings.Settings;
@@ -240,6 +241,9 @@ public class CustomPlaybackSpeedPatch {
// Store the dialog reference.
currentDialog = new WeakReference<>(dialog);
// Enable dismissing the dialog when tapping outside.
dialog.setCanceledOnTouchOutside(true);
// Create main vertical LinearLayout for dialog content.
LinearLayout mainLayout = new LinearLayout(context);
mainLayout.setOrientation(LinearLayout.VERTICAL);
@@ -259,15 +263,15 @@ public class CustomPlaybackSpeedPatch {
// Set rounded rectangle background for the main layout.
RoundRectShape roundRectShape = new RoundRectShape(
createCornerRadii(12), null, null);
Utils.createCornerRadii(12), null, null);
ShapeDrawable background = new ShapeDrawable(roundRectShape);
background.getPaint().setColor(ThemeHelper.getDialogBackgroundColor());
background.getPaint().setColor(Utils.getDialogBackgroundColor());
mainLayout.setBackground(background);
// Add handle bar at the top.
View handleBar = new View(context);
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
createCornerRadii(4), null, null));
Utils.createCornerRadii(4), null, null));
handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true));
handleBar.setBackground(handleBackground);
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(
@@ -285,7 +289,7 @@ public class CustomPlaybackSpeedPatch {
float currentSpeed = VideoInformation.getPlaybackSpeed();
// Initially show with only 0 minimum digits, so 1.0 shows as 1x
currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0));
currentSpeedText.setTextColor(ThemeHelper.getForegroundColor());
currentSpeedText.setTextColor(Utils.getAppForegroundColor());
currentSpeedText.setTextSize(16);
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
currentSpeedText.setGravity(Gravity.CENTER);
@@ -305,7 +309,8 @@ public class CustomPlaybackSpeedPatch {
// Create minus button.
Button minusButton = new Button(context, null, 0); // Disable default theme style.
minusButton.setText(""); // No text on button.
ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null));
ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(
Utils.createCornerRadii(20), null, null));
minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
minusButton.setBackground(minusBackground);
OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol.
@@ -319,9 +324,9 @@ public class CustomPlaybackSpeedPatch {
speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
speedSlider.setProgress(speedToProgressValue(currentSpeed));
speedSlider.getProgressDrawable().setColorFilter(
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
speedSlider.getThumb().setColorFilter(
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons.
@@ -331,7 +336,7 @@ public class CustomPlaybackSpeedPatch {
Button plusButton = new Button(context, null, 0); // Disable default theme style.
plusButton.setText(""); // No text on button.
ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape(
createCornerRadii(20), null, null));
Utils.createCornerRadii(20), null, null));
plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
plusButton.setBackground(plusBackground);
OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol.
@@ -418,13 +423,13 @@ public class CustomPlaybackSpeedPatch {
// Create speed button.
Button speedButton = new Button(context, null, 0);
speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format.
speedButton.setTextColor(ThemeHelper.getForegroundColor());
speedButton.setTextColor(Utils.getAppForegroundColor());
speedButton.setTextSize(12);
speedButton.setAllCaps(false);
speedButton.setGravity(Gravity.CENTER);
ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
createCornerRadii(20), null, null));
Utils.createCornerRadii(20), null, null));
buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
speedButton.setBackground(buttonBackground);
speedButton.setPadding(dip5, dip5, dip5, dip5);
@@ -442,7 +447,7 @@ public class CustomPlaybackSpeedPatch {
TextView normalLabel = new TextView(context);
// Use same 'Normal' string as stock YouTube.
normalLabel.setText(str("normal_playback_rate_label"));
normalLabel.setTextColor(ThemeHelper.getForegroundColor());
normalLabel.setTextColor(Utils.getAppForegroundColor());
normalLabel.setTextSize(10);
normalLabel.setGravity(Gravity.CENTER);
@@ -489,6 +494,77 @@ public class CustomPlaybackSpeedPatch {
window.setBackgroundDrawable(null); // Remove default dialog background.
}
// Apply slide-in animation when showing the dialog.
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
slideInABottomAnimation.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideInABottomAnimation);
// Set touch listener on mainLayout to enable drag-to-dismiss.
//noinspection ClickableViewAccessibility
mainLayout.setOnTouchListener(new View.OnTouchListener() {
/** Threshold for dismissing the dialog. */
final float dismissThreshold = dipToPixels(100); // Distance to drag to dismiss.
/** Store initial Y position of touch. */
float touchY;
/** Track current translation. */
float translationY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Capture initial Y position of touch.
touchY = event.getRawY();
translationY = mainLayout.getTranslationY();
return true;
case MotionEvent.ACTION_MOVE:
// Calculate drag distance and apply translation downwards only.
final float deltaY = event.getRawY() - touchY;
// Only allow downward drag (positive deltaY).
if (deltaY >= 0) {
mainLayout.setTranslationY(translationY + deltaY);
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// Check if dialog should be dismissed based on drag distance.
if (mainLayout.getTranslationY() > dismissThreshold) {
// Animate dialog off-screen and dismiss.
//noinspection ExtractMethodRecommender
final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
- mainLayout.getTop();
TranslateAnimation slideOut = new TranslateAnimation(
0, 0, mainLayout.getTranslationY(), remainingDistance);
slideOut.setDuration(fadeDurationFast);
slideOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
dialog.dismiss();
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
mainLayout.startAnimation(slideOut);
} else {
// Animate back to original position if not dragged far enough.
TranslateAnimation slideBack = new TranslateAnimation(
0, 0, mainLayout.getTranslationY(), 0);
slideBack.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideBack);
mainLayout.setTranslationY(0);
}
return true;
default:
return false;
}
}
});
// Create observer for PlayerType changes.
Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() {
@Override
@@ -515,27 +591,9 @@ public class CustomPlaybackSpeedPatch {
Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
});
// Apply slide-in animation when showing the dialog.
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
slideInABottomAnimation.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideInABottomAnimation);
dialog.show(); // Display the dialog.
}
/**
* Creates an array of corner radii for a rounded rectangle shape.
*
* @param dp The radius in density-independent pixels (dp) to apply to all corners.
* @return An array of eight float values representing the corner radii
* (top-left, top-right, bottom-right, bottom-left).
*/
private static float[] createCornerRadii(float dp) {
final float radius = dipToPixels(dp);
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
}
/**
* @param speed The playback speed value to format.
* @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x").
@@ -559,9 +617,9 @@ public class CustomPlaybackSpeedPatch {
* @return The rounded speed, constrained to the specified bounds.
*/
private static float roundSpeedToNearestIncrement(float speed) {
// Round to nearest 0.05 speed.
final float roundedSpeed = Math.round(speed / 0.05f) * 0.05f;
return Utils.clamp(roundedSpeed, 0.05f, PLAYBACK_SPEED_MAXIMUM);
// 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);
}
/**
@@ -573,12 +631,12 @@ public class CustomPlaybackSpeedPatch {
* for light themes to ensure visual contrast.
*/
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
final int baseColor = ThemeHelper.getDialogBackgroundColor();
final int baseColor = Utils.getDialogBackgroundColor();
float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
return ThemeHelper.isDarkTheme()
? ThemeHelper.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
: ThemeHelper.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
return Utils.isDarkModeEnabled()
? Utils.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
: Utils.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
}
}
@@ -592,7 +650,7 @@ class OutlineSymbolDrawable extends Drawable {
OutlineSymbolDrawable(boolean isPlus) {
this.isPlus = isPlus;
paint = new Paint(Paint.ANTI_ALIAS_FLAG); // Enable anti-aliasing for smooth rendering.
paint.setColor(ThemeHelper.getForegroundColor());
paint.setColor(Utils.getAppForegroundColor());
paint.setStyle(Paint.Style.STROKE); // Use stroke style for outline.
paint.setStrokeWidth(dipToPixels(1)); // 1dp stroke width.
}

View File

@@ -57,7 +57,8 @@ public final class RememberPlaybackSpeedPatch {
}
Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed);
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get())
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
}, TOAST_DELAY_MILLISECONDS);
}
} catch (Exception ex) {

View File

@@ -2,6 +2,7 @@ package app.revanced.extension.youtube.patches.theme;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.clamp;
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle;
import android.content.res.Resources;
import android.graphics.Color;
@@ -173,23 +174,15 @@ public final class SeekbarColorPatch {
*/
public static void setSplashAnimationLottie(LottieAnimationView view, int resourceId) {
try {
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
SplashScreenAnimationStyle animationStyle = Settings.SPLASH_SCREEN_ANIMATION_STYLE.get();
if (!SEEKBAR_CUSTOM_COLOR_ENABLED
// Black and white animations cannot use color replacements.
|| animationStyle == SplashScreenAnimationStyle.FPS_30_BLACK_AND_WHITE
|| animationStyle == SplashScreenAnimationStyle.FPS_60_BLACK_AND_WHITE) {
view.patch_setAnimation(resourceId);
return;
}
//noinspection ConstantConditions
if (false) { // Set true to force slow animation for development.
final int longAnimation = Utils.getResourceIdentifier(
Utils.isDarkModeEnabled()
? "startup_animation_5s_30fps_dark"
: "startup_animation_5s_30fps_light",
"raw");
if (longAnimation != 0) {
resourceId = longAnimation;
}
}
// Must specify primary key name otherwise the morphing YT logo color is also changed.
String originalKey = "\"k\":";
String originalPrimary = originalKey + "[1,0,0.2,1]";
@@ -199,21 +192,16 @@ public final class SeekbarColorPatch {
String replacementAccent = originalKey + getColorStringArray(customSeekbarColorGradient[1]);
String json = loadRawResourceAsString(resourceId);
if (json == null) {
return; // Should never happen.
}
String replacement = json
.replace(originalPrimary, replacementPrimary)
.replace(originalAccent, replacementAccent);
if (BaseSettings.DEBUG.get() && (!json.contains(originalPrimary) || !json.contains(originalAccent))) {
String jsonFinal = json;
Logger.printException(() -> "Could not replace launch animation colors: " + jsonFinal);
Logger.printException(() -> "Could not replace splash animation colors: " + json);
}
Logger.printDebug(() -> "Replacing Lottie animation JSON");
json = json.replace(originalPrimary, replacementPrimary);
json = json.replace(originalAccent, replacementAccent);
// cacheKey is not needed since the animation will not be reused.
view.patch_setAnimation(new ByteArrayInputStream(json.getBytes()), null);
view.patch_setAnimation(new ByteArrayInputStream(replacement.getBytes()), null);
} catch (Exception ex) {
Logger.printException(() -> "setSplashAnimationLottie failure", ex);
}
@@ -234,8 +222,7 @@ public final class SeekbarColorPatch {
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name()).useDelimiter("\\A")) {
return scanner.next();
} catch (IOException e) {
Logger.printException(() -> "Could not load resource: " + resourceId);
return null;
throw new IllegalStateException("Could not load resource: " + resourceId);
}
}

View File

@@ -1,11 +1,48 @@
package app.revanced.extension.youtube.patches.theme;
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle.styleFromOrdinal;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ThemePatch {
public enum SplashScreenAnimationStyle {
DEFAULT(0),
FPS_60_ONE_SECOND(1),
FPS_60_TWO_SECOND(2),
FPS_60_FIVE_SECOND(3),
FPS_60_BLACK_AND_WHITE(4),
FPS_30_ONE_SECOND(5),
FPS_30_TWO_SECOND(6),
FPS_30_FIVE_SECOND(7),
FPS_30_BLACK_AND_WHITE(8);
// There exists a 10th json style used as the switch statement default,
// but visually it is identical to 60fps one second.
@Nullable
static SplashScreenAnimationStyle styleFromOrdinal(int style) {
// Alternatively can return using values()[style]
for (SplashScreenAnimationStyle value : values()) {
if (value.style == style) {
return value;
}
}
return null;
}
final int style;
SplashScreenAnimationStyle(int style) {
this.style = style;
}
}
// color constants used in relation with litho components
private static final int[] WHITE_VALUES = {
-1, // comments chip background
@@ -37,7 +74,7 @@ public class ThemePatch {
* @return The new or original color value
*/
public static int getValue(int originalValue) {
if (ThemeHelper.isDarkTheme()) {
if (Utils.isDarkModeEnabled()) {
if (anyEquals(originalValue, DARK_VALUES)) return BLACK_COLOR;
} else {
if (anyEquals(originalValue, WHITE_VALUES)) return WHITE_COLOR;
@@ -58,4 +95,22 @@ public class ThemePatch {
public static boolean gradientLoadingScreenEnabled(boolean original) {
return GRADIENT_LOADING_SCREEN_ENABLED;
}
/**
* Injection point.
*/
public static int getLoadingScreenType(int original) {
SplashScreenAnimationStyle style = Settings.SPLASH_SCREEN_ANIMATION_STYLE.get();
if (style == SplashScreenAnimationStyle.DEFAULT) {
return original;
}
final int replacement = style.style;
if (original != replacement) {
Logger.printDebug(() -> "Overriding splash screen style from: "
+ styleFromOrdinal(original) + " to: " + style);
}
return replacement;
}
}

View File

@@ -30,11 +30,15 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.returnyoutubedislike.requests.RYDVoteData;
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.youtube.settings.Settings;
@@ -177,7 +181,7 @@ public class ReturnYouTubeDislike {
* Ideally, this would be the actual color YT uses at runtime.
*/
private static int getSeparatorColor() {
return ThemeHelper.isDarkTheme()
return Utils.isDarkModeEnabled()
? 0x33FFFFFF
: 0xFFD9D9D9;
}

View File

@@ -14,7 +14,6 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
@@ -27,6 +26,8 @@ import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFrag
@SuppressWarnings("unused")
public class LicenseActivityHook {
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
private static ViewGroup.LayoutParams toolbarLayoutParams;
public static void setToolbarLayoutParams(Toolbar toolbar) {
@@ -78,8 +79,8 @@ public class LicenseActivityHook {
*/
public static void initialize(Activity licenseActivity) {
try {
ThemeHelper.setActivityTheme(licenseActivity);
ThemeHelper.setNavigationBarColor(licenseActivity.getWindow());
setActivityTheme(licenseActivity);
ReVancedPreferenceFragment.setNavigationBarColor(licenseActivity.getWindow());
licenseActivity.setContentView(getResourceIdentifier(
"revanced_settings_with_toolbar", "layout"));
@@ -114,7 +115,7 @@ public class LicenseActivityHook {
toolBarParent.removeView(dummyToolbar);
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
toolbar.setBackgroundColor(getToolbarBackgroundColor());
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
@@ -124,7 +125,7 @@ public class LicenseActivityHook {
TextView toolbarTextView = Utils.getChildView(toolbar, false,
view -> view instanceof TextView);
if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor());
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
}
setToolbarLayoutParams(toolbar);
@@ -135,4 +136,34 @@ public class LicenseActivityHook {
toolBarParent.addView(toolbar, 0);
}
public static void setActivityTheme(Activity activity) {
final var theme = Utils.isDarkModeEnabled()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
activity.setTheme(getResourceIdentifier(theme, "style"));
}
public static int getToolbarBackgroundColor() {
final String colorName = Utils.isDarkModeEnabled()
? "yt_black3"
: "yt_white1";
return Utils.getColorFromString(colorName);
}
/**
* Injection point.
*
* Updates dark/light mode since YT settings can force light/dark mode
* which can differ from the global device settings.
*/
@SuppressWarnings("unused")
public static void updateLightDarkModeStatus(Enum<?> value) {
final int themeOrdinal = value.ordinal();
if (currentThemeValueOrdinal != themeOrdinal) {
currentThemeValueOrdinal = themeOrdinal;
Utils.setIsDarkModeEnabled(themeOrdinal == 1);
}
}
}

View File

@@ -4,9 +4,10 @@ import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.graphics.drawable.GradientDrawable;
import android.util.Pair;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -18,6 +19,7 @@ import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import java.util.ArrayList;
@@ -31,7 +33,6 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
/**
@@ -58,11 +59,7 @@ public class SearchViewController {
GradientDrawable background = new GradientDrawable();
background.setShape(GradientDrawable.RECTANGLE);
background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
int baseColor = ThemeHelper.getBackgroundColor();
int adjustedColor = ThemeHelper.isDarkTheme()
? ThemeHelper.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme.
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
background.setColor(adjustedColor);
background.setColor(getSearchViewBackground());
return background;
}
@@ -72,10 +69,17 @@ public class SearchViewController {
private static GradientDrawable createSuggestionBackgroundDrawable(Context context) {
GradientDrawable background = new GradientDrawable();
background.setShape(GradientDrawable.RECTANGLE);
background.setCornerRadius(8 * context.getResources().getDisplayMetrics().density); // 8dp corner radius.
background.setColor(getSearchViewBackground());
return background;
}
@ColorInt
public static int getSearchViewBackground() {
return Utils.isDarkModeEnabled()
? Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), 1.11f)
: Utils.adjustColorBrightness(Utils.getThemeLightColor(), 0.95f);
}
/**
* Adds search view components to the activity.
*/
@@ -171,10 +175,6 @@ public class SearchViewController {
final int actionSearchId = getResourceIdentifier("action_search", "id");
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
searchItem.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
? "yt_outline_search_white_24"
: "yt_outline_search_black_24",
"drawable")).setTooltipText(null);
// Set menu item click listener.
toolbar.setOnMenuItemClickListener(item -> {
@@ -316,12 +316,7 @@ public class SearchViewController {
private void closeSearch() {
isSearchActive = false;
toolbar.getMenu().findItem(getResourceIdentifier(
"action_search", "id"))
.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
? "yt_outline_search_white_24"
: "yt_outline_search_black_24",
"drawable")
).setVisible(true);
"action_search", "id")).setVisible(true);
toolbar.setTitle(originalTitle);
searchContainer.setVisibility(View.GONE);
searchView.setQuery("", false);
@@ -365,13 +360,22 @@ public class SearchViewController {
// Set long click listener for deletion confirmation.
convertView.setOnLongClickListener(v -> {
new AlertDialog.Builder(activity)
.setTitle(query)
.setMessage(str("revanced_settings_search_remove_message"))
.setPositiveButton(android.R.string.ok,
(dialog, which) -> removeSearchQuery(query))
.setNegativeButton(android.R.string.cancel, null)
.show();
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
activity,
query, // Title.
str("revanced_settings_search_remove_message"), // Message.
null, // No EditText.
null, // OK button text.
() -> removeSearchQuery(query), // OK button action.
() -> {}, // Cancel button action (dismiss only).
null, // No Neutral button text.
() -> {}, // Neutral button action (dismiss only).
true // Dismiss dialog when onNeutralClick.
);
Dialog dialog = dialogPair.first;
dialog.setCancelable(true); // Allow dismissal via back button.
dialog.show(); // Show the dialog.
return true;
});

View File

@@ -21,14 +21,12 @@ import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerT
import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPatch.ShortsPlayerType;
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.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;
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
import android.graphics.Color;
import app.revanced.extension.shared.Logger;
@@ -40,18 +38,22 @@ import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.settings.LongSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.DeArrowAvailability;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime;
import app.revanced.extension.youtube.patches.MiniplayerPatch;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
public class Settings extends BaseSettings {
// Video
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);
@@ -60,6 +62,8 @@ public class Settings extends BaseSettings {
// 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);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, false,
parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
public static final BooleanSetting CUSTOM_SPEED_MENU = new BooleanSetting("revanced_custom_speed_menu", TRUE);
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",
@@ -135,6 +139,7 @@ public class Settings extends BaseSettings {
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);
@@ -179,7 +184,7 @@ public class Settings extends BaseSettings {
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_CREATE_A_SHORT_BUTTON = new BooleanSetting("revanced_hide_comments_create_a_short_button", TRUE);
public static final BooleanSetting HIDE_COMMENTS_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comments_timestamp_and_emoji_buttons", 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);
@@ -195,15 +200,16 @@ public class Settings extends BaseSettings {
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);
public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", TRUE);
public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE);
public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE);
public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE);
public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", TRUE);
public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE);
public static final BooleanSetting HIDE_SAVE_BUTTON = new BooleanSetting("revanced_hide_save_button", FALSE);
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);
public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE);
// 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);
@@ -226,6 +232,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 BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
"revanced_remove_viewer_discretion_dialog_user_dialog_message");
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message");
@@ -259,7 +267,9 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE);
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE);
public static final BooleanSetting HIDE_SHORTS_EFFECT_BUTTON = new BooleanSetting("revanced_hide_shorts_effect_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_NEW_POSTS_BUTTON = new BooleanSetting("revanced_hide_shorts_new_posts_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_HASHTAG_BUTTON = new BooleanSetting("revanced_hide_shorts_hashtag_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_HISTORY = new BooleanSetting("revanced_hide_shorts_history", FALSE);
public static final BooleanSetting HIDE_SHORTS_HOME = new BooleanSetting("revanced_hide_shorts_home", FALSE);
@@ -285,6 +295,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
public static final BooleanSetting HIDE_SHORTS_UPCOMING_BUTTON = new BooleanSetting("revanced_hide_shorts_upcoming_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_USE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_sound_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
public static final BooleanSetting SHORTS_AUTOPLAY = new BooleanSetting("revanced_shorts_autoplay", FALSE);

View File

@@ -3,17 +3,18 @@ package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.StringRef.sf;
import android.content.Context;
import android.preference.ListPreference;
import android.util.AttributeSet;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings;
/**
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
* Custom video speeds used by {@link CustomPlaybackSpeedPatch}.
*/
@SuppressWarnings({"unused", "deprecation"})
public final class CustomVideoSpeedListPreference extends ListPreference {
public final class CustomVideoSpeedListPreference extends CustomDialogListPreference {
/**
* Initialize a settings preference list with the available playback speeds.
@@ -59,4 +60,5 @@ public final class CustomVideoSpeedListPreference extends ListPreference {
public CustomVideoSpeedListPreference(Context context) {
super(context);
}
}

View File

@@ -18,6 +18,7 @@ import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets;
import android.widget.TextView;
import android.widget.Toolbar;
@@ -40,7 +41,6 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
import app.revanced.extension.youtube.ThemeHelper;
import app.revanced.extension.youtube.settings.LicenseActivityHook;
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
@@ -71,11 +71,27 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
@SuppressLint("UseCompatLoadingForDrawables")
public static Drawable getBackButtonDrawable() {
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
? "yt_outline_arrow_left_white_24"
: "yt_outline_arrow_left_black_24",
"drawable");
return Utils.getContext().getResources().getDrawable(backButtonResource);
final int backButtonResource = getResourceIdentifier("revanced_settings_toolbar_arrow_left", "drawable");
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
drawable.setTint(Utils.getAppForegroundColor());
return drawable;
}
/**
* Sets the system navigation bar color for the activity.
* Applies the background color obtained from {@link Utils#getAppBackgroundColor()} to the navigation bar.
* For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
*/
public static void setNavigationBarColor(@Nullable Window window) {
if (window == null) {
Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
return;
}
window.setNavigationBarColor(Utils.getAppBackgroundColor());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.setNavigationBarContrastEnforced(true);
}
}
/**
@@ -201,9 +217,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
// Set icon for the placeholder preference.
noResultsPreference.setLayoutResource(getResourceIdentifier(
"revanced_preference_with_icon_no_search_result", "layout"));
noResultsPreference.setIcon(getResourceIdentifier(
ThemeHelper.isDarkTheme() ? "yt_outline_search_white_24" : "yt_outline_search_black_24",
"drawable"));
noResultsPreference.setIcon(getResourceIdentifier("revanced_settings_search_icon", "drawable"));
preferenceScreen.addPreference(noResultsPreference);
}
}
@@ -226,7 +240,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
.getParent();
// Fix the system navigation bar color for submenus.
ThemeHelper.setNavigationBarColor(preferenceScreenDialog.getWindow());
setNavigationBarColor(preferenceScreenDialog.getWindow());
// Fix edge-to-edge screen with Android 15 and YT 19.45+
// https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
@@ -250,7 +264,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
TextView toolbarTextView = Utils.getChildView(toolbar,
true, TextView.class::isInstance);
if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor());
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
}
LicenseActivityHook.setToolbarLayoutParams(toolbar);
@@ -304,10 +318,10 @@ class AbstractPreferenceSearchData<T extends Preference> {
return text;
}
final int baseColor = ThemeHelper.getBackgroundColor();
final int adjustedColor = ThemeHelper.isDarkTheme()
? ThemeHelper.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
final int baseColor = Utils.getAppBackgroundColor();
final int adjustedColor = Utils.isDarkModeEnabled()
? Utils.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
: Utils.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
SpannableStringBuilder spannable = new SpannableStringBuilder(text);

View File

@@ -1,32 +0,0 @@
package app.revanced.extension.youtube.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
import app.revanced.extension.youtube.ThemeHelper;
@SuppressWarnings("unused")
public class ReVancedYouTubeAboutPreference extends ReVancedAboutPreference {
public int getLightColor() {
return ThemeHelper.getLightThemeColor();
}
public int getDarkColor() {
return ThemeHelper.getDarkThemeColor();
}
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReVancedYouTubeAboutPreference(Context context) {
super(context);
}
}

View File

@@ -2,9 +2,11 @@ package app.revanced.extension.youtube.sponsorblock;
import static app.revanced.extension.shared.StringRef.str;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.util.Pair;
import android.util.Patterns;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -56,7 +58,7 @@ public class SponsorBlockSettings {
}
}
for (int i = 0; i < categorySelectionsArray.length(); i++) {
for (int i = 0, length = categorySelectionsArray.length(); i < length; i++) {
JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i);
String categoryKey = categorySelectionObject.getString("name");
@@ -181,13 +183,25 @@ public class SponsorBlockSettings {
// If user has a SponsorBlock user id then show a warning.
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId()
&& !Settings.SB_HIDE_EXPORT_WARNING.get()) {
new AlertDialog.Builder(dialogContext)
.setMessage(str("revanced_sb_settings_revanced_export_user_id_warning"))
.setNeutralButton(str("revanced_sb_settings_revanced_export_user_id_warning_dismiss"),
(dialog, which) -> Settings.SB_HIDE_EXPORT_WARNING.save(true))
.setPositiveButton(android.R.string.ok, null)
.setCancelable(false)
.show();
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
dialogContext,
null, // No title.
str("revanced_sb_settings_revanced_export_user_id_warning"), // Message.
null, // No EditText.
null, // OK button text.
() -> {}, // OK button action (dismiss only).
null, // No cancel button action.
str("revanced_sb_settings_revanced_export_user_id_warning_dismiss"), // Neutral button text.
() -> Settings.SB_HIDE_EXPORT_WARNING.save(true), // Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
// Set dialog as non-cancelable.
dialogPair.first.setCancelable(false);
// Show the dialog.
dialogPair.first.show();
}
}

View File

@@ -2,12 +2,12 @@ package app.revanced.extension.youtube.sponsorblock.objects;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Bundle;
@@ -15,13 +15,10 @@ import android.preference.ListPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.*;
import androidx.annotation.ColorInt;
@@ -52,6 +49,7 @@ public class SegmentCategoryListPreference extends ListPreference {
private EditText dialogColorEditText;
private EditText dialogOpacityEditText;
private ColorPickerView dialogColorPickerView;
private Dialog dialog;
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
super(context);
@@ -75,30 +73,45 @@ public class SegmentCategoryListPreference extends ListPreference {
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
protected void showDialog(Bundle state) {
try {
Utils.setEditTextDialogTheme(builder);
Context context = getContext();
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
selectedDialogEntryIndex = findIndexOfValue(getValue());
Context context = builder.getContext();
LinearLayout mainLayout = new LinearLayout(context);
mainLayout.setOrientation(LinearLayout.VERTICAL);
mainLayout.setPadding(70, 0, 70, 0);
// Create the main layout for the dialog content.
LinearLayout contentLayout = new LinearLayout(context);
contentLayout.setOrientation(LinearLayout.VERTICAL);
// Add behavior selection radio buttons.
RadioGroup radioGroup = new RadioGroup(context);
radioGroup.setOrientation(RadioGroup.VERTICAL);
CharSequence[] entries = getEntries();
for (int i = 0; i < entries.length; i++) {
RadioButton radioButton = new RadioButton(context);
radioButton.setText(entries[i]);
radioButton.setId(i);
radioButton.setChecked(i == selectedDialogEntryIndex);
radioGroup.addView(radioButton);
}
radioGroup.setOnCheckedChangeListener((group, checkedId) -> selectedDialogEntryIndex = checkedId);
radioGroup.setPadding(dipToPixels(10), 0, 0, 0);
contentLayout.addView(radioGroup);
// Inflate the color picker view.
View colorPickerContainer = LayoutInflater.from(context)
.inflate(getResourceIdentifier("revanced_color_picker", "layout"), null);
dialogColorPickerView = colorPickerContainer.findViewById(
getResourceIdentifier("color_picker_view", "id"));
getResourceIdentifier("revanced_color_picker_view", "id"));
dialogColorPickerView.setColor(categoryColor);
mainLayout.addView(colorPickerContainer);
contentLayout.addView(colorPickerContainer);
// Grid layout for color and opacity inputs.
GridLayout gridLayout = new GridLayout(context);
gridLayout.setColumnCount(3);
gridLayout.setRowCount(2);
gridLayout.setPadding(dipToPixels(16), 0, 0, 0);
GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row.
@@ -111,7 +124,7 @@ public class SegmentCategoryListPreference extends ListPreference {
gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row.
gridParams.columnSpec = GridLayout.spec(1); // Second column.
gridParams.setMargins(0, 0, 10, 0);
gridParams.setMargins(0, 0, dipToPixels(10), 0);
dialogColorDotView = new TextView(context);
dialogColorDotView.setLayoutParams(gridParams);
gridLayout.addView(dialogColorDotView);
@@ -162,8 +175,7 @@ public class SegmentCategoryListPreference extends ListPreference {
}
}
});
dialogColorEditText.setLayoutParams(gridParams);
gridLayout.addView(dialogColorEditText);
gridLayout.addView(dialogColorEditText, gridParams);
gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(1); // Second row.
@@ -226,11 +238,70 @@ public class SegmentCategoryListPreference extends ListPreference {
}
}
});
dialogOpacityEditText.setLayoutParams(gridParams);
gridLayout.addView(dialogOpacityEditText);
gridLayout.addView(dialogOpacityEditText, gridParams);
updateOpacityText();
mainLayout.addView(gridLayout);
contentLayout.addView(gridLayout);
// Create ScrollView to wrap the content layout.
ScrollView contentScrollView = new ScrollView(context);
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
1.0f
);
contentScrollView.setLayoutParams(scrollViewParams);
contentScrollView.addView(contentLayout);
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
category.title.toString(), // Title.
null, // No message (replaced by contentLayout).
null, // No EditText.
null, // OK button text.
() -> {
// OK button action.
if (selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[selectedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
SegmentCategory.updateEnabledCategories();
}
try {
category.setColor(dialogColorEditText.getText().toString());
category.setOpacity(categoryOpacity);
} catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_settings_color_invalid"));
}
updateUI();
}
},
() -> {}, // Cancel button action (dismiss only).
str("revanced_settings_reset_color"), // Neutral button text.
() -> {
// Neutral button action (Reset).
try {
// Setting view color causes callback to update the UI.
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
categoryOpacity = category.getOpacityDefault();
updateOpacityText();
} catch (Exception ex) {
Logger.printException(() -> "resetButton onClick failure", ex);
}
},
false // Do not dismiss dialog on Neutral button click.
);
// Add the ScrollView to the dialog's main layout.
LinearLayout dialogMainLayout = dialogPair.second;
dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
// Set up color picker listener.
// Do last to prevent listener callbacks while setting up view.
@@ -247,69 +318,25 @@ public class SegmentCategoryListPreference extends ListPreference {
dialogColorEditText.setSelection(hexColor.length());
});
builder.setView(mainLayout);
builder.setTitle(category.title.toString());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
});
builder.setNeutralButton(str("revanced_settings_reset_color"), null);
builder.setNegativeButton(android.R.string.cancel, null);
selectedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), selectedDialogEntryIndex,
(dialog, which) -> selectedDialogEntryIndex = which);
// Show the dialog.
dialog = dialogPair.first;
dialog.show();
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
Logger.printException(() -> "showDialog failure", ex);
}
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
// Do not close dialog when reset is pressed.
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
button.setOnClickListener(view -> {
try {
// Setting view color causes callback to update the UI.
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
categoryOpacity = category.getOpacityDefault();
updateOpacityText();
} catch (Exception ex) {
Logger.printException(() -> "setOnClickListener failure", ex);
}
});
}
@Override
protected void onDialogClosed(boolean positiveResult) {
try {
if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[selectedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
SegmentCategory.updateEnabledCategories();
}
// Nullify dialog references.
dialogColorDotView = null;
dialogColorEditText = null;
dialogOpacityEditText = null;
dialogColorPickerView = null;
try {
category.setColor(dialogColorEditText.getText().toString());
category.setOpacity(categoryOpacity);
} catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_settings_color_invalid"));
}
updateUI();
}
} catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex);
} finally {
dialogColorDotView = null;
dialogColorEditText = null;
dialogOpacityEditText = null;
dialogColorPickerView = null;
if (dialog != null) {
dialog.dismiss();
dialog = null;
}
}

View File

@@ -3,19 +3,25 @@ package app.revanced.extension.youtube.sponsorblock.ui;
import static app.revanced.extension.shared.StringRef.str;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.preference.*;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceGroup;
import android.preference.SwitchPreference;
import android.text.Html;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.List;
@@ -279,14 +285,26 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
Boolean newValue = (Boolean) o;
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("revanced_sb_guidelines_popup_title"))
.setMessage(str("revanced_sb_guidelines_popup_content"))
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
.setCancelable(false)
.show();
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
preference1.getContext(),
str("revanced_sb_guidelines_popup_title"), // Title.
str("revanced_sb_guidelines_popup_content"), // Message.
null, // No EditText.
str("revanced_sb_guidelines_popup_open"), // OK button text.
() -> openGuidelines(), // OK button action.
null, // Cancel button action.
str("revanced_sb_guidelines_popup_already_read"), // Neutral button text.
() -> {}, // Neutral button action (dismiss only).
true // Dismiss dialog when onNeutralClick.
);
// Set dialog as non-cancelable.
dialogPair.first.setCancelable(false);
dialogPair.first.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true));
// Show the dialog.
dialogPair.first.show();
}
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
updateUI();
@@ -372,16 +390,52 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
generalCategory.addPreference(minSegmentDuration);
privateUserId = new EditTextPreference(context) {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder);
@Override
protected void showDialog(Bundle state) {
try {
Context context = getContext();
EditText editText = getEditText();
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
try {
Utils.setClipboard(getEditText().getText());
} catch (Exception ex) {
Logger.printException(() -> "Copy settings failure", ex);
}
});
// Set initial EditText value to the current persisted value or empty string.
String initialValue = getText() != null ? getText() : "";
editText.setText(initialValue);
editText.setSelection(initialValue.length()); // Move cursor to end.
// Create custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
getTitle() != null ? getTitle().toString() : "", // Title.
null, // Message is replaced by EditText.
editText, // Pass the EditText.
null, // OK button text.
() -> {
// OK button action. Persist the EditText value when OK is clicked.
String newValue = editText.getText().toString();
if (callChangeListener(newValue)) {
setText(newValue);
}
},
() -> {}, // Cancel button action (dismiss only).
str("revanced_sb_settings_copy"), // Neutral button text (Copy).
() -> {
// Neutral button action (Copy).
try {
Utils.setClipboard(getEditText().getText());
} catch (Exception ex) {
Logger.printException(() -> "Copy settings failure", ex);
}
},
true // Dismiss dialog when onNeutralClick.
);
// Set dialog as cancelable.
dialogPair.first.setCancelable(true);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
};
privateUserId.setTitle(str("revanced_sb_general_uuid"));
@@ -407,51 +461,90 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
editText.setText(Settings.SB_API_URL.get());
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
Settings.SB_API_URL.resetToDefault();
Utils.showToastLong(str("revanced_sb_api_url_reset"));
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
String serverAddress = editText.getText().toString();
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
Settings.SB_API_URL.save(serverAddress);
Utils.showToastLong(str("revanced_sb_api_url_changed"));
}
}
};
new AlertDialog.Builder(context)
.setTitle(apiUrl.getTitle())
.setView(editText)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("revanced_settings_reset"), urlChangeListener)
.setPositiveButton(android.R.string.ok, urlChangeListener)
.show();
// Create a custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("revanced_sb_general_api_url"), // Title.
null, // No message, EditText replaces it.
editText, // Pass the EditText.
null, // OK button text.
() -> {
// OK button action.
String serverAddress = editText.getText().toString();
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
Settings.SB_API_URL.save(serverAddress);
Utils.showToastLong(str("revanced_sb_api_url_changed"));
}
},
() -> {}, // Cancel button action (dismiss dialog).
str("revanced_settings_reset"), // Neutral (Reset) button text.
() -> {
// Neutral button action.
Settings.SB_API_URL.resetToDefault();
Utils.showToastLong(str("revanced_sb_api_url_reset"));
},
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
return true;
});
generalCategory.addPreference(apiUrl);
importExport = new EditTextPreference(context) {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder);
@Override
protected void showDialog(Bundle state) {
try {
Context context = getContext();
EditText editText = getEditText();
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
try {
Utils.setClipboard(getEditText().getText());
} catch (Exception ex) {
Logger.printException(() -> "Copy settings failure", ex);
}
});
// Create a custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("revanced_sb_settings_ie"), // Title.
null, // No message, EditText replaces it.
editText, // Pass the EditText.
str("revanced_settings_import"), // OK button text.
() -> {
// OK button action. Trigger OnPreferenceChangeListener.
String newValue = editText.getText().toString();
if (getOnPreferenceChangeListener() != null) {
getOnPreferenceChangeListener().onPreferenceChange(this, newValue);
}
},
() -> {}, // Cancel button action (dismiss only).
str("revanced_sb_settings_copy"), // Neutral button text (Copy).
() -> {
// Neutral button action (Copy).
try {
Utils.setClipboard(editText.getText());
} catch (Exception ex) {
Logger.printException(() -> "Copy settings failure", ex);
}
},
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
};
importExport.setTitle(str("revanced_sb_settings_ie"));
// Summary is set in updateUI()
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
// Summary is set in updateUI().
EditText editText = importExport.getEditText();
editText.setInputType(InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
importExport.getEditText().setAutofillHints((String) null);
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
editText.setAutofillHints((String) null);
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
// Set preference listeners.
importExport.setOnPreferenceClickListener(preference1 -> {
importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
return true;

View File

@@ -3,7 +3,7 @@ package app.revanced.extension.youtube.sponsorblock.ui;
import static android.text.Html.fromHtml;
import static app.revanced.extension.shared.StringRef.str;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
@@ -11,6 +11,8 @@ import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.util.Pair;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
@@ -194,14 +196,26 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
updateStatsSelfSaved.run();
preference.setOnPreferenceClickListener(preference1 -> {
new AlertDialog.Builder(preference1.getContext())
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
preference.getContext(),
str("revanced_sb_stats_self_saved_reset_title"), // Title.
null, // No message.
null, // No EditText.
null, // OK button text.
() -> {
// OK button action.
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
updateStatsSelfSaved.run();
})
.setNegativeButton(android.R.string.no, null).show();
},
() -> {}, // Cancel button action (dismiss only).
null, // No neutral button.
null, // No neutral button action.
true // Dismiss dialog when onNeutralClick.
);
// Show the dialog.
dialogPair.first.show();
return true;
});

View File

@@ -42,7 +42,6 @@ public class PlaybackSpeedDialogButton {
: Settings.PLAYBACK_SPEED_DEFAULT.get();
VideoInformation.overridePlaybackSpeed(speed);
showToastShort(str("revanced_custom_playback_speeds_reset_toast", (speed + "x")));
} catch (Exception ex) {
Logger.printException(() -> "speed button reset failure", ex);
}

View File

@@ -1,5 +1,5 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
alias(libs.plugins.android.library)
}
android {

View File

@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
version = 5.26.1-dev.1
version = 5.29.1-dev.1

View File

@@ -11,14 +11,25 @@ appcompat = "1.7.0"
okhttp = "5.0.0-alpha.14"
retrofit = "2.11.0"
guava = "33.4.0-jre"
protobuf-javalite = "4.31.1"
protoc = "4.31.1"
protobuf = "0.9.5"
antlr4 = "4.13.2"
nanohttpd = "2.3.1"
apksig = "8.10.1"
[libraries]
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
antlr4 = { module = "org.antlr:antlr4", version.ref = "antlr4" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf-javalite" }
protobuf-protoc = { module = "com.google.protobuf:protoc", version.ref = "protoc" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
apksig = { group = "com.android.tools.build", name = "apksig", version.ref = "apksig" }
[plugins]
android-library = { id = "com.android.library", version.ref = "agp" }
android-library = { id = "com.android.library" }
protobuf = { id = "com.google.protobuf", version.ref = "protobuf" }

View File

@@ -1,8 +1,6 @@
#Mon Jun 16 14:39:32 CEST 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -6,6 +6,10 @@ public final class app/revanced/patches/all/misc/adb/HideAdbPatchKt {
public static final fun getHideAdbStatusPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/all/misc/appicon/HideAppIconPatchKt {
public static final fun getHideAppIconPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatchKt {
public static final fun baseSpoofBuildInfoPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -112,6 +116,10 @@ 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/targetSdk/SetTargetSdkVersion34Kt {
public static final fun getSetTargetSdkVersion34 ()Lapp/revanced/patcher/patch/ResourcePatch;
}
@@ -156,6 +164,14 @@ public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecks
public static final fun getBypassRootChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
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/crunchyroll/ads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/duolingo/ad/DisableAdsPatchKt {
public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -192,6 +208,10 @@ public final class app/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatchK
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/googlephotos/misc/backup/EnableDCIMFoldersBackupControlPatchKt {
public static final fun getEnableDCIMFoldersBackupControlPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/googlephotos/misc/extension/ExtensionPatchKt {
public static final fun getExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -284,6 +304,10 @@ public final class app/revanced/patches/messenger/inputfield/DisableTypingIndica
public static final fun getDisableTypingIndicatorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/messenger/layout/HideFacebookButtonPatchKt {
public static final fun getHideFacebookButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/messenger/metaai/RemoveMetaAIPatchKt {
public static final fun getRemoveMetaAIPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -644,17 +668,39 @@ public final class app/revanced/patches/shared/misc/gms/GmsCoreSupportPatchKt {
public static synthetic fun gmsCoreSupportResourcePatch$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/shared/misc/hex/HexPatchKt {
public static final fun hexPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/RawResourcePatch;
public final class app/revanced/patches/shared/misc/hex/HexPatchBuilder : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> ()V
public fun add (Lapp/revanced/patches/shared/misc/hex/Replacement;)Z
public synthetic fun add (Ljava/lang/Object;)Z
public fun addAll (Ljava/util/Collection;)Z
public final fun asPatternTo (Ljava/lang/String;Ljava/lang/String;)Lkotlin/Pair;
public fun clear ()V
public fun contains (Lapp/revanced/patches/shared/misc/hex/Replacement;)Z
public final fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun getSize ()I
public final fun inFile (Lkotlin/Pair;Ljava/lang/String;)V
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun retainAll (Ljava/util/Collection;)Z
public final fun size ()I
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
}
public final class app/revanced/patches/shared/misc/hex/HexPatchBuilderKt {
public static final fun hexPatch (ZLkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static final fun hexPatch (ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun hexPatch$default (ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun hexPatch$default (ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
}
public final class app/revanced/patches/shared/misc/hex/Replacement {
public static final field Companion Lapp/revanced/patches/shared/misc/hex/Replacement$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun replacePattern ([B)V
}
public final class app/revanced/patches/shared/misc/hex/Replacement$Companion {
public fun <init> ([B[BLjava/lang/String;)V
public final fun getReplacementBytesPadded ()[B
}
public final class app/revanced/patches/shared/misc/mapping/ResourceElement {
@@ -682,6 +728,7 @@ public final class app/revanced/patches/shared/misc/pairip/license/DisableLicens
}
public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt {
public static final fun overrideThemeColors (Ljava/lang/String;Ljava/lang/String;)V
public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun settingsPatch (Lkotlin/Pair;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
public static synthetic fun settingsPatch$default (Ljava/util/List;Ljava/util/Set;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
@@ -897,6 +944,10 @@ public final class app/revanced/patches/spotify/misc/extension/ExtensionPatchKt
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/fix/SpoofClientPatchKt {
public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/fix/SpoofPackageInfoPatchKt {
public static final fun getSpoofPackageInfoPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -909,6 +960,10 @@ public final class app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginP
public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatchKt {
public static final fun getChangeLyricsProviderPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -941,6 +996,10 @@ public final class app/revanced/patches/swissid/integritycheck/RemoveGooglePlayI
public static final fun getRemoveGooglePlayIntegrityCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/threads/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatchKt {
public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
@@ -1662,6 +1721,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V
@@ -1671,6 +1731,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V

View File

@@ -15,6 +15,9 @@ patches {
dependencies {
// Required due to smali, or build fails. Can be removed once smali is bumped.
implementation(libs.guava)
implementation(libs.apksig)
// Android API stubs defined here.
compileOnly(project(":patches:stub"))
}

View File

@@ -0,0 +1,48 @@
package app.revanced.patches.all.misc.appicon
import app.revanced.patcher.patch.resourcePatch
import app.revanced.util.asSequence
import app.revanced.util.childElementsSequence
import java.util.logging.Logger
import org.w3c.dom.Element
@Suppress("unused")
val hideAppIconPatch = resourcePatch(
name = "Hide app icon",
description = "Hides the app icon from the Android launcher.",
use = false,
) {
execute {
document("AndroidManifest.xml").use { document ->
var changed = false
val intentFilters = document.getElementsByTagName("intent-filter")
for (node in intentFilters.asSequence().filterIsInstance<Element>()) {
var hasMainAction = false
var launcherCategory: Element? = null
for (child in node.childElementsSequence()) {
when (child.tagName) {
"action" -> if (child.getAttribute("android:name") == "android.intent.action.MAIN") {
hasMainAction = true
}
"category" -> if (child.getAttribute("android:name") == "android.intent.category.LAUNCHER") {
launcherCategory = child
}
}
}
if (hasMainAction && launcherCategory != null) {
launcherCategory.setAttribute("android:name", "android.intent.category.DEFAULT")
changed = true
}
}
if (!changed) {
Logger.getLogger(this::class.java.name)
.warning("No changes made: Did not find any launcher intent-filters to change.")
}
}
}
}

View File

@@ -3,7 +3,7 @@ package app.revanced.patches.all.misc.hex
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.rawResourcePatch
import app.revanced.patcher.patch.stringsOption
import app.revanced.patches.shared.misc.hex.Replacement
import app.revanced.patches.shared.misc.hex.HexPatchBuilder
import app.revanced.patches.shared.misc.hex.hexPatch
import app.revanced.util.Utils.trimIndentMultiline
@@ -13,44 +13,42 @@ val hexPatch = rawResourcePatch(
description = "Replaces a hexadecimal patterns of bytes of files in an APK.",
use = false,
) {
// TODO: Instead of stringArrayOption, use a custom option type to work around
// https://github.com/ReVanced/revanced-library/issues/48.
// Replace the custom option type with a stringArrayOption once the issue is resolved.
val replacements by stringsOption(
key = "replacements",
title = "Replacements",
description = """
Hexadecimal patterns to search for and replace with another in a target file.
A pattern is a sequence of case insensitive strings, each representing hexadecimal bytes, separated by spaces.
An example pattern is 'aa 01 02 FF'.
Every pattern must be followed by a pipe ('|'), the replacement pattern,
another pipe ('|'), and the path to the file to make the changes in relative to the APK root.
The replacement pattern must have the same length as the original pattern.
The replacement pattern must be shorter or equal in length to the pattern.
Full example of a valid input:
'aa 01 02 FF|00 00 00 00|path/to/file'
Full example of a valid replacement:
'01 02 aa FF|03 04|path/to/file'
""".trimIndentMultiline(),
required = true,
)
dependsOn(
hexPatch {
replacements!!.map { from ->
val (pattern, replacementPattern, targetFilePath) = try {
from.split("|", limit = 3)
} catch (e: Exception) {
throw PatchException(
"Invalid input: $from.\n" +
"Every pattern must be followed by a pipe ('|'), " +
"the replacement pattern, another pipe ('|'), " +
"and the path to the file to make the changes in relative to the APK root. ",
)
hexPatch(
block = fun HexPatchBuilder.() {
replacements!!.forEach { replacement ->
try {
val (pattern, replacementPattern, targetFilePath) = replacement.split("|", limit = 3)
pattern asPatternTo replacementPattern inFile targetFilePath
} catch (e: Exception) {
throw PatchException(
"Invalid replacement: $replacement.\n" +
"Every pattern must be followed by a pipe ('|'), " +
"the replacement pattern, another pipe ('|'), " +
"and the path to the file to make the changes in relative to the APK root. ",
)
}
}
Replacement(pattern, replacementPattern, targetFilePath)
}.toSet()
},
},
)
)
}

View File

@@ -0,0 +1,95 @@
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;
}

View File

@@ -3,5 +3,5 @@ package app.revanced.patches.bandcamp.limitations
import app.revanced.patcher.fingerprint
internal val handlePlaybackLimitsFingerprint = fingerprint {
strings("play limits processing track", "found play_count")
strings("track_id", "play_count")
}

View File

@@ -1,7 +1,7 @@
package app.revanced.patches.bandcamp.limitations
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused")
val removePlayLimitsPatch = bytecodePatch(
@@ -11,6 +11,6 @@ val removePlayLimitsPatch = bytecodePatch(
compatibleWith("com.bandcamp.android")
execute {
handlePlaybackLimitsFingerprint.method.addInstructions(0, "return-void")
handlePlaybackLimitsFingerprint.method.returnEarly()
}
}

View File

@@ -0,0 +1,27 @@
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.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused")
val disableAdsPatch = bytecodePatch (
name = "Hide ads",
) {
compatibleWith("com.cricbuzz.android"("6.23.02"))
execute {
userStateSwitchFingerprint.method.apply {
val opcodeIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_OBJECT)
val register = getInstruction<OneRegisterInstruction>(opcodeIndex).registerA
addInstruction(
opcodeIndex + 1,
"const-string v$register, \"ACTIVE\""
)
}
}
}

View File

@@ -0,0 +1,9 @@
package app.revanced.patches.cricbuzz.ads
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)
}

View File

@@ -0,0 +1,7 @@
package app.revanced.patches.crunchyroll.ads
import app.revanced.patcher.fingerprint
internal val videoUrlReadyToStringFingerprint = fingerprint {
strings("VideoUrlReady(url=", ", enableAds=")
}

View File

@@ -0,0 +1,46 @@
package app.revanced.patches.crunchyroll.ads
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.removeFlags
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
@Suppress("unused")
val hideAdsPatch = bytecodePatch(
name = "Hide Ads"
) {
compatibleWith("com.crunchyroll.crunchyroid")
execute {
// Get obfuscated "enableAds" field from toString method.
val enableAdsField = videoUrlReadyToStringFingerprint.let {
val strIndex = videoUrlReadyToStringFingerprint.stringMatches!!.last().index
val fieldIndex = it.method.indexOfFirstInstruction(strIndex, Opcode.IGET_BOOLEAN)
it.method.getInstruction<ReferenceInstruction>(fieldIndex).getReference<FieldReference>()!!
}
// Remove final access flag on field.
videoUrlReadyToStringFingerprint.classDef.fields
.first { it.name == enableAdsField.name }
.removeFlags(AccessFlags.FINAL)
// Override enableAds field in non-default constructor.
val constructor = videoUrlReadyToStringFingerprint.classDef.methods.first {
AccessFlags.CONSTRUCTOR.isSet(it.accessFlags) && it.parameters.isNotEmpty()
}
constructor.addInstructions(
constructor.instructions.count() - 1,
"""
move-object/from16 v0, p0
const/4 v1, 0x0
iput-boolean v1, v0, $enableAdsField
""")
}
}

View File

@@ -0,0 +1,18 @@
package app.revanced.patches.googlephotos.misc.backup
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused")
val enableDCIMFoldersBackupControlPatch = bytecodePatch(
name = "Enable DCIM folders backup control",
description = "Disables always on backup for the Camera and other DCIM folders, allowing you to control backup " +
"for each folder individually. This will make the app default to having no folders backed up.",
use = false,
) {
compatibleWith("com.google.android.apps.photos")
execute {
isDCIMFolderBackupControlDisabled.method.returnEarly(false)
}
}

View File

@@ -0,0 +1,8 @@
package app.revanced.patches.googlephotos.misc.backup
import app.revanced.patcher.fingerprint
internal val isDCIMFolderBackupControlDisabled = fingerprint {
returns("Z")
strings("/dcim", "/mars_files/")
}

View File

@@ -1,7 +1,7 @@
package app.revanced.patches.idaustria.detection.signature
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused")
val spoofSignaturePatch = bytecodePatch(
@@ -24,12 +24,6 @@ val spoofSignaturePatch = bytecodePatch(
"77ef1be61b2c01ebdabddcbf53cc4b6fd9a3c445606ee77b3758162c80ad8f8137b3c6864e92db904807dcb2be9d7717dd21" +
"bf42c121d620ddfb7914f7a95c713d9e1c1b7bdb4a03d618e40cf7e9e235c0b5687e03b7ab3,publicExponent=10001}"
spoofSignatureFingerprint.method.addInstructions(
0,
"""
const-string v0, "$expectedSignature"
return-object v0
""",
)
spoofSignatureFingerprint.method.returnEarly(expectedSignature)
}
}

View File

@@ -1,9 +1,16 @@
package app.revanced.patches.instagram.ads
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.meta.ads.adInjectorFingerprint
import app.revanced.util.returnEarly
@Deprecated("Patch was moved to different package: app.revanced.patches.meta.ads.hideAdsPatch")
@Suppress("unused")
val hideAdsPatch = bytecodePatch {
dependsOn(app.revanced.patches.meta.ads.hideAdsPatch)
val hideAdsPatch = bytecodePatch(
name = "Hide ads",
) {
compatibleWith("com.instagram.android")
execute {
adInjectorFingerprint.method.returnEarly(false)
}
}

View File

@@ -0,0 +1,10 @@
package app.revanced.patches.messenger.layout
import app.revanced.patcher.fingerprint
internal val isFacebookButtonEnabledFingerprint = fingerprint {
parameters()
returns("Z")
strings("com.facebook.messaging.inbox.tab.plugins.core.tabtoolbarbutton." +
"facebookbutton.facebooktoolbarbutton.FacebookButtonTabButtonImplementation")
}

View File

@@ -0,0 +1,16 @@
package app.revanced.patches.messenger.layout
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused")
val hideFacebookButtonPatch = bytecodePatch(
name = "Hide Facebook button",
description = "Hides the Facebook button in the top toolbar."
) {
compatibleWith("com.facebook.orca")
execute {
isFacebookButtonEnabledFingerprint.method.returnEarly(false)
}
}

View File

@@ -6,9 +6,20 @@ import app.revanced.patcher.fingerprint
internal val getMobileConfigBoolFingerprint = fingerprint {
parameters("J")
returns("Z")
opcodes(Opcode.RETURN)
custom { method, classDef ->
method.implementation ?: return@custom false // unsure if this is necessary
opcodes(Opcode.RETURN)
custom { _, classDef ->
classDef.interfaces.contains("Lcom/facebook/mobileconfig/factory/MobileConfigUnsafeContext;")
}
}
}
internal val metaAIKillSwitchCheckFingerprint = fingerprint {
strings("SearchAiagentImplementationsKillSwitch")
opcodes(Opcode.CONST_WIDE)
}
internal val extensionMethodFingerprint = fingerprint {
strings("REPLACED_BY_PATCH")
custom { method, classDef ->
method.name == EXTENSION_METHOD_NAME && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}

View File

@@ -2,11 +2,14 @@ package app.revanced.patches.messenger.metaai
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.messenger.misc.extension.sharedExtensionPatch
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/messenger/metaai/RemoveMetaAIPatch;"
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/messenger/metaai/RemoveMetaAIPatch;"
internal const val EXTENSION_METHOD_NAME = "overrideBooleanFlag"
@Suppress("unused")
val removeMetaAIPatch = bytecodePatch(
@@ -25,10 +28,25 @@ val removeMetaAIPatch = bytecodePatch(
addInstructions(
returnIndex,
"""
invoke-static { p1, p2, v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideConfigBool(JZ)Z
invoke-static { p1, p2, v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->$EXTENSION_METHOD_NAME(JZ)Z
move-result v$returnRegister
"""
)
}
// Extract the common starting digits of Meta AI flag IDs from a flag found in code.
val relevantDigits = with(metaAIKillSwitchCheckFingerprint) {
method.getInstruction<WideLiteralInstruction>(patternMatch!!.startIndex).wideLiteral
}.toString().substring(0, 7)
// Replace placeholder in the extension method.
with(extensionMethodFingerprint) {
method.replaceInstruction(
stringMatches!!.first().index,
"""
const-string v1, "$relevantDigits"
"""
)
}
}
}
}

View File

@@ -2,4 +2,4 @@ package app.revanced.patches.messenger.misc.extension
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch("messenger", mainActivityOnCreateHook)
val sharedExtensionPatch = sharedExtensionPatch("messenger", messengerApplicationOnCreateHook)

View File

@@ -2,6 +2,8 @@ package app.revanced.patches.messenger.misc.extension
import app.revanced.patches.shared.misc.extension.extensionHook
internal val mainActivityOnCreateHook = extensionHook {
strings("MainActivity_onCreate_begin")
internal val messengerApplicationOnCreateHook = extensionHook {
custom { method, classDef ->
method.name == "onCreate" && classDef.endsWith("/MessengerApplication;")
}
}

View File

@@ -3,18 +3,9 @@ package app.revanced.patches.meta.ads
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Deprecated("Instead use the Instagram or Threads specific hide ads patch")
@Suppress("unused")
val hideAdsPatch = bytecodePatch(
name = "Hide ads",
) {
/**
* Patch is identical for both Instagram and Threads app.
*/
compatibleWith(
"com.instagram.android",
"com.instagram.barcelona",
)
val hideAdsPatch = bytecodePatch {
execute {
adInjectorFingerprint.method.returnEarly(false)
}

View File

@@ -1,7 +1,7 @@
package app.revanced.patches.nunl.firebase
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused")
val spoofCertificatePatch = bytecodePatch(
@@ -12,13 +12,7 @@ val spoofCertificatePatch = bytecodePatch(
execute {
getFingerprintHashForPackageFingerprints.forEach { fingerprint ->
fingerprint.method.addInstructions(
0,
"""
const-string v0, "eae41fc018df2731a9b6ae1ac327da44a288667b"
return-object v0
""",
)
fingerprint.method.returnEarly("eae41fc018df2731a9b6ae1ac327da44a288667b")
}
}
}

View File

@@ -1,12 +1,7 @@
package app.revanced.patches.pandora.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.pandora.shared.constructUserDataFingerprint
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import app.revanced.util.returnEarly
@Suppress("unused")
val disableAudioAdsPatch = bytecodePatch(
@@ -15,16 +10,7 @@ val disableAudioAdsPatch = bytecodePatch(
compatibleWith("com.pandora.android")
execute {
constructUserDataFingerprint.method.apply {
// First match is "hasAudioAds".
val hasAudioAdsStringIndex = constructUserDataFingerprint.stringMatches!!.first().index
val moveResultIndex = indexOfFirstInstructionOrThrow(hasAudioAdsStringIndex, Opcode.MOVE_RESULT)
val hasAudioAdsRegister = getInstruction<OneRegisterInstruction>(moveResultIndex).registerA
addInstruction(
moveResultIndex + 1,
"const/4 v$hasAudioAdsRegister, 0"
)
}
getIsAdSupportedFingerprint.method.returnEarly(false)
requestAudioAdFingerprint.method.returnEarly()
}
}

View File

@@ -0,0 +1,15 @@
package app.revanced.patches.pandora.ads
import app.revanced.patcher.fingerprint
internal val getIsAdSupportedFingerprint = fingerprint {
custom { method, classDef ->
method.name == "getIsAdSupported" && classDef.endsWith("UserData;")
}
}
internal val requestAudioAdFingerprint = fingerprint {
custom { method, classDef ->
method.name == "requestAudioAdFromAdSDK" && classDef.endsWith("ContentServiceOpsImpl;")
}
}

View File

@@ -1,12 +1,7 @@
package app.revanced.patches.pandora.misc
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.pandora.shared.constructUserDataFingerprint
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import app.revanced.util.returnEarly
@Suppress("unused")
val enableUnlimitedSkipsPatch = bytecodePatch(
@@ -15,17 +10,6 @@ val enableUnlimitedSkipsPatch = bytecodePatch(
compatibleWith("com.pandora.android")
execute {
constructUserDataFingerprint.method.apply {
// Last match is "skipLimitBehavior".
val skipLimitBehaviorStringIndex = constructUserDataFingerprint.stringMatches!!.last().index
val moveResultObjectIndex =
indexOfFirstInstructionOrThrow(skipLimitBehaviorStringIndex, Opcode.MOVE_RESULT_OBJECT)
val skipLimitBehaviorRegister = getInstruction<OneRegisterInstruction>(moveResultObjectIndex).registerA
addInstruction(
moveResultObjectIndex + 1,
"const-string v$skipLimitBehaviorRegister, \"unlimited\""
)
}
skipLimitBehaviorFingerprint.method.returnEarly("unlimited")
}
}

View File

@@ -0,0 +1,9 @@
package app.revanced.patches.pandora.misc
import app.revanced.patcher.fingerprint
internal val skipLimitBehaviorFingerprint = fingerprint {
custom { method, classDef ->
method.name == "getSkipLimitBehavior" && classDef.endsWith("UserData;")
}
}

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