Compare commits

...

185 Commits

Author SHA1 Message Date
semantic-release-bot
87eaf61ef1 chore: Release v5.2.4-dev.1 [skip ci]
## [5.2.4-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.3...v5.2.4-dev.1) (2024-12-07)

### Bug Fixes

* **Twitter:** Merge correct extension by depending on correct extension patch ([35594d0](35594d0a20))
2024-12-07 05:25:21 +00:00
oSumAtrIX
35594d0a20 fix(Twitter): Merge correct extension by depending on correct extension patch 2024-12-07 06:23:25 +01:00
semantic-release-bot
e3c54d8a64 chore: Release v5.2.3 [skip ci]
## [5.2.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3) (2024-12-06)

### Bug Fixes

* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([decd3fc](decd3fcb47))
2024-12-06 22:57:11 +00:00
oSumAtrIX
06202c8807 chore: Merge branch dev to main (#4057) 2024-12-06 23:54:36 +01:00
semantic-release-bot
53efe10222 chore: Release v5.2.3-dev.1 [skip ci]
## [5.2.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3-dev.1) (2024-12-06)

### Bug Fixes

* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([decd3fc](decd3fcb47))
2024-12-06 21:17:13 +00:00
LisoUseInAIKyrios
decd3fcb47 fix(YouTube Music - GmsCore support): Resolve patching errors (#4056) 2024-12-06 22:14:22 +01:00
semantic-release-bot
c7692d7561 chore: Release v5.2.2 [skip ci]
## [5.2.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2) (2024-12-06)

### Bug Fixes

* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([3015993](3015993f55))
2024-12-06 07:37:53 +00:00
oSumAtrIX
73c7c8c93a chore: Merge branch dev to main (#4041) 2024-12-06 08:33:46 +01:00
semantic-release-bot
3a4a124f0b chore: Release v5.2.2-dev.1 [skip ci]
## [5.2.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2-dev.1) (2024-12-05)

### Bug Fixes

* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([3015993](3015993f55))
2024-12-05 15:45:47 +00:00
LisoUseInAIKyrios
3015993f55 fix(YouTube - Spoof video streams): Use system language as default iOS audio stream (#4042) 2024-12-05 19:42:08 +04:00
oSumAtrIX
e04c681424 chore: Separate extensions by app (#3905) 2024-12-05 15:12:48 +04:00
semantic-release-bot
de492de77d chore: Release v5.2.1 [skip ci]
## [5.2.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.0...v5.2.1) (2024-12-04)

### Bug Fixes

* **Twitch:** Resolve setting menu crashes ([#4025](https://github.com/ReVanced/revanced-patches/issues/4025)) ([78390a8](78390a8bca))
* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4014](https://github.com/ReVanced/revanced-patches/issues/4014)) ([f74fd71](f74fd7113f))
* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4019](https://github.com/ReVanced/revanced-patches/issues/4019)) ([57a8e47](57a8e47041))
* **YouTube Music - Hide category bar:** Add support for latest release ([#3968](https://github.com/ReVanced/revanced-patches/issues/3968)) ([9bcde94](9bcde94724))

### Performance Improvements

* Move variables to local scope ([4ee70e3](4ee70e3869))
2024-12-04 19:36:38 +00:00
oSumAtrIX
fc5dcbd13c chore: Merge branch dev to main (#4016) 2024-12-04 20:32:56 +01:00
github-actions[bot]
91a5c95f9a chore: Sync translations (#4039) 2024-12-04 22:53:19 +04:00
semantic-release-bot
a7aa8de6a8 chore: Release v5.2.1-dev.5 [skip ci]
## [5.2.1-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.4...v5.2.1-dev.5) (2024-12-01)

### Performance Improvements

* Move variables to local scope ([4ee70e3](4ee70e3869))
2024-12-01 08:28:06 +00:00
oSumAtrIX
4ee70e3869 perf: Move variables to local scope 2024-12-01 09:25:30 +01:00
LisoUseInAIKyrios
c912a662ab refactor(YouTube): Change fingerprints to support a wider range of target versions (#4026) 2024-11-30 19:55:15 +04:00
semantic-release-bot
d3b3262a31 chore: Release v5.2.1-dev.4 [skip ci]
## [5.2.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.3...v5.2.1-dev.4) (2024-11-30)

### Bug Fixes

* **Twitch:** Resolve setting menu crashes ([#4025](https://github.com/ReVanced/revanced-patches/issues/4025)) ([78390a8](78390a8bca))
2024-11-30 08:56:25 +00:00
LisoUseInAIKyrios
78390a8bca fix(Twitch): Resolve setting menu crashes (#4025) 2024-11-30 12:53:08 +04:00
semantic-release-bot
85bfa4ca91 chore: Release v5.2.1-dev.3 [skip ci]
## [5.2.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.2...v5.2.1-dev.3) (2024-11-29)

### Bug Fixes

* **YouTube Music - Hide category bar:** Add support for latest release ([#3968](https://github.com/ReVanced/revanced-patches/issues/3968)) ([9bcde94](9bcde94724))
2024-11-29 06:38:16 +00:00
FullerBread2032
9bcde94724 fix(YouTube Music - Hide category bar): Add support for latest release (#3968) 2024-11-29 10:35:19 +04:00
LisoUseInAIKyrios
0cfd8e6760 chore: Remove 19.25 and 19.34 compatibility target since the lowest spoof target is 19.35 2024-11-28 20:16:28 +04:00
semantic-release-bot
3265372035 chore: Release v5.2.1-dev.2 [skip ci]
## [5.2.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.1...v5.2.1-dev.2) (2024-11-28)

### Bug Fixes

* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4019](https://github.com/ReVanced/revanced-patches/issues/4019)) ([57a8e47](57a8e47041))
2024-11-28 16:08:33 +00:00
LisoUseInAIKyrios
57a8e47041 fix(YouTube - Spoof app version): Update spoof target to resolve library tab crashes (#4019) 2024-11-28 20:05:10 +04:00
github-actions[bot]
cd476c1227 chore: Sync translations (#4017) 2024-11-28 14:51:16 +04:00
semantic-release-bot
064be93ee2 chore: Release v5.2.1-dev.1 [skip ci]
## [5.2.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.0...v5.2.1-dev.1) (2024-11-28)

### Bug Fixes

* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4014](https://github.com/ReVanced/revanced-patches/issues/4014)) ([f74fd71](f74fd7113f))
2024-11-28 10:51:01 +00:00
LisoUseInAIKyrios
f74fd7113f fix(YouTube - Spoof app version): Update spoof target to resolve library tab crashes (#4014) 2024-11-28 14:47:57 +04:00
github-actions[bot]
628afc22bc chore: Sync translations (#4015) 2024-11-28 14:44:17 +04:00
semantic-release-bot
8686bd9f20 chore: Release v5.2.0 [skip ci]
# [5.2.0](https://github.com/ReVanced/revanced-patches/compare/v5.1.0...v5.2.0) (2024-11-27)

### Bug Fixes

* **My Expenses - Unlock pro:** Constrain compatible version to working version ([#3974](https://github.com/ReVanced/revanced-patches/issues/3974)) ([abcaa63](abcaa6336a))
* **YouTube - Hide Shorts components:** Add missing options to patch ([736b6a9](736b6a96b8))
* **YouTube - Playback speed:** Allow long press 2x speed when using custom playback speeds ([#3990](https://github.com/ReVanced/revanced-patches/issues/3990)) ([fafed09](fafed099c5))
* **YouTube - Settings:** Do not clip settings menus when using an Android 15 device ([#3999](https://github.com/ReVanced/revanced-patches/issues/3999)) ([e33082f](e33082f765))
* **YouTube - Settings:** Show navigation back button in setting sub menus ([#3991](https://github.com/ReVanced/revanced-patches/issues/3991)) ([5c3c684](5c3c68406e))
* **YouTube - Spoof video streams:** Log out the iOS client to restore kids videos playback ([#4000](https://github.com/ReVanced/revanced-patches/issues/4000)) ([fe15213](fe15213cf9))

### Features

* **TikTok:** Add ReVanced settings about screen ([#4009](https://github.com/ReVanced/revanced-patches/issues/4009)) ([046bd3e](046bd3ec88))
* **VSCO:** Remove non functional `Unlock pro` patch ([1a910a2](1a910a2cf6))
* **YouTube - Theme:** Apply custom seekbar color to splash screen animation ([#3978](https://github.com/ReVanced/revanced-patches/issues/3978)) ([7f67759](7f6775950e))
* **YouTube:** Support version `19.46.42` ([#4010](https://github.com/ReVanced/revanced-patches/issues/4010)) ([02732ab](02732ab432))
2024-11-27 13:57:41 +00:00
oSumAtrIX
534996f251 chore: Merge branch dev to main (#3980) 2024-11-27 14:54:10 +01:00
semantic-release-bot
ca4a16dbd8 chore: Release v5.2.0-dev.7 [skip ci]
# [5.2.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.6...v5.2.0-dev.7) (2024-11-27)

### Bug Fixes

* **YouTube - Settings:** Do not clip settings menus when using an Android 15 device ([#3999](https://github.com/ReVanced/revanced-patches/issues/3999)) ([e33082f](e33082f765))
2024-11-27 13:46:42 +00:00
LisoUseInAIKyrios
e33082f765 fix(YouTube - Settings): Do not clip settings menus when using an Android 15 device (#3999) 2024-11-27 17:43:29 +04:00
github-actions[bot]
18360464a9 chore: Sync translations (#4011) 2024-11-27 17:40:12 +04:00
semantic-release-bot
968e6e9b69 chore: Release v5.2.0-dev.6 [skip ci]
# [5.2.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.5...v5.2.0-dev.6) (2024-11-27)

### Features

* **YouTube:** Support version `19.46.42` ([#4010](https://github.com/ReVanced/revanced-patches/issues/4010)) ([02732ab](02732ab432))
2024-11-27 13:39:40 +00:00
LisoUseInAIKyrios
02732ab432 feat(YouTube): Support version 19.46.42 (#4010) 2024-11-27 17:35:59 +04:00
semantic-release-bot
77aea074a9 chore: Release v5.2.0-dev.5 [skip ci]
# [5.2.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.4...v5.2.0-dev.5) (2024-11-27)

### Bug Fixes

* **YouTube - Spoof video streams:** Log out the iOS client to restore kids videos playback ([#4000](https://github.com/ReVanced/revanced-patches/issues/4000)) ([fe15213](fe15213cf9))

### Features

* **TikTok:** Add ReVanced settings about screen ([#4009](https://github.com/ReVanced/revanced-patches/issues/4009)) ([046bd3e](046bd3ec88))
2024-11-27 13:35:27 +00:00
oSumAtrIX
fe15213cf9 fix(YouTube - Spoof video streams): Log out the iOS client to restore kids videos playback (#4000) 2024-11-27 14:31:52 +01:00
LisoUseInAIKyrios
046bd3ec88 feat(TikTok): Add ReVanced settings about screen (#4009) 2024-11-27 17:30:55 +04:00
LisoUseInAIKyrios
d6bc998365 chore: Fix spoof version setting migration
Previously this failed because of cyclic initialization of the settings and the spoof version patch
2024-11-27 13:10:22 +04:00
semantic-release-bot
545e16913a chore: Release v5.2.0-dev.4 [skip ci]
# [5.2.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.3...v5.2.0-dev.4) (2024-11-26)

### Bug Fixes

* **YouTube - Playback speed:** Allow long press 2x speed when using custom playback speeds ([#3990](https://github.com/ReVanced/revanced-patches/issues/3990)) ([fafed09](fafed099c5))
2024-11-26 21:16:15 +00:00
LisoUseInAIKyrios
fafed099c5 fix(YouTube - Playback speed): Allow long press 2x speed when using custom playback speeds (#3990) 2024-11-26 22:12:46 +01:00
semantic-release-bot
a65bbebfdb chore: Release v5.2.0-dev.3 [skip ci]
# [5.2.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.2...v5.2.0-dev.3) (2024-11-26)

### Features

* **VSCO:** Remove non functional `Unlock pro` patch ([1a910a2](1a910a2cf6))
2024-11-26 21:06:29 +00:00
oSumAtrIX
1a910a2cf6 feat(VSCO): Remove non functional Unlock pro patch 2024-11-26 22:03:31 +01:00
semantic-release-bot
6d23a4e000 chore: Release v5.2.0-dev.2 [skip ci]
# [5.2.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.1...v5.2.0-dev.2) (2024-11-26)

### Bug Fixes

* **YouTube - Settings:** Show navigation back button in setting sub menus ([#3991](https://github.com/ReVanced/revanced-patches/issues/3991)) ([5c3c684](5c3c68406e))
2024-11-26 16:49:10 +00:00
LisoUseInAIKyrios
5c3c68406e fix(YouTube - Settings): Show navigation back button in setting sub menus (#3991) 2024-11-26 20:45:48 +04:00
github-actions[bot]
b0c3709be7 chore: Sync translations (#3993) 2024-11-26 20:45:18 +04:00
LisoUseInAIKyrios
cd19f976e7 chore: Fix redundant patch description 2024-11-26 16:47:36 +04:00
semantic-release-bot
c181135cc1 chore: Release v5.2.0-dev.1 [skip ci]
# [5.2.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.1.1-dev.2...v5.2.0-dev.1) (2024-11-25)

### Features

* **YouTube - Theme:** Apply custom seekbar color to splash screen animation ([#3978](https://github.com/ReVanced/revanced-patches/issues/3978)) ([7f67759](7f6775950e))
2024-11-25 16:52:34 +00:00
LisoUseInAIKyrios
7f6775950e feat(YouTube - Theme): Apply custom seekbar color to splash screen animation (#3978) 2024-11-25 20:49:05 +04:00
github-actions[bot]
4b2abaf17e chore: Sync translations (#3985) 2024-11-25 20:47:23 +04:00
semantic-release-bot
677b18c41a chore: Release v5.1.1-dev.2 [skip ci]
## [5.1.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.1.1-dev.1...v5.1.1-dev.2) (2024-11-25)

### Bug Fixes

* **YouTube - Hide Shorts components:** Add missing options to patch ([736b6a9](736b6a96b8))
2024-11-25 15:43:26 +00:00
oSumAtrIX
736b6a96b8 fix(YouTube - Hide Shorts components): Add missing options to patch 2024-11-25 16:40:06 +01:00
semantic-release-bot
8c371d8579 chore: Release v5.1.1-dev.1 [skip ci]
## [5.1.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.1.0...v5.1.1-dev.1) (2024-11-24)

### Bug Fixes

* **My Expenses - Unlock pro:** Constrain compatible version to working version ([#3974](https://github.com/ReVanced/revanced-patches/issues/3974)) ([abcaa63](abcaa6336a))
2024-11-24 22:49:39 +00:00
LisoUseInAIKyrios
abcaa6336a fix(My Expenses - Unlock pro): Constrain compatible version to working version (#3974) 2024-11-24 23:47:15 +01:00
semantic-release-bot
11537526a4 chore: Release v5.1.0 [skip ci]
# [5.1.0](https://github.com/ReVanced/revanced-patches/compare/v5.0.2...v5.1.0) (2024-11-24)

### Bug Fixes

* **YouTube - Change header:** Apply header changes to A/B layout ([#3907](https://github.com/ReVanced/revanced-patches/issues/3907)) ([69c504c](69c504ca2f))
* **YouTube - Hide Shorts components:** Do not hide Shorts action buttons on app first launch ([#3933](https://github.com/ReVanced/revanced-patches/issues/3933)) ([c3701c4](c3701c4b6e))
* **YouTube - Playback speed:** Add 'Auto' speed. Always override speed if default is set to 1.0x ([#3914](https://github.com/ReVanced/revanced-patches/issues/3914)) ([78f3fd6](78f3fd6aa4))
* **YouTube - SponsorBlock:** Fix create new segment crash on tablet custom roms ([#3946](https://github.com/ReVanced/revanced-patches/issues/3946)) ([a7fc08a](a7fc08a491))
* **YouTube - Spoof app version:** Adjust legacy spoof targets ([#3934](https://github.com/ReVanced/revanced-patches/issues/3934)) ([2e3b3dc](2e3b3dca4b))
* **YouTube - Spoof app version:** Remove broken spoof targets when patching 19.25+ ([#3915](https://github.com/ReVanced/revanced-patches/issues/3915)) ([17b5b2e](17b5b2e384))

### Features

* **YouTube - Miniplayer:** Add option to disable miniplayer ([#3961](https://github.com/ReVanced/revanced-patches/issues/3961)) ([01cc8e0](01cc8e0abf))
* **YouTube:** Support version `19.45.38` ([#3938](https://github.com/ReVanced/revanced-patches/issues/3938)) ([8c6c8e0](8c6c8e0442))
2024-11-24 04:53:48 +00:00
oSumAtrIX
403116f591 chore: Merge branch dev to main (#3909) 2024-11-24 05:49:58 +01:00
1fexd
a1d14cffe9 chore: Move Sync video downloads patch to correct package 2024-11-24 04:06:02 +04:00
semantic-release-bot
10f221f374 chore: Release v5.1.0-dev.3 [skip ci]
# [5.1.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.1.0-dev.2...v5.1.0-dev.3) (2024-11-22)

### Features

* **YouTube - Miniplayer:** Add option to disable miniplayer ([#3961](https://github.com/ReVanced/revanced-patches/issues/3961)) ([01cc8e0](01cc8e0abf))
2024-11-22 16:44:41 +00:00
github-actions[bot]
ba1aab6d4d chore: Sync translations (#3962) 2024-11-22 20:41:24 +04:00
LisoUseInAIKyrios
01cc8e0abf feat(YouTube - Miniplayer): Add option to disable miniplayer (#3961)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-11-22 20:37:35 +04:00
LisoUseInAIKyrios
518958350d refactor(YouTube - Announcements): Do not include announcement setting with import/export 2024-11-21 18:48:06 +04:00
semantic-release-bot
a625309d1f chore: Release v5.1.0-dev.2 [skip ci]
# [5.1.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.1.0-dev.1...v5.1.0-dev.2) (2024-11-21)

### Bug Fixes

* **YouTube - SponsorBlock:** Fix create new segment crash on tablet custom roms ([#3946](https://github.com/ReVanced/revanced-patches/issues/3946)) ([a7fc08a](a7fc08a491))
2024-11-21 05:47:29 +00:00
LisoUseInAIKyrios
a7fc08a491 fix(YouTube - SponsorBlock): Fix create new segment crash on tablet custom roms (#3946) 2024-11-21 09:44:32 +04:00
semantic-release-bot
97b129e088 chore: Release v5.1.0-dev.1 [skip ci]
# [5.1.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.5...v5.1.0-dev.1) (2024-11-20)

### Features

* **YouTube:** Support version `19.45.38` ([#3938](https://github.com/ReVanced/revanced-patches/issues/3938)) ([8c6c8e0](8c6c8e0442))
2024-11-20 09:54:50 +00:00
LisoUseInAIKyrios
8c6c8e0442 feat(YouTube): Support version 19.45.38 (#3938) 2024-11-20 13:51:36 +04:00
LisoUseInAIKyrios
16c090d2c0 refactor(YouTube): Use consistent language for 'auto' speed and quality 2024-11-19 11:07:49 +04:00
semantic-release-bot
ed35a2a4a9 chore: Release v5.0.3-dev.5 [skip ci]
## [5.0.3-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.4...v5.0.3-dev.5) (2024-11-18)

### Bug Fixes

* **YouTube - Hide Shorts components:** Do not hide Shorts action buttons on app first launch ([#3933](https://github.com/ReVanced/revanced-patches/issues/3933)) ([c3701c4](c3701c4b6e))
2024-11-18 08:28:49 +00:00
LisoUseInAIKyrios
c3701c4b6e fix(YouTube - Hide Shorts components): Do not hide Shorts action buttons on app first launch (#3933) 2024-11-18 12:25:32 +04:00
github-actions[bot]
e0dc821c50 chore: Sync translations (#3936) 2024-11-18 12:21:46 +04:00
semantic-release-bot
b9efb05271 chore: Release v5.0.3-dev.4 [skip ci]
## [5.0.3-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.3...v5.0.3-dev.4) (2024-11-18)

### Bug Fixes

* **YouTube - Spoof app version:** Adjust legacy spoof targets ([#3934](https://github.com/ReVanced/revanced-patches/issues/3934)) ([2e3b3dc](2e3b3dca4b))
2024-11-18 08:20:28 +00:00
LisoUseInAIKyrios
2e3b3dca4b fix(YouTube - Spoof app version): Adjust legacy spoof targets (#3934) 2024-11-18 12:17:22 +04:00
semantic-release-bot
19eaee09d0 chore: Release v5.0.3-dev.3 [skip ci]
## [5.0.3-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.2...v5.0.3-dev.3) (2024-11-15)

### Bug Fixes

* **YouTube - Playback speed:** Add 'Auto' speed. Always override speed if default is set to 1.0x ([#3914](https://github.com/ReVanced/revanced-patches/issues/3914)) ([78f3fd6](78f3fd6aa4))
2024-11-15 03:59:51 +00:00
LisoUseInAIKyrios
78f3fd6aa4 fix(YouTube - Playback speed): Add 'Auto' speed. Always override speed if default is set to 1.0x (#3914) 2024-11-15 07:56:36 +04:00
semantic-release-bot
71ed37beb1 chore: Release v5.0.3-dev.2 [skip ci]
## [5.0.3-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.1...v5.0.3-dev.2) (2024-11-15)

### Bug Fixes

* **YouTube - Spoof app version:** Remove broken spoof targets when patching 19.25+ ([#3915](https://github.com/ReVanced/revanced-patches/issues/3915)) ([17b5b2e](17b5b2e384))
2024-11-15 03:51:14 +00:00
github-actions[bot]
5aae234c43 chore: Sync translations (#3920) 2024-11-15 07:47:49 +04:00
LisoUseInAIKyrios
17b5b2e384 fix(YouTube - Spoof app version): Remove broken spoof targets when patching 19.25+ (#3915) 2024-11-15 07:44:28 +04:00
LisoUseInAIKyrios
462b61c2e9 refactor(YouTube - Hide mix playlist): Do not search path or buffer unless setting is enabled 2024-11-14 01:37:14 +04:00
semantic-release-bot
f23b7fffc8 chore: Release v5.0.3-dev.1 [skip ci]
## [5.0.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.2...v5.0.3-dev.1) (2024-11-13)

### Bug Fixes

* **YouTube - Change header:** Apply header changes to A/B layout ([#3907](https://github.com/ReVanced/revanced-patches/issues/3907)) ([69c504c](69c504ca2f))
2024-11-13 13:15:15 +00:00
LisoUseInAIKyrios
69c504ca2f fix(YouTube - Change header): Apply header changes to A/B layout (#3907) 2024-11-13 17:12:26 +04:00
semantic-release-bot
fc4b0d7c39 chore: Release v5.0.2 [skip ci]
## [5.0.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.1...v5.0.2) (2024-11-12)

### Bug Fixes

* **Sync for Reddit - Fix /s/ links:** Fix patch by using correct fingerprints ([68ec011](68ec011003))
* **Sync for Reddit - Spoof client:** Fix patch by using correct fingerprints ([273bedc](273bedc74c))
* **YouTube - Player controls:** Show player control buttons with A/B layout ([#3901](https://github.com/ReVanced/revanced-patches/issues/3901)) ([e441745](e4417455c9))
2024-11-12 13:47:36 +00:00
oSumAtrIX
02e66b3d43 chore: Merge branch dev to main (#3899) 2024-11-12 14:44:18 +01:00
semantic-release-bot
a75c15b950 chore: Release v5.0.2-dev.2 [skip ci]
## [5.0.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.2-dev.1...v5.0.2-dev.2) (2024-11-12)

### Bug Fixes

* **YouTube - Player controls:** Show player control buttons with A/B layout ([#3901](https://github.com/ReVanced/revanced-patches/issues/3901)) ([e441745](e4417455c9))
2024-11-12 06:10:45 +00:00
LisoUseInAIKyrios
e4417455c9 fix(YouTube - Player controls): Show player control buttons with A/B layout (#3901) 2024-11-12 10:07:32 +04:00
semantic-release-bot
5253f4bfa4 chore: Release v5.0.2-dev.1 [skip ci]
## [5.0.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.1...v5.0.2-dev.1) (2024-11-11)

### Bug Fixes

* **Sync for Reddit - Fix /s/ links:** Fix patch by using correct fingerprints ([68ec011](68ec011003))
* **Sync for Reddit - Spoof client:** Fix patch by using correct fingerprints ([273bedc](273bedc74c))
2024-11-11 23:57:42 +00:00
oSumAtrIX
273bedc74c fix(Sync for Reddit - Spoof client): Fix patch by using correct fingerprints 2024-11-12 00:55:01 +01:00
oSumAtrIX
68ec011003 fix(Sync for Reddit - Fix /s/ links): Fix patch by using correct fingerprints 2024-11-12 00:44:54 +01:00
semantic-release-bot
f3d1103287 chore: Release v5.0.1 [skip ci]
## [5.0.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.0...v5.0.1) (2024-11-11)

### Bug Fixes

* **Sync:** Fix patches by not throwing unnecessarily ([3059aca](3059aca69d))
* **Tiktok - Settings:** Fix the patch by depending on the correct settings patch ([2094a23](2094a23ccc))
* **Twitter:** Fix patches by depending on patch that merges required extension ([3e1b5cb](3e1b5cbaf5))
* **Twitter:** Fix patches by matching fingerprint using correct class ([3793b21](3793b2103c))
* **YouTube - Playback speed:** Remember playback speed when using non 1.0x default speed ([d881d8b](d881d8bc44)), closes [#3810](https://github.com/ReVanced/revanced-patches/issues/3810)

### Performance Improvements

* Check for extension without a class proxy ([53b6b1f](53b6b1ff41))
2024-11-11 20:42:31 +00:00
oSumAtrIX
50a3541e98 chore: Merge branch dev to main (#3888) 2024-11-11 21:39:55 +01:00
github-actions[bot]
c6069a7ff6 chore: Sync translations (#3897)
Co-authored-by: revanced-bot <github@revanced.app>
2024-11-11 21:39:34 +01:00
semantic-release-bot
b10b624b4b chore: Release v5.0.1-dev.4 [skip ci]
## [5.0.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.3...v5.0.1-dev.4) (2024-11-11)

### Bug Fixes

* **Twitter:** Fix patches by depending on patch that merges required extension ([3e1b5cb](3e1b5cbaf5))
2024-11-11 17:01:35 +00:00
oSumAtrIX
3e1b5cbaf5 fix(Twitter): Fix patches by depending on patch that merges required extension 2024-11-11 17:58:56 +01:00
semantic-release-bot
ef37b78b45 chore: Release v5.0.1-dev.3 [skip ci]
## [5.0.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.2...v5.0.1-dev.3) (2024-11-11)

### Bug Fixes

* **YouTube - Playback speed:** Remember playback speed when using non 1.0x default speed ([d881d8b](d881d8bc44)), closes [#3810](https://github.com/ReVanced/revanced-patches/issues/3810)
2024-11-11 06:32:25 +00:00
LisoUseInAIKyrios
d881d8bc44 fix(YouTube - Playback speed): Remember playback speed when using non 1.0x default speed
This code was previously present with PR #3810 but was accidentally left out during the DSL migration.
2024-11-11 10:29:15 +04:00
semantic-release-bot
0cb993d6ea chore: Release v5.0.1-dev.2 [skip ci]
## [5.0.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.1...v5.0.1-dev.2) (2024-11-11)

### Bug Fixes

* **Twitter:** Fix patches by matching fingerprint using correct class ([3793b21](3793b2103c))
2024-11-11 01:53:03 +00:00
oSumAtrIX
3793b2103c fix(Twitter): Fix patches by matching fingerprint using correct class 2024-11-11 02:50:21 +01:00
semantic-release-bot
658370f035 chore: Release v5.0.1-dev.1 [skip ci]
## [5.0.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.0...v5.0.1-dev.1) (2024-11-11)

### Bug Fixes

* **Sync:** Fix patches by not throwing unnecessarily ([3059aca](3059aca69d))
* **Tiktok - Settings:** Fix the patch by depending on the correct settings patch ([2094a23](2094a23ccc))

### Performance Improvements

* Check for extension without a class proxy ([53b6b1f](53b6b1ff41))
2024-11-11 01:40:33 +00:00
oSumAtrIX
3059aca69d fix(Sync): Fix patches by not throwing unnecessarily 2024-11-11 02:37:44 +01:00
oSumAtrIX
2094a23ccc fix(Tiktok - Settings): Fix the patch by depending on the correct settings patch 2024-11-11 02:33:12 +01:00
oSumAtrIX
53b6b1ff41 perf: Check for extension without a class proxy 2024-11-11 02:25:17 +01:00
semantic-release-bot
5657a7d8c8 chore: Release v5.0.0 [skip ci]
# [5.0.0](https://github.com/ReVanced/revanced-patches/compare/v4.17.0...v5.0.0) (2024-11-10)

### Bug Fixes

* Add missing dependency to patch ([327ebd3](327ebd3649))
* **MyFitnessPal - Hide ads:** Constrain patch to last working version ([#3847](https://github.com/ReVanced/revanced-patches/issues/3847)) ([9c3bec6](9c3bec69d8))
* **Twitter - Change link sharing domain:** Support latest app version ([#3786](https://github.com/ReVanced/revanced-patches/issues/3786)) ([5bdb3a2](5bdb3a2e29))
* **YouTube - Copy video URL:** Support A/B player layout ([c832143](c832143eec))
* **YouTube - Custom branding:** Change icon correctly on 19.34+ ([#3866](https://github.com/ReVanced/revanced-patches/issues/3866)) ([4d1b3fb](4d1b3fba99))
* **YouTube - Hide ads:** Hide new types of ads ([6ceb084](6ceb084831))
* **YouTube - Hide layout components:** Move hide chips settings to Feed menu ([716f0d8](716f0d8d64))
* **YouTube - Hide layout components:** Remove obsolete 'Hide gray separator' ([30a063e](30a063ea12))
* **YouTube - Playback speed:** Remember playback speed with new speed menu ([#3810](https://github.com/ReVanced/revanced-patches/issues/3810)) ([e09b039](e09b03997e))
* **YouTube - Playback speed:** Restore old playback speed menu ([#3817](https://github.com/ReVanced/revanced-patches/issues/3817)) ([1215d16](1215d16a30))
* **YouTube - Remember video quality:** Correctly set default quality when changing from a low quality video ([#3879](https://github.com/ReVanced/revanced-patches/issues/3879)) ([75d661f](75d661fcdc))
* **YouTube - Remove background playback restrictions:** Enable for Shorts as well ([#3671](https://github.com/ReVanced/revanced-patches/issues/3671)) ([a803660](a8036606c1))
* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([a0c227f](a0c227f1a0))
* **YouTube - Return YouTube Dislike:** Use latest separator height ([94adb2e](94adb2eb65))
* **YouTube - Seekbar:** Use latest shade of YouTube red ([e94b216](e94b216e44))
* **YouTube - Settings:** Use multiline preference title for localized languages ([#3821](https://github.com/ReVanced/revanced-patches/issues/3821)) ([2082f14](2082f14f34))
* **YouTube - SponsorBlock:** Show correct segment behavior in settings UI after importing ([53c2f96](53c2f96b6a))
* **YouTube - Spoof app version:** Remove obsolete 17.33.42 spoof target ([#3825](https://github.com/ReVanced/revanced-patches/issues/3825)) ([2165953](2165953a4e))
* **YouTube:** Merge `Restore old seekbar thumbnails` into `Seekbar thumbnails` ([#3860](https://github.com/ReVanced/revanced-patches/issues/3860)) ([042515a](042515a4f0))

### Build System

* Bump ReVanced Patcher ([928cf5f](928cf5f945))

### Features

* **YouTube - Hide layout components:** Hide player shopping shelf ([#3804](https://github.com/ReVanced/revanced-patches/issues/3804)) ([69329df](69329df355))
* **YouTube - Hide player flyout menu items:** Hide stable volume ([#3827](https://github.com/ReVanced/revanced-patches/issues/3827)) ([d3c4811](d3c481166f))
* **YouTube - Miniplayer:** Add horizontal drag gesture ([#3859](https://github.com/ReVanced/revanced-patches/issues/3859)) ([ecf8bd4](ecf8bd445a))
* **YouTube - Player flyout menu:** Hide sleep timer ([#3637](https://github.com/ReVanced/revanced-patches/issues/3637)) ([6348a66](6348a66fef))
* **YouTube:** Add `Seekbar thumbnails` patch ([#3813](https://github.com/ReVanced/revanced-patches/issues/3813)) ([aa2f963](aa2f963e05))
* **YouTube:** Add `Shorts autoplay` patch ([#3794](https://github.com/ReVanced/revanced-patches/issues/3794)) ([132d925](132d925d5c))
* **YouTube:** Hide player shopping shelf in playlists ([#3806](https://github.com/ReVanced/revanced-patches/issues/3806)) ([68d9edf](68d9edfd8c))
* **YouTube:** Merge multiple layout patches into `Hide Layout Components` ([#3799](https://github.com/ReVanced/revanced-patches/issues/3799)) ([24d612d](24d612d0d1))
* **YouTube:** Merge multiple player overlay patches into `Hide player overlay buttons` ([#3800](https://github.com/ReVanced/revanced-patches/issues/3800)) ([cee0641](cee0641247))
* **YouTube:** Support version `19.43.41` ([#3854](https://github.com/ReVanced/revanced-patches/issues/3854)) ([43f5e99](43f5e99e0b))

### BREAKING CHANGES

* Various APIs have been changed or removed.
2024-11-10 14:59:53 +00:00
oSumAtrIX
08ce458e28 chore: Merge branch dev to main (#3798) 2024-11-10 15:56:12 +01:00
github-actions[bot]
b5e4022fbb chore: Sync translations (#3881)
Co-authored-by: revanced-bot <github@revanced.app>
2024-11-10 15:48:08 +01:00
semantic-release-bot
6e75ffd5f1 chore: Release v5.0.0-dev.4 [skip ci]
# [5.0.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.3...v5.0.0-dev.4) (2024-11-09)

### Bug Fixes

* **YouTube - Remember video quality:** Correctly set default quality when changing from a low quality video ([#3879](https://github.com/ReVanced/revanced-patches/issues/3879)) ([75d661f](75d661fcdc))
2024-11-09 18:56:29 +00:00
LisoUseInAIKyrios
75d661fcdc fix(YouTube - Remember video quality): Correctly set default quality when changing from a low quality video (#3879) 2024-11-09 22:53:01 +04:00
semantic-release-bot
4d1de6bc50 chore: Release v5.0.0-dev.3 [skip ci]
# [5.0.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.2...v5.0.0-dev.3) (2024-11-09)

### Bug Fixes

* Add missing dependency to patch ([327ebd3](327ebd3649))
2024-11-09 04:20:45 +00:00
oSumAtrIX
327ebd3649 fix: Add missing dependency to patch 2024-11-09 05:17:06 +01:00
LisoUseInAIKyrios
34e98a54e0 chore(YouTube - Announcements): Remove language parameter of API call (#3874) 2024-11-08 22:11:31 -04:00
semantic-release-bot
bc3c61a6a0 chore: Release v5.0.0-dev.2 [skip ci]
# [5.0.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.1...v5.0.0-dev.2) (2024-11-09)

### Bug Fixes

* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([a0c227f](a0c227f1a0))
2024-11-09 01:21:00 +00:00
LisoUseInAIKyrios
a0c227f1a0 fix(YouTube - Return YouTube Dislike): Show Shorts dislikes with new A/B button icons 2024-11-08 21:17:02 -04:00
LisoUseInAIKyrios
87fcf3135d refactor(YouTube - Theme): Cleanup 2024-11-07 08:27:20 -04:00
LisoUseInAIKyrios
4b4670cd4f refactor(YouTube - Theme): Use more robust gradient theme insert index 2024-11-07 08:20:39 -04:00
semantic-release-bot
fc4a0b929a chore: Release v5.0.0-dev.1 [skip ci]
# [5.0.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.6...v5.0.0-dev.1) (2024-11-06)

### Bug Fixes

* **MyFitnessPal - Hide ads:** Constrain patch to last working version ([#3847](https://github.com/ReVanced/revanced-patches/issues/3847)) ([9c3bec6](9c3bec69d8))
* **YouTube - Copy video URL:** Support A/B player layout ([c832143](c832143eec))
* **YouTube - Custom branding:** Change icon correctly on 19.34+ ([#3866](https://github.com/ReVanced/revanced-patches/issues/3866)) ([4d1b3fb](4d1b3fba99))
* **YouTube - Hide ads:** Hide new types of ads ([6ceb084](6ceb084831))
* **YouTube - Hide layout components:** Remove obsolete 'Hide gray separator' ([30a063e](30a063ea12))
* **YouTube - Playback speed:** Restore old playback speed menu ([#3817](https://github.com/ReVanced/revanced-patches/issues/3817)) ([1215d16](1215d16a30))
* **YouTube - Remove background playback restrictions:** Enable for Shorts as well ([#3671](https://github.com/ReVanced/revanced-patches/issues/3671)) ([a803660](a8036606c1))
* **YouTube - Return YouTube Dislike:** Use latest separator height ([94adb2e](94adb2eb65))
* **YouTube - Seekbar:** Use latest shade of YouTube red ([e94b216](e94b216e44))
* **YouTube - Settings:** Use multiline preference title for localized languages ([#3821](https://github.com/ReVanced/revanced-patches/issues/3821)) ([2082f14](2082f14f34))
* **YouTube - SponsorBlock:** Show correct segment behavior in settings UI after importing ([53c2f96](53c2f96b6a))
* **YouTube - Spoof app version:** Remove obsolete 17.33.42 spoof target ([#3825](https://github.com/ReVanced/revanced-patches/issues/3825)) ([2165953](2165953a4e))
* **YouTube:** Merge `Restore old seekbar thumbnails` into `Seekbar thumbnails` ([#3860](https://github.com/ReVanced/revanced-patches/issues/3860)) ([042515a](042515a4f0))

### Build System

* Bump ReVanced Patcher ([928cf5f](928cf5f945))

### Features

* **YouTube - Hide player flyout menu items:** Hide stable volume ([#3827](https://github.com/ReVanced/revanced-patches/issues/3827)) ([d3c4811](d3c481166f))
* **YouTube - Miniplayer:** Add horizontal drag gesture ([#3859](https://github.com/ReVanced/revanced-patches/issues/3859)) ([ecf8bd4](ecf8bd445a))
* **YouTube - Player flyout menu:** Hide sleep timer ([#3637](https://github.com/ReVanced/revanced-patches/issues/3637)) ([6348a66](6348a66fef))
* **YouTube:** Add `Seekbar thumbnails` patch ([#3813](https://github.com/ReVanced/revanced-patches/issues/3813)) ([aa2f963](aa2f963e05))
* **YouTube:** Support version `19.43.41` ([#3854](https://github.com/ReVanced/revanced-patches/issues/3854)) ([43f5e99](43f5e99e0b))

### BREAKING CHANGES

* Various APIs have been changed or removed.
2024-11-06 16:25:06 +00:00
github-actions[bot]
1d5d837d90 chore: Sync translations (#3871) 2024-11-06 12:20:50 -04:00
oSumAtrIX
27d7636d8e chore(YouTube - Announcements): Use API v4 to get announcements (#3869) 2024-11-06 17:19:16 +01:00
LisoUseInAIKyrios
94adb2eb65 fix(YouTube - Return YouTube Dislike): Use latest separator height 2024-11-06 17:19:16 +01:00
oSumAtrIX
4d1b3fba99 fix(YouTube - Custom branding): Change icon correctly on 19.34+ (#3866) 2024-11-06 17:19:15 +01:00
github-actions[bot]
3306090176 chore: Sync translations (#3863) 2024-11-06 17:19:15 +01:00
oSumAtrIX
df6ea01f0e build: Increase heap size to mitigate OOM 2024-11-06 17:19:14 +01:00
oSumAtrIX
c317baf71d build: Bump ReVanced Patcher (#3862)
Co-authored-by: Ushie <ushiekane@gmail.com>
2024-11-06 17:19:14 +01:00
LisoUseInAIKyrios
ecf8bd445a feat(YouTube - Miniplayer): Add horizontal drag gesture (#3859) 2024-11-06 17:19:14 +01:00
LisoUseInAIKyrios
042515a4f0 fix(YouTube): Merge Restore old seekbar thumbnails into Seekbar thumbnails (#3860)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-11-06 17:19:13 +01:00
LisoUseInAIKyrios
b7b536f1bc refactor(YouTube): Add missing version tags to "any version" bytecode patches 2024-11-06 17:19:13 +01:00
LisoUseInAIKyrios
53c2f96b6a fix(YouTube - SponsorBlock): Show correct segment behavior in settings UI after importing 2024-11-06 17:19:13 +01:00
LisoUseInAIKyrios
48cca5d08f chore: Remove obsolete error toast custom message.
All patches that can encounter network errors now manage their own error toasts.
2024-11-06 17:19:13 +01:00
eiqnepm
9c3bec69d8 fix(MyFitnessPal - Hide ads): Constrain patch to last working version (#3847) 2024-11-06 17:19:12 +01:00
LisoUseInAIKyrios
6ceb084831 fix(YouTube - Hide ads): Hide new types of ads 2024-11-06 17:19:12 +01:00
LisoUseInAIKyrios
43f5e99e0b feat(YouTube): Support version 19.43.41 (#3854) 2024-11-06 17:19:12 +01:00
LisoUseInAIKyrios
c832143eec fix(YouTube - Copy video URL): Support A/B player layout 2024-11-06 17:19:12 +01:00
github-actions[bot]
9ced010568 chore: Sync translations (#3850) 2024-11-06 17:19:11 +01:00
LisoUseInAIKyrios
30a063ea12 fix(YouTube - Hide layout components): Remove obsolete 'Hide gray separator' 2024-11-06 17:19:11 +01:00
LisoUseInAIKyrios
8e5116197c chore: Fix merge fix for 19.34 target 2024-11-06 17:19:11 +01:00
github-actions[bot]
0a3a0058a9 chore: Sync translations (#3834) 2024-11-06 17:19:10 +01:00
LisoUseInAIKyrios
3fbc6973bb chore: Fix merge typo, refactor 2024-11-06 17:19:10 +01:00
LisoUseInAIKyrios
d3c481166f feat(YouTube - Hide player flyout menu items): Hide stable volume (#3827) 2024-11-06 17:19:10 +01:00
LisoUseInAIKyrios
518aab4c71 chore: Fix string declared for the wrong patch 2024-11-06 17:19:10 +01:00
LisoUseInAIKyrios
2165953a4e fix(YouTube - Spoof app version): Remove obsolete 17.33.42 spoof target (#3825) 2024-11-06 17:19:09 +01:00
LisoUseInAIKyrios
cae6975870 chore: Fix merge typo 2024-11-06 17:19:09 +01:00
LisoUseInAIKyrios
e94b216e44 fix(YouTube - Seekbar): Use latest shade of YouTube red 2024-11-06 17:19:09 +01:00
LisoUseInAIKyrios
2082f14f34 fix(YouTube - Settings): Use multiline preference title for localized languages (#3821) 2024-11-06 17:19:08 +01:00
github-actions[bot]
fbce497723 chore: Sync translations (#3820) 2024-11-06 17:19:08 +01:00
oSumAtrIX
b3c53eec20 ci: Update paths for translations CI and remove scheduled pull of strings (#3819) 2024-11-06 17:19:08 +01:00
Zain
a8036606c1 fix(YouTube - Remove background playback restrictions): Enable for Shorts as well (#3671) 2024-11-06 17:19:08 +01:00
Zain
6348a66fef feat(YouTube - Player flyout menu): Hide sleep timer (#3637) 2024-11-06 17:19:07 +01:00
LisoUseInAIKyrios
1215d16a30 fix(YouTube - Playback speed): Restore old playback speed menu (#3817) 2024-11-06 17:19:07 +01:00
LisoUseInAIKyrios
aa2f963e05 feat(YouTube): Add Seekbar thumbnails patch (#3813) 2024-11-06 17:19:07 +01:00
LisoUseInAIKyrios
c2195dcf4a refactor: Simplify extensions register declaration (#3084) 2024-11-06 17:19:07 +01:00
LisoUseInAIKyrios
cd74726ab6 chore: Remove obsolete code (#3650) 2024-11-06 17:19:06 +01:00
oSumAtrIX
55d55db86f chore: Merge integrations 2024-11-06 17:19:06 +01:00
oSumAtrIX
928cf5f945 build: Bump ReVanced Patcher
BREAKING CHANGE: Various APIs have been changed or removed.
2024-11-06 17:19:04 +01:00
semantic-release-bot
abd100f24c chore: Release v4.18.0-dev.6 [skip ci]
# [4.18.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.5...v4.18.0-dev.6) (2024-10-24)

### Bug Fixes

* **YouTube - Playback speed:** Remember playback speed with new speed menu ([#3810](https://github.com/ReVanced/revanced-patches/issues/3810)) ([e09b039](e09b03997e))
2024-10-24 11:48:59 +00:00
LisoUseInAIKyrios
e09b03997e fix(YouTube - Playback speed): Remember playback speed with new speed menu (#3810) 2024-10-24 07:47:01 -04:00
github-actions[bot]
df346c727d chore: Sync translations (#3814) 2024-10-24 07:44:37 -04:00
semantic-release-bot
62bdb53691 chore: Release v4.18.0-dev.5 [skip ci]
# [4.18.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.4...v4.18.0-dev.5) (2024-10-23)

### Features

* **YouTube:** Hide player shopping shelf in playlists ([#3806](https://github.com/ReVanced/revanced-patches/issues/3806)) ([68d9edf](68d9edfd8c))
2024-10-23 03:58:10 +00:00
LisoUseInAIKyrios
68d9edfd8c feat(YouTube): Hide player shopping shelf in playlists (#3806) 2024-10-22 23:56:02 -04:00
github-actions[bot]
834425f720 chore: Sync translations (#3807) 2024-10-22 23:51:52 -04:00
semantic-release-bot
ea00874105 chore: Release v4.18.0-dev.4 [skip ci]
# [4.18.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.3...v4.18.0-dev.4) (2024-10-23)

### Features

* **YouTube - Hide layout components:** Hide player shopping shelf ([#3804](https://github.com/ReVanced/revanced-patches/issues/3804)) ([69329df](69329df355))
2024-10-23 02:15:28 +00:00
LisoUseInAIKyrios
69329df355 feat(YouTube - Hide layout components): Hide player shopping shelf (#3804) 2024-10-23 04:13:24 +02:00
github-actions[bot]
5ecda855b8 chore: Sync translations (#3803) 2024-10-22 20:59:25 -04:00
semantic-release-bot
5c36f7fc4d chore: Release v4.18.0-dev.3 [skip ci]
# [4.18.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.2...v4.18.0-dev.3) (2024-10-22)

### Features

* **YouTube:** Merge multiple layout patches into `Hide Layout Components` ([#3799](https://github.com/ReVanced/revanced-patches/issues/3799)) ([24d612d](24d612d0d1))
* **YouTube:** Merge multiple player overlay patches into `Hide player overlay buttons` ([#3800](https://github.com/ReVanced/revanced-patches/issues/3800)) ([cee0641](cee0641247))
2024-10-22 22:33:09 +00:00
LisoUseInAIKyrios
cee0641247 feat(YouTube): Merge multiple player overlay patches into Hide player overlay buttons (#3800)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-10-23 00:31:07 +02:00
LisoUseInAIKyrios
24d612d0d1 feat(YouTube): Merge multiple layout patches into Hide Layout Components (#3799) 2024-10-23 00:29:23 +02:00
semantic-release-bot
59b648c049 chore: Release v4.18.0-dev.2 [skip ci]
# [4.18.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.1...v4.18.0-dev.2) (2024-10-22)

### Bug Fixes

* **Twitter - Change link sharing domain:** Support latest app version ([#3786](https://github.com/ReVanced/revanced-patches/issues/3786)) ([5bdb3a2](5bdb3a2e29))
2024-10-22 21:05:36 +00:00
Alex
5bdb3a2e29 fix(Twitter - Change link sharing domain): Support latest app version (#3786) 2024-10-22 23:03:33 +02:00
semantic-release-bot
9f6c4c19aa chore: Release v4.18.0-dev.1 [skip ci]
# [4.18.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.17.0...v4.18.0-dev.1) (2024-10-21)

### Bug Fixes

* **YouTube - Hide layout components:** Move hide chips settings to Feed menu ([716f0d8](716f0d8d64))

### Features

* **YouTube:** Add `Shorts autoplay` patch ([#3794](https://github.com/ReVanced/revanced-patches/issues/3794)) ([132d925](132d925d5c))
2024-10-21 06:56:34 +00:00
LisoUseInAIKyrios
716f0d8d64 fix(YouTube - Hide layout components): Move hide chips settings to Feed menu 2024-10-21 02:54:06 -04:00
LisoUseInAIKyrios
132d925d5c feat(YouTube): Add Shorts autoplay patch (#3794) 2024-10-21 02:53:09 -04:00
semantic-release-bot
8d5f92b2fa chore: Release v4.17.0 [skip ci]
# [4.17.0](https://github.com/ReVanced/revanced-patches/compare/v4.16.0...v4.17.0) (2024-10-20)

### Bug Fixes

* **Twitter - Unlock downloads:** Make it work with latest versions ([#3782](https://github.com/ReVanced/revanced-patches/issues/3782)) ([773fd81](773fd81dfd))
* **YouTube - GmsCore support:** Add more replacements ([6965b7d](6965b7d653))
* **YouTube - GmsCore support:** Remove unclear patch changes ([ab10b42](ab10b42388))
* **YouTube - Hide layout components:** Adjust settings text ([#3745](https://github.com/ReVanced/revanced-patches/issues/3745)) ([2f7e1f6](2f7e1f601a))
* **YouTube - Spoof video streams:** Fix playback for Android VR by removing invalid body as well ([#3769](https://github.com/ReVanced/revanced-patches/issues/3769)) ([68b61b4](68b61b4725))

### Features

* **Backdrops - Pro unlock:** Support latest versions by removing version constraint ([98c33f0](98c33f03dd))
* **Facebook:** Add `Hide sponsored stories` patch ([#3627](https://github.com/ReVanced/revanced-patches/issues/3627)) ([fc7644d](fc7644d3b7))
* **Sync for Reddit:** Add `Fix video downloads` patch ([#3739](https://github.com/ReVanced/revanced-patches/issues/3739)) ([38e7884](38e7884d17))
* **Twitter:** Add `Change link sharing domain` patch ([#3753](https://github.com/ReVanced/revanced-patches/issues/3753)) ([0077028](007702825b))
* **Willhaben:** Add `Hide ads` patch ([#3740](https://github.com/ReVanced/revanced-patches/issues/3740)) ([42ed29b](42ed29b4c0))
* **YouTube - Hide layout components:** Add option to hide Yoodles (YouTube Doodles) ([#3743](https://github.com/ReVanced/revanced-patches/issues/3743)) ([0679a47](0679a47b22))
* **YouTube - Hide Shorts components:** Add option to hide `Use template`, `Upcoming`, `Green screen` buttons ([#3752](https://github.com/ReVanced/revanced-patches/issues/3752)) ([d5a1368](d5a13684f6))
* **YouTube - Hide Shorts components:** Add option to hide like fountain ([#3731](https://github.com/ReVanced/revanced-patches/issues/3731)) ([d76f4c9](d76f4c96a4))
* **YouTube - Hide Shorts components:** Hide `Hashtag` button ([#3787](https://github.com/ReVanced/revanced-patches/issues/3787)) ([d9a70a3](d9a70a3ae0))
* **YouTube:** Support versions `19.25` and `19.34` ([#3629](https://github.com/ReVanced/revanced-patches/issues/3629)) ([172886f](172886fe4b))

### Performance Improvements

* **YouTube - GmsCore support:** Improve performance by using hashsets ([6bc6ea8](6bc6ea854b))
2024-10-20 01:42:35 +00:00
LisoUseInAIKyrios
0317aa1df7 chore: Merge branch dev to main (#3724) 2024-10-19 21:40:07 -04:00
github-actions[bot]
7466ffff9f chore: Sync translations (#3792) 2024-10-19 21:29:04 -04:00
semantic-release-bot
4d7a9dbb06 chore: Release v4.17.0-dev.13 [skip ci]
# [4.17.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.12...v4.17.0-dev.13) (2024-10-19)

### Bug Fixes

* **YouTube - GmsCore support:** Add more replacements ([6965b7d](6965b7d653))
* **YouTube - GmsCore support:** Remove unclear patch changes ([ab10b42](ab10b42388))

### Performance Improvements

* **YouTube - GmsCore support:** Improve performance by using hashsets ([6bc6ea8](6bc6ea854b))
2024-10-19 14:50:26 +00:00
github-actions[bot]
2113f9a83a chore: Sync translations (#3791) 2024-10-19 10:39:17 -04:00
oSumAtrIX
ab10b42388 fix(YouTube - GmsCore support): Remove unclear patch changes 2024-10-19 09:43:34 -04:00
oSumAtrIX
6965b7d653 fix(YouTube - GmsCore support): Add more replacements 2024-10-19 09:43:24 -04:00
oSumAtrIX
6bc6ea854b perf(YouTube - GmsCore support): Improve performance by using hashsets 2024-10-19 09:38:18 -04:00
github-actions[bot]
0390c95a10 chore: Sync translations (#3790) 2024-10-19 09:37:58 -04:00
github-actions[bot]
5151a5f37c chore: Sync translations (#3789)
Co-authored-by: revanced-bot <github@revanced.app>
2024-10-19 09:08:25 -04:00
oSumAtrIX
8c9d73fc39 ci: Grant workflow necessary permission 2024-10-19 15:03:25 +02:00
semantic-release-bot
07e65d8e83 chore: Release v4.17.0-dev.12 [skip ci]
# [4.17.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.11...v4.17.0-dev.12) (2024-10-19)

### Features

* **YouTube - Hide Shorts components:** Hide `Hashtag` button ([#3787](https://github.com/ReVanced/revanced-patches/issues/3787)) ([d9a70a3](d9a70a3ae0))
2024-10-19 12:41:16 +00:00
MarcaD
d9a70a3ae0 feat(YouTube - Hide Shorts components): Hide Hashtag button (#3787) 2024-10-19 08:39:08 -04:00
semantic-release-bot
945b6b0b34 chore: Release v4.17.0-dev.11 [skip ci]
# [4.17.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.10...v4.17.0-dev.11) (2024-10-19)

### Features

* **YouTube:** Support versions `19.25` and `19.34` ([#3629](https://github.com/ReVanced/revanced-patches/issues/3629)) ([172886f](172886fe4b))
2024-10-19 12:29:04 +00:00
Zain
172886fe4b feat(YouTube): Support versions 19.25 and 19.34 (#3629)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-10-19 08:26:39 -04:00
semantic-release-bot
651e34b997 chore: Release v4.17.0-dev.10 [skip ci]
# [4.17.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.9...v4.17.0-dev.10) (2024-10-17)

### Features

* **Facebook:** Add `Hide sponsored stories` patch ([#3627](https://github.com/ReVanced/revanced-patches/issues/3627)) ([fc7644d](fc7644d3b7))
2024-10-17 15:39:04 +00:00
PantlessCoding
fc7644d3b7 feat(Facebook): Add Hide sponsored stories patch (#3627)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-10-17 17:36:35 +02:00
semantic-release-bot
061ebcb7c6 chore: Release v4.17.0-dev.9 [skip ci]
# [4.17.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.8...v4.17.0-dev.9) (2024-10-17)

### Features

* **Sync for Reddit:** Add `Fix video downloads` patch ([#3739](https://github.com/ReVanced/revanced-patches/issues/3739)) ([38e7884](38e7884d17))
2024-10-17 15:30:41 +00:00
1fexd
38e7884d17 feat(Sync for Reddit): Add Fix video downloads patch (#3739)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-10-17 17:28:30 +02:00
1669 changed files with 66328 additions and 43384 deletions

View File

@@ -16,6 +16,12 @@ jobs:
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1

View File

@@ -20,13 +20,12 @@ jobs:
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: "main"
pr_title: "chore: ${{ env.MESSAGE }}"
destination_branch: main
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: |
This pull request will ${{ env.MESSAGE }}.
## Before merging this PR
- [ ] Remember about https://github.com/revanced/revanced-integrations
- [ ] Pull translations from Crowdin
pr_draft: true

View File

@@ -2,14 +2,13 @@ name: Pull strings
on:
workflow_dispatch:
schedule:
- cron: 0 0 1 * *
jobs:
pull:
name: Pull strings
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@@ -6,7 +6,7 @@ on:
branches:
- dev
paths:
- src/main/resources/addresources/values/strings.xml
- patches/src/main/resources/addresources/values/strings.xml
jobs:
push:

View File

@@ -23,13 +23,19 @@ jobs:
persist-credentials: false
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew generateMeta clean
run: ./gradlew build clean
- name: Setup Node.js
uses: actions/setup-node@v4

5
.gitignore vendored
View File

@@ -122,5 +122,8 @@ gradle-app.setting
# Dependency directories
node_modules/
# gradle properties, due to Github token
# Gradle properties, due to Github token
./gradle.properties
# One package is called the same as the Gradle build folder
!**/src/**/build/

2
.idea/misc.xml generated
View File

@@ -4,5 +4,5 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="azul-17" project-jdk-type="JavaSDK" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="azul-17" project-jdk-type="JavaSDK" />
</project>

View File

@@ -23,7 +23,6 @@
"assets": [
"CHANGELOG.md",
"gradle.properties",
"patches.json"
],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
@@ -33,11 +32,8 @@
{
"assets": [
{
"path": "build/libs/revanced-patches*"
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
},
{
"path": "patches.json"
}
],
successComment: false
}

View File

@@ -1,3 +1,551 @@
## [5.2.4-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.3...v5.2.4-dev.1) (2024-12-07)
### Bug Fixes
* **Twitter:** Merge correct extension by depending on correct extension patch ([8281cf6](https://github.com/ReVanced/revanced-patches/commit/8281cf6a3eead8cc25a277371e0b0ab2be982497))
## [5.2.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3) (2024-12-06)
### Bug Fixes
* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b))
## [5.2.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3-dev.1) (2024-12-06)
### Bug Fixes
* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b))
## [5.2.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2) (2024-12-06)
### Bug Fixes
* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([4017185](https://github.com/ReVanced/revanced-patches/commit/4017185e760c0569e6644b94bbe66a84fa245b4b))
## [5.2.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2-dev.1) (2024-12-05)
### Bug Fixes
* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([4017185](https://github.com/ReVanced/revanced-patches/commit/4017185e760c0569e6644b94bbe66a84fa245b4b))
## [5.2.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.0...v5.2.1) (2024-12-04)
### Bug Fixes
* **Twitch:** Resolve setting menu crashes ([#4025](https://github.com/ReVanced/revanced-patches/issues/4025)) ([62df596](https://github.com/ReVanced/revanced-patches/commit/62df5965d7331e47b3143425d169a79a19eac447))
* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4014](https://github.com/ReVanced/revanced-patches/issues/4014)) ([c8eced5](https://github.com/ReVanced/revanced-patches/commit/c8eced54704017df4e91e536dbef1e9514306f67))
* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4019](https://github.com/ReVanced/revanced-patches/issues/4019)) ([d89ad65](https://github.com/ReVanced/revanced-patches/commit/d89ad6501a7cdb3c074c6204dac7960ca3e252f1))
* **YouTube Music - Hide category bar:** Add support for latest release ([#3968](https://github.com/ReVanced/revanced-patches/issues/3968)) ([b63fdeb](https://github.com/ReVanced/revanced-patches/commit/b63fdeb10b504468307a77bd5de69407906848bf))
### Performance Improvements
* Move variables to local scope ([43c0421](https://github.com/ReVanced/revanced-patches/commit/43c04216c6e647eaf6ad7e813eb5f0df0c108b77))
## [5.2.1-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.4...v5.2.1-dev.5) (2024-12-01)
### Performance Improvements
* Move variables to local scope ([43c0421](https://github.com/ReVanced/revanced-patches/commit/43c04216c6e647eaf6ad7e813eb5f0df0c108b77))
## [5.2.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.3...v5.2.1-dev.4) (2024-11-30)
### Bug Fixes
* **Twitch:** Resolve setting menu crashes ([#4025](https://github.com/ReVanced/revanced-patches/issues/4025)) ([62df596](https://github.com/ReVanced/revanced-patches/commit/62df5965d7331e47b3143425d169a79a19eac447))
## [5.2.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.2...v5.2.1-dev.3) (2024-11-29)
### Bug Fixes
* **YouTube Music - Hide category bar:** Add support for latest release ([#3968](https://github.com/ReVanced/revanced-patches/issues/3968)) ([b63fdeb](https://github.com/ReVanced/revanced-patches/commit/b63fdeb10b504468307a77bd5de69407906848bf))
## [5.2.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.1...v5.2.1-dev.2) (2024-11-28)
### Bug Fixes
* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4019](https://github.com/ReVanced/revanced-patches/issues/4019)) ([d89ad65](https://github.com/ReVanced/revanced-patches/commit/d89ad6501a7cdb3c074c6204dac7960ca3e252f1))
## [5.2.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.0...v5.2.1-dev.1) (2024-11-28)
### Bug Fixes
* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4014](https://github.com/ReVanced/revanced-patches/issues/4014)) ([c8eced5](https://github.com/ReVanced/revanced-patches/commit/c8eced54704017df4e91e536dbef1e9514306f67))
# [5.2.0](https://github.com/ReVanced/revanced-patches/compare/v5.1.0...v5.2.0) (2024-11-27)
### Bug Fixes
* **My Expenses - Unlock pro:** Constrain compatible version to working version ([#3974](https://github.com/ReVanced/revanced-patches/issues/3974)) ([ba3bf69](https://github.com/ReVanced/revanced-patches/commit/ba3bf69df07ec8dab46868c3940ebd56db0cd137))
* **YouTube - Hide Shorts components:** Add missing options to patch ([65f62fc](https://github.com/ReVanced/revanced-patches/commit/65f62fcd5ac340616a96542c64faf2af2a60df28))
* **YouTube - Playback speed:** Allow long press 2x speed when using custom playback speeds ([#3990](https://github.com/ReVanced/revanced-patches/issues/3990)) ([79a543a](https://github.com/ReVanced/revanced-patches/commit/79a543a57470638f983862c61270e046f3ac5cb7))
* **YouTube - Settings:** Do not clip settings menus when using an Android 15 device ([#3999](https://github.com/ReVanced/revanced-patches/issues/3999)) ([7382a02](https://github.com/ReVanced/revanced-patches/commit/7382a020b8322a7abc016a4569bc15f9caf05546))
* **YouTube - Settings:** Show navigation back button in setting sub menus ([#3991](https://github.com/ReVanced/revanced-patches/issues/3991)) ([e61686c](https://github.com/ReVanced/revanced-patches/commit/e61686c1039ae29e443273e4da4ec63956216841))
* **YouTube - Spoof video streams:** Log out the iOS client to restore kids videos playback ([#4000](https://github.com/ReVanced/revanced-patches/issues/4000)) ([cc2ac4e](https://github.com/ReVanced/revanced-patches/commit/cc2ac4e4cd15ca2a23d60abd160d915bc98f99b4))
### Features
* **TikTok:** Add ReVanced settings about screen ([#4009](https://github.com/ReVanced/revanced-patches/issues/4009)) ([12ea26b](https://github.com/ReVanced/revanced-patches/commit/12ea26b10ddea5ad39da1d35e2b8fd0b48c15d88))
* **VSCO:** Remove non functional `Unlock pro` patch ([4fddb19](https://github.com/ReVanced/revanced-patches/commit/4fddb1930bc7adeee3b60ae9cd346b143e88bd42))
* **YouTube - Theme:** Apply custom seekbar color to splash screen animation ([#3978](https://github.com/ReVanced/revanced-patches/issues/3978)) ([98d57e2](https://github.com/ReVanced/revanced-patches/commit/98d57e28af7206099867474b7aa3760cd4fe333f))
* **YouTube:** Support version `19.46.42` ([#4010](https://github.com/ReVanced/revanced-patches/issues/4010)) ([122aac6](https://github.com/ReVanced/revanced-patches/commit/122aac6aee8ef0737f18564f11bbc2a6addf4a6b))
# [5.2.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.6...v5.2.0-dev.7) (2024-11-27)
### Bug Fixes
* **YouTube - Settings:** Do not clip settings menus when using an Android 15 device ([#3999](https://github.com/ReVanced/revanced-patches/issues/3999)) ([7382a02](https://github.com/ReVanced/revanced-patches/commit/7382a020b8322a7abc016a4569bc15f9caf05546))
# [5.2.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.5...v5.2.0-dev.6) (2024-11-27)
### Features
* **YouTube:** Support version `19.46.42` ([#4010](https://github.com/ReVanced/revanced-patches/issues/4010)) ([122aac6](https://github.com/ReVanced/revanced-patches/commit/122aac6aee8ef0737f18564f11bbc2a6addf4a6b))
# [5.2.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.4...v5.2.0-dev.5) (2024-11-27)
### Bug Fixes
* **YouTube - Spoof video streams:** Log out the iOS client to restore kids videos playback ([#4000](https://github.com/ReVanced/revanced-patches/issues/4000)) ([cc2ac4e](https://github.com/ReVanced/revanced-patches/commit/cc2ac4e4cd15ca2a23d60abd160d915bc98f99b4))
### Features
* **TikTok:** Add ReVanced settings about screen ([#4009](https://github.com/ReVanced/revanced-patches/issues/4009)) ([12ea26b](https://github.com/ReVanced/revanced-patches/commit/12ea26b10ddea5ad39da1d35e2b8fd0b48c15d88))
# [5.2.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.3...v5.2.0-dev.4) (2024-11-26)
### Bug Fixes
* **YouTube - Playback speed:** Allow long press 2x speed when using custom playback speeds ([#3990](https://github.com/ReVanced/revanced-patches/issues/3990)) ([79a543a](https://github.com/ReVanced/revanced-patches/commit/79a543a57470638f983862c61270e046f3ac5cb7))
# [5.2.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.2...v5.2.0-dev.3) (2024-11-26)
### Features
* **VSCO:** Remove non functional `Unlock pro` patch ([4fddb19](https://github.com/ReVanced/revanced-patches/commit/4fddb1930bc7adeee3b60ae9cd346b143e88bd42))
# [5.2.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.1...v5.2.0-dev.2) (2024-11-26)
### Bug Fixes
* **YouTube - Settings:** Show navigation back button in setting sub menus ([#3991](https://github.com/ReVanced/revanced-patches/issues/3991)) ([e61686c](https://github.com/ReVanced/revanced-patches/commit/e61686c1039ae29e443273e4da4ec63956216841))
# [5.2.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.1.1-dev.2...v5.2.0-dev.1) (2024-11-25)
### Features
* **YouTube - Theme:** Apply custom seekbar color to splash screen animation ([#3978](https://github.com/ReVanced/revanced-patches/issues/3978)) ([98d57e2](https://github.com/ReVanced/revanced-patches/commit/98d57e28af7206099867474b7aa3760cd4fe333f))
## [5.1.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.1.1-dev.1...v5.1.1-dev.2) (2024-11-25)
### Bug Fixes
* **YouTube - Hide Shorts components:** Add missing options to patch ([65f62fc](https://github.com/ReVanced/revanced-patches/commit/65f62fcd5ac340616a96542c64faf2af2a60df28))
## [5.1.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.1.0...v5.1.1-dev.1) (2024-11-24)
### Bug Fixes
* **My Expenses - Unlock pro:** Constrain compatible version to working version ([#3974](https://github.com/ReVanced/revanced-patches/issues/3974)) ([ba3bf69](https://github.com/ReVanced/revanced-patches/commit/ba3bf69df07ec8dab46868c3940ebd56db0cd137))
# [5.1.0](https://github.com/ReVanced/revanced-patches/compare/v5.0.2...v5.1.0) (2024-11-24)
### Bug Fixes
* **YouTube - Change header:** Apply header changes to A/B layout ([#3907](https://github.com/ReVanced/revanced-patches/issues/3907)) ([6ccf114](https://github.com/ReVanced/revanced-patches/commit/6ccf11426ec9e9cd9c8e89a2443f0d0645cc78b1))
* **YouTube - Hide Shorts components:** Do not hide Shorts action buttons on app first launch ([#3933](https://github.com/ReVanced/revanced-patches/issues/3933)) ([0d78815](https://github.com/ReVanced/revanced-patches/commit/0d78815e33bf2ae216e519f067fb773df0f2084e))
* **YouTube - Playback speed:** Add 'Auto' speed. Always override speed if default is set to 1.0x ([#3914](https://github.com/ReVanced/revanced-patches/issues/3914)) ([497739e](https://github.com/ReVanced/revanced-patches/commit/497739e8ce6933c1f1ea46edffc102e56b985623))
* **YouTube - SponsorBlock:** Fix create new segment crash on tablet custom roms ([#3946](https://github.com/ReVanced/revanced-patches/issues/3946)) ([a0da377](https://github.com/ReVanced/revanced-patches/commit/a0da377ba8f90ba39e905ed9730b3e819633bd50))
* **YouTube - Spoof app version:** Adjust legacy spoof targets ([#3934](https://github.com/ReVanced/revanced-patches/issues/3934)) ([f5794c1](https://github.com/ReVanced/revanced-patches/commit/f5794c1f896c331d76fdfc299e31a2773f2209ca))
* **YouTube - Spoof app version:** Remove broken spoof targets when patching 19.25+ ([#3915](https://github.com/ReVanced/revanced-patches/issues/3915)) ([9e18eca](https://github.com/ReVanced/revanced-patches/commit/9e18ecab1877dd33a3ad0fe216e6b91a8daaf1f8))
### Features
* **YouTube - Miniplayer:** Add option to disable miniplayer ([#3961](https://github.com/ReVanced/revanced-patches/issues/3961)) ([e565cdb](https://github.com/ReVanced/revanced-patches/commit/e565cdb583aacfc0052d12c430f56fd9abd5bf00))
* **YouTube:** Support version `19.45.38` ([#3938](https://github.com/ReVanced/revanced-patches/issues/3938)) ([7c4e3fe](https://github.com/ReVanced/revanced-patches/commit/7c4e3fe97e8cbbb8cf16a2fb95f64223ca2bd7ef))
# [5.1.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.1.0-dev.2...v5.1.0-dev.3) (2024-11-22)
### Features
* **YouTube - Miniplayer:** Add option to disable miniplayer ([#3961](https://github.com/ReVanced/revanced-patches/issues/3961)) ([e565cdb](https://github.com/ReVanced/revanced-patches/commit/e565cdb583aacfc0052d12c430f56fd9abd5bf00))
# [5.1.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.1.0-dev.1...v5.1.0-dev.2) (2024-11-21)
### Bug Fixes
* **YouTube - SponsorBlock:** Fix create new segment crash on tablet custom roms ([#3946](https://github.com/ReVanced/revanced-patches/issues/3946)) ([a0da377](https://github.com/ReVanced/revanced-patches/commit/a0da377ba8f90ba39e905ed9730b3e819633bd50))
# [5.1.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.5...v5.1.0-dev.1) (2024-11-20)
### Features
* **YouTube:** Support version `19.45.38` ([#3938](https://github.com/ReVanced/revanced-patches/issues/3938)) ([7c4e3fe](https://github.com/ReVanced/revanced-patches/commit/7c4e3fe97e8cbbb8cf16a2fb95f64223ca2bd7ef))
## [5.0.3-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.4...v5.0.3-dev.5) (2024-11-18)
### Bug Fixes
* **YouTube - Hide Shorts components:** Do not hide Shorts action buttons on app first launch ([#3933](https://github.com/ReVanced/revanced-patches/issues/3933)) ([0d78815](https://github.com/ReVanced/revanced-patches/commit/0d78815e33bf2ae216e519f067fb773df0f2084e))
## [5.0.3-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.3...v5.0.3-dev.4) (2024-11-18)
### Bug Fixes
* **YouTube - Spoof app version:** Adjust legacy spoof targets ([#3934](https://github.com/ReVanced/revanced-patches/issues/3934)) ([f5794c1](https://github.com/ReVanced/revanced-patches/commit/f5794c1f896c331d76fdfc299e31a2773f2209ca))
## [5.0.3-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.2...v5.0.3-dev.3) (2024-11-15)
### Bug Fixes
* **YouTube - Playback speed:** Add 'Auto' speed. Always override speed if default is set to 1.0x ([#3914](https://github.com/ReVanced/revanced-patches/issues/3914)) ([497739e](https://github.com/ReVanced/revanced-patches/commit/497739e8ce6933c1f1ea46edffc102e56b985623))
## [5.0.3-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.1...v5.0.3-dev.2) (2024-11-15)
### Bug Fixes
* **YouTube - Spoof app version:** Remove broken spoof targets when patching 19.25+ ([#3915](https://github.com/ReVanced/revanced-patches/issues/3915)) ([9e18eca](https://github.com/ReVanced/revanced-patches/commit/9e18ecab1877dd33a3ad0fe216e6b91a8daaf1f8))
## [5.0.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.2...v5.0.3-dev.1) (2024-11-13)
### Bug Fixes
* **YouTube - Change header:** Apply header changes to A/B layout ([#3907](https://github.com/ReVanced/revanced-patches/issues/3907)) ([6ccf114](https://github.com/ReVanced/revanced-patches/commit/6ccf11426ec9e9cd9c8e89a2443f0d0645cc78b1))
## [5.0.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.1...v5.0.2) (2024-11-12)
### Bug Fixes
* **Sync for Reddit - Fix /s/ links:** Fix patch by using correct fingerprints ([a0ad07e](https://github.com/ReVanced/revanced-patches/commit/a0ad07ef3170dbe1d91ebd40f11d97b63d1c63d0))
* **Sync for Reddit - Spoof client:** Fix patch by using correct fingerprints ([5776de3](https://github.com/ReVanced/revanced-patches/commit/5776de3cfbfa62360267eb6026525d2da8c45654))
* **YouTube - Player controls:** Show player control buttons with A/B layout ([#3901](https://github.com/ReVanced/revanced-patches/issues/3901)) ([bb526bc](https://github.com/ReVanced/revanced-patches/commit/bb526bc00a384eb808f46267e5802c8e5beaa7d5))
## [5.0.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.2-dev.1...v5.0.2-dev.2) (2024-11-12)
### Bug Fixes
* **YouTube - Player controls:** Show player control buttons with A/B layout ([#3901](https://github.com/ReVanced/revanced-patches/issues/3901)) ([bb526bc](https://github.com/ReVanced/revanced-patches/commit/bb526bc00a384eb808f46267e5802c8e5beaa7d5))
## [5.0.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.1...v5.0.2-dev.1) (2024-11-11)
### Bug Fixes
* **Sync for Reddit - Fix /s/ links:** Fix patch by using correct fingerprints ([a0ad07e](https://github.com/ReVanced/revanced-patches/commit/a0ad07ef3170dbe1d91ebd40f11d97b63d1c63d0))
* **Sync for Reddit - Spoof client:** Fix patch by using correct fingerprints ([5776de3](https://github.com/ReVanced/revanced-patches/commit/5776de3cfbfa62360267eb6026525d2da8c45654))
## [5.0.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.0...v5.0.1) (2024-11-11)
### Bug Fixes
* **Sync:** Fix patches by not throwing unnecessarily ([2ee1316](https://github.com/ReVanced/revanced-patches/commit/2ee13160d51dba3c5806594b2387f806e5946b9a))
* **Tiktok - Settings:** Fix the patch by depending on the correct settings patch ([0c75929](https://github.com/ReVanced/revanced-patches/commit/0c75929a83729841197b482d28f7f7f5f9cec332))
* **Twitter:** Fix patches by depending on patch that merges required extension ([c330e9d](https://github.com/ReVanced/revanced-patches/commit/c330e9d67d3e8c8c3535fa43e52c9f06e33ff0bf))
* **Twitter:** Fix patches by matching fingerprint using correct class ([6ae0d12](https://github.com/ReVanced/revanced-patches/commit/6ae0d124e1f27faecd20e4008951b08353572d98))
* **YouTube - Playback speed:** Remember playback speed when using non 1.0x default speed ([05b9f87](https://github.com/ReVanced/revanced-patches/commit/05b9f8709895dae67e8cc12e8b7bdb87ff401997)), closes [#3810](https://github.com/ReVanced/revanced-patches/issues/3810)
### Performance Improvements
* Check for extension without a class proxy ([a6a74e2](https://github.com/ReVanced/revanced-patches/commit/a6a74e289db1fe04db230d1e864cb9e752f9a01d))
## [5.0.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.3...v5.0.1-dev.4) (2024-11-11)
### Bug Fixes
* **Twitter:** Fix patches by depending on patch that merges required extension ([c330e9d](https://github.com/ReVanced/revanced-patches/commit/c330e9d67d3e8c8c3535fa43e52c9f06e33ff0bf))
## [5.0.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.2...v5.0.1-dev.3) (2024-11-11)
### Bug Fixes
* **YouTube - Playback speed:** Remember playback speed when using non 1.0x default speed ([05b9f87](https://github.com/ReVanced/revanced-patches/commit/05b9f8709895dae67e8cc12e8b7bdb87ff401997)), closes [#3810](https://github.com/ReVanced/revanced-patches/issues/3810)
## [5.0.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.1...v5.0.1-dev.2) (2024-11-11)
### Bug Fixes
* **Twitter:** Fix patches by matching fingerprint using correct class ([6ae0d12](https://github.com/ReVanced/revanced-patches/commit/6ae0d124e1f27faecd20e4008951b08353572d98))
## [5.0.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.0...v5.0.1-dev.1) (2024-11-11)
### Bug Fixes
* **Sync:** Fix patches by not throwing unnecessarily ([2ee1316](https://github.com/ReVanced/revanced-patches/commit/2ee13160d51dba3c5806594b2387f806e5946b9a))
* **Tiktok - Settings:** Fix the patch by depending on the correct settings patch ([0c75929](https://github.com/ReVanced/revanced-patches/commit/0c75929a83729841197b482d28f7f7f5f9cec332))
### Performance Improvements
* Check for extension without a class proxy ([a6a74e2](https://github.com/ReVanced/revanced-patches/commit/a6a74e289db1fe04db230d1e864cb9e752f9a01d))
# [5.0.0](https://github.com/ReVanced/revanced-patches/compare/v4.17.0...v5.0.0) (2024-11-10)
### Bug Fixes
* Add missing dependency to patch ([97f5240](https://github.com/ReVanced/revanced-patches/commit/97f5240d53b9978fb3745170fe03619c7c90274a))
* **MyFitnessPal - Hide ads:** Constrain patch to last working version ([#3847](https://github.com/ReVanced/revanced-patches/issues/3847)) ([f9fa526](https://github.com/ReVanced/revanced-patches/commit/f9fa526b04c2848175c389d6bb911aa5a245b60f))
* **Twitter - Change link sharing domain:** Support latest app version ([#3786](https://github.com/ReVanced/revanced-patches/issues/3786)) ([b54592c](https://github.com/ReVanced/revanced-patches/commit/b54592cf9c5d859e1af2f02e8e6aaad7d47ab760))
* **YouTube - Copy video URL:** Support A/B player layout ([0f42574](https://github.com/ReVanced/revanced-patches/commit/0f42574b7f4b1c9a48df8550c7d710093f76ce8c))
* **YouTube - Custom branding:** Change icon correctly on 19.34+ ([#3866](https://github.com/ReVanced/revanced-patches/issues/3866)) ([2e47903](https://github.com/ReVanced/revanced-patches/commit/2e4790382546256e106a5842cd8c530f41b161e5))
* **YouTube - Hide ads:** Hide new types of ads ([454281a](https://github.com/ReVanced/revanced-patches/commit/454281ac2108648832b7f0203f5fb7e814887835))
* **YouTube - Hide layout components:** Move hide chips settings to Feed menu ([1ed677f](https://github.com/ReVanced/revanced-patches/commit/1ed677f7b8ba561b2bb173dcaf5d6123c22179c4))
* **YouTube - Hide layout components:** Remove obsolete 'Hide gray separator' ([a697701](https://github.com/ReVanced/revanced-patches/commit/a697701c5f1f9510b51e310b1ff212b609f38519))
* **YouTube - Playback speed:** Remember playback speed with new speed menu ([#3810](https://github.com/ReVanced/revanced-patches/issues/3810)) ([c3a5e14](https://github.com/ReVanced/revanced-patches/commit/c3a5e14a0a24973a0f9956845c9e0f99c1301d42))
* **YouTube - Playback speed:** Restore old playback speed menu ([#3817](https://github.com/ReVanced/revanced-patches/issues/3817)) ([806b210](https://github.com/ReVanced/revanced-patches/commit/806b21093e3251697f03cd8804e5d5cd26070716))
* **YouTube - Remember video quality:** Correctly set default quality when changing from a low quality video ([#3879](https://github.com/ReVanced/revanced-patches/issues/3879)) ([ddb73e8](https://github.com/ReVanced/revanced-patches/commit/ddb73e857d7c26fd27ea995a27f53f5660d3f71c))
* **YouTube - Remove background playback restrictions:** Enable for Shorts as well ([#3671](https://github.com/ReVanced/revanced-patches/issues/3671)) ([7db1a77](https://github.com/ReVanced/revanced-patches/commit/7db1a7751dc47c4e36096fbdc2b3761b0ae11ccb))
* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([084e0a5](https://github.com/ReVanced/revanced-patches/commit/084e0a527b1c75d1ef15dc706c429aa48d0ffe6b))
* **YouTube - Return YouTube Dislike:** Use latest separator height ([ae160a3](https://github.com/ReVanced/revanced-patches/commit/ae160a37985cc96c6de7e1a2fe5a1c83bc523046))
* **YouTube - Seekbar:** Use latest shade of YouTube red ([4b77648](https://github.com/ReVanced/revanced-patches/commit/4b77648607a84eb29f4cae9ddb42b87084be7cd0))
* **YouTube - Settings:** Use multiline preference title for localized languages ([#3821](https://github.com/ReVanced/revanced-patches/issues/3821)) ([ff85d49](https://github.com/ReVanced/revanced-patches/commit/ff85d490887de64eb6c6fd42e385a3e75969ff10))
* **YouTube - SponsorBlock:** Show correct segment behavior in settings UI after importing ([e3f25a0](https://github.com/ReVanced/revanced-patches/commit/e3f25a03cd314eeae786e7660a6beacb275a6a76))
* **YouTube - Spoof app version:** Remove obsolete 17.33.42 spoof target ([#3825](https://github.com/ReVanced/revanced-patches/issues/3825)) ([33aeba2](https://github.com/ReVanced/revanced-patches/commit/33aeba2a0895e9ecaba27ba4a3b22b86c9f1a51c))
* **YouTube:** Merge `Restore old seekbar thumbnails` into `Seekbar thumbnails` ([#3860](https://github.com/ReVanced/revanced-patches/issues/3860)) ([e377b1e](https://github.com/ReVanced/revanced-patches/commit/e377b1e6ad93dea8e5f3829cd3894f71851887a3))
### Build System
* Bump ReVanced Patcher ([eee1692](https://github.com/ReVanced/revanced-patches/commit/eee16922779f994f5752190a20a9016ea98ec4cb))
### Features
* **YouTube - Hide layout components:** Hide player shopping shelf ([#3804](https://github.com/ReVanced/revanced-patches/issues/3804)) ([1952f3b](https://github.com/ReVanced/revanced-patches/commit/1952f3b3c4bca08ed0f6e5b1117e0a6c51f00ed2))
* **YouTube - Hide player flyout menu items:** Hide stable volume ([#3827](https://github.com/ReVanced/revanced-patches/issues/3827)) ([b91e932](https://github.com/ReVanced/revanced-patches/commit/b91e932e65c04b1c1aee9a2f3dc3a73772d9c225))
* **YouTube - Miniplayer:** Add horizontal drag gesture ([#3859](https://github.com/ReVanced/revanced-patches/issues/3859)) ([e32b19e](https://github.com/ReVanced/revanced-patches/commit/e32b19e170a5571b23547c3211b497089d0cd441))
* **YouTube - Player flyout menu:** Hide sleep timer ([#3637](https://github.com/ReVanced/revanced-patches/issues/3637)) ([7e1bdab](https://github.com/ReVanced/revanced-patches/commit/7e1bdab520dba65682f018f819c0b7d9783f94ca))
* **YouTube:** Add `Seekbar thumbnails` patch ([#3813](https://github.com/ReVanced/revanced-patches/issues/3813)) ([5988b75](https://github.com/ReVanced/revanced-patches/commit/5988b759752b944b6999b401faa394e2089e4003))
* **YouTube:** Add `Shorts autoplay` patch ([#3794](https://github.com/ReVanced/revanced-patches/issues/3794)) ([96b5aed](https://github.com/ReVanced/revanced-patches/commit/96b5aede482f7a69d6df17864a2e17568b0da880))
* **YouTube:** Hide player shopping shelf in playlists ([#3806](https://github.com/ReVanced/revanced-patches/issues/3806)) ([a553a13](https://github.com/ReVanced/revanced-patches/commit/a553a13c0326ef2fff7f785fed592d553a7963ce))
* **YouTube:** Merge multiple layout patches into `Hide Layout Components` ([#3799](https://github.com/ReVanced/revanced-patches/issues/3799)) ([bbcb57a](https://github.com/ReVanced/revanced-patches/commit/bbcb57a32dfc8f031886f98b1b9701285105c579))
* **YouTube:** Merge multiple player overlay patches into `Hide player overlay buttons` ([#3800](https://github.com/ReVanced/revanced-patches/issues/3800)) ([4ba0300](https://github.com/ReVanced/revanced-patches/commit/4ba0300590dd988bdcaa0761c4e606c1d7f86ce5))
* **YouTube:** Support version `19.43.41` ([#3854](https://github.com/ReVanced/revanced-patches/issues/3854)) ([85de5c7](https://github.com/ReVanced/revanced-patches/commit/85de5c7d96ce2d67f6386d1438e43620d31cc645))
### BREAKING CHANGES
* Various APIs have been changed or removed.
# [5.0.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.3...v5.0.0-dev.4) (2024-11-09)
### Bug Fixes
* **YouTube - Remember video quality:** Correctly set default quality when changing from a low quality video ([#3879](https://github.com/ReVanced/revanced-patches/issues/3879)) ([ddb73e8](https://github.com/ReVanced/revanced-patches/commit/ddb73e857d7c26fd27ea995a27f53f5660d3f71c))
# [5.0.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.2...v5.0.0-dev.3) (2024-11-09)
### Bug Fixes
* Add missing dependency to patch ([97f5240](https://github.com/ReVanced/revanced-patches/commit/97f5240d53b9978fb3745170fe03619c7c90274a))
# [5.0.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.1...v5.0.0-dev.2) (2024-11-09)
### Bug Fixes
* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([084e0a5](https://github.com/ReVanced/revanced-patches/commit/084e0a527b1c75d1ef15dc706c429aa48d0ffe6b))
# [5.0.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.6...v5.0.0-dev.1) (2024-11-06)
### Bug Fixes
* **MyFitnessPal - Hide ads:** Constrain patch to last working version ([#3847](https://github.com/ReVanced/revanced-patches/issues/3847)) ([f9fa526](https://github.com/ReVanced/revanced-patches/commit/f9fa526b04c2848175c389d6bb911aa5a245b60f))
* **YouTube - Copy video URL:** Support A/B player layout ([0f42574](https://github.com/ReVanced/revanced-patches/commit/0f42574b7f4b1c9a48df8550c7d710093f76ce8c))
* **YouTube - Custom branding:** Change icon correctly on 19.34+ ([#3866](https://github.com/ReVanced/revanced-patches/issues/3866)) ([2e47903](https://github.com/ReVanced/revanced-patches/commit/2e4790382546256e106a5842cd8c530f41b161e5))
* **YouTube - Hide ads:** Hide new types of ads ([454281a](https://github.com/ReVanced/revanced-patches/commit/454281ac2108648832b7f0203f5fb7e814887835))
* **YouTube - Hide layout components:** Remove obsolete 'Hide gray separator' ([a697701](https://github.com/ReVanced/revanced-patches/commit/a697701c5f1f9510b51e310b1ff212b609f38519))
* **YouTube - Playback speed:** Restore old playback speed menu ([#3817](https://github.com/ReVanced/revanced-patches/issues/3817)) ([806b210](https://github.com/ReVanced/revanced-patches/commit/806b21093e3251697f03cd8804e5d5cd26070716))
* **YouTube - Remove background playback restrictions:** Enable for Shorts as well ([#3671](https://github.com/ReVanced/revanced-patches/issues/3671)) ([7db1a77](https://github.com/ReVanced/revanced-patches/commit/7db1a7751dc47c4e36096fbdc2b3761b0ae11ccb))
* **YouTube - Return YouTube Dislike:** Use latest separator height ([ae160a3](https://github.com/ReVanced/revanced-patches/commit/ae160a37985cc96c6de7e1a2fe5a1c83bc523046))
* **YouTube - Seekbar:** Use latest shade of YouTube red ([4b77648](https://github.com/ReVanced/revanced-patches/commit/4b77648607a84eb29f4cae9ddb42b87084be7cd0))
* **YouTube - Settings:** Use multiline preference title for localized languages ([#3821](https://github.com/ReVanced/revanced-patches/issues/3821)) ([ff85d49](https://github.com/ReVanced/revanced-patches/commit/ff85d490887de64eb6c6fd42e385a3e75969ff10))
* **YouTube - SponsorBlock:** Show correct segment behavior in settings UI after importing ([e3f25a0](https://github.com/ReVanced/revanced-patches/commit/e3f25a03cd314eeae786e7660a6beacb275a6a76))
* **YouTube - Spoof app version:** Remove obsolete 17.33.42 spoof target ([#3825](https://github.com/ReVanced/revanced-patches/issues/3825)) ([33aeba2](https://github.com/ReVanced/revanced-patches/commit/33aeba2a0895e9ecaba27ba4a3b22b86c9f1a51c))
* **YouTube:** Merge `Restore old seekbar thumbnails` into `Seekbar thumbnails` ([#3860](https://github.com/ReVanced/revanced-patches/issues/3860)) ([e377b1e](https://github.com/ReVanced/revanced-patches/commit/e377b1e6ad93dea8e5f3829cd3894f71851887a3))
### Build System
* Bump ReVanced Patcher ([eee1692](https://github.com/ReVanced/revanced-patches/commit/eee16922779f994f5752190a20a9016ea98ec4cb))
### Features
* **YouTube - Hide player flyout menu items:** Hide stable volume ([#3827](https://github.com/ReVanced/revanced-patches/issues/3827)) ([b91e932](https://github.com/ReVanced/revanced-patches/commit/b91e932e65c04b1c1aee9a2f3dc3a73772d9c225))
* **YouTube - Miniplayer:** Add horizontal drag gesture ([#3859](https://github.com/ReVanced/revanced-patches/issues/3859)) ([e32b19e](https://github.com/ReVanced/revanced-patches/commit/e32b19e170a5571b23547c3211b497089d0cd441))
* **YouTube - Player flyout menu:** Hide sleep timer ([#3637](https://github.com/ReVanced/revanced-patches/issues/3637)) ([7e1bdab](https://github.com/ReVanced/revanced-patches/commit/7e1bdab520dba65682f018f819c0b7d9783f94ca))
* **YouTube:** Add `Seekbar thumbnails` patch ([#3813](https://github.com/ReVanced/revanced-patches/issues/3813)) ([5988b75](https://github.com/ReVanced/revanced-patches/commit/5988b759752b944b6999b401faa394e2089e4003))
* **YouTube:** Support version `19.43.41` ([#3854](https://github.com/ReVanced/revanced-patches/issues/3854)) ([85de5c7](https://github.com/ReVanced/revanced-patches/commit/85de5c7d96ce2d67f6386d1438e43620d31cc645))
### BREAKING CHANGES
* Various APIs have been changed or removed.
# [4.18.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.5...v4.18.0-dev.6) (2024-10-24)
### Bug Fixes
* **YouTube - Playback speed:** Remember playback speed with new speed menu ([#3810](https://github.com/ReVanced/revanced-patches/issues/3810)) ([c3a5e14](https://github.com/ReVanced/revanced-patches/commit/c3a5e14a0a24973a0f9956845c9e0f99c1301d42))
# [4.18.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.4...v4.18.0-dev.5) (2024-10-23)
### Features
* **YouTube:** Hide player shopping shelf in playlists ([#3806](https://github.com/ReVanced/revanced-patches/issues/3806)) ([a553a13](https://github.com/ReVanced/revanced-patches/commit/a553a13c0326ef2fff7f785fed592d553a7963ce))
# [4.18.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.3...v4.18.0-dev.4) (2024-10-23)
### Features
* **YouTube - Hide layout components:** Hide player shopping shelf ([#3804](https://github.com/ReVanced/revanced-patches/issues/3804)) ([1952f3b](https://github.com/ReVanced/revanced-patches/commit/1952f3b3c4bca08ed0f6e5b1117e0a6c51f00ed2))
# [4.18.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.2...v4.18.0-dev.3) (2024-10-22)
### Features
* **YouTube:** Merge multiple layout patches into `Hide Layout Components` ([#3799](https://github.com/ReVanced/revanced-patches/issues/3799)) ([bbcb57a](https://github.com/ReVanced/revanced-patches/commit/bbcb57a32dfc8f031886f98b1b9701285105c579))
* **YouTube:** Merge multiple player overlay patches into `Hide player overlay buttons` ([#3800](https://github.com/ReVanced/revanced-patches/issues/3800)) ([4ba0300](https://github.com/ReVanced/revanced-patches/commit/4ba0300590dd988bdcaa0761c4e606c1d7f86ce5))
# [4.18.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.1...v4.18.0-dev.2) (2024-10-22)
### Bug Fixes
* **Twitter - Change link sharing domain:** Support latest app version ([#3786](https://github.com/ReVanced/revanced-patches/issues/3786)) ([b54592c](https://github.com/ReVanced/revanced-patches/commit/b54592cf9c5d859e1af2f02e8e6aaad7d47ab760))
# [4.18.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.17.0...v4.18.0-dev.1) (2024-10-21)
### Bug Fixes
* **YouTube - Hide layout components:** Move hide chips settings to Feed menu ([1ed677f](https://github.com/ReVanced/revanced-patches/commit/1ed677f7b8ba561b2bb173dcaf5d6123c22179c4))
### Features
* **YouTube:** Add `Shorts autoplay` patch ([#3794](https://github.com/ReVanced/revanced-patches/issues/3794)) ([96b5aed](https://github.com/ReVanced/revanced-patches/commit/96b5aede482f7a69d6df17864a2e17568b0da880))
# [4.17.0](https://github.com/ReVanced/revanced-patches/compare/v4.16.0...v4.17.0) (2024-10-20)
### Bug Fixes
* **Twitter - Unlock downloads:** Make it work with latest versions ([#3782](https://github.com/ReVanced/revanced-patches/issues/3782)) ([5189122](https://github.com/ReVanced/revanced-patches/commit/5189122006b0f72d5bfb50422021c3b0f3a9ae4a))
* **YouTube - GmsCore support:** Add more replacements ([4d39770](https://github.com/ReVanced/revanced-patches/commit/4d39770602b39b6cb399eb0d8c52947b6ebafbb0))
* **YouTube - GmsCore support:** Remove unclear patch changes ([021d858](https://github.com/ReVanced/revanced-patches/commit/021d8584a7f5a6d1a028c5d18dc91a3b988b2884))
* **YouTube - Hide layout components:** Adjust settings text ([#3745](https://github.com/ReVanced/revanced-patches/issues/3745)) ([13998bb](https://github.com/ReVanced/revanced-patches/commit/13998bbf95ac3cde8bf24754d60258d0ff9bc4f4))
* **YouTube - Spoof video streams:** Fix playback for Android VR by removing invalid body as well ([#3769](https://github.com/ReVanced/revanced-patches/issues/3769)) ([5150a15](https://github.com/ReVanced/revanced-patches/commit/5150a15ad4ca73a747f0a89f933db7f2d686ec2d))
### Features
* **Backdrops - Pro unlock:** Support latest versions by removing version constraint ([a62b506](https://github.com/ReVanced/revanced-patches/commit/a62b50691c49d1ce529a7c9c4e49da0d0dd46df2))
* **Facebook:** Add `Hide sponsored stories` patch ([#3627](https://github.com/ReVanced/revanced-patches/issues/3627)) ([214c72b](https://github.com/ReVanced/revanced-patches/commit/214c72baeb7f87f21cd2ca34301ab11fa0ff1a4f))
* **Sync for Reddit:** Add `Fix video downloads` patch ([#3739](https://github.com/ReVanced/revanced-patches/issues/3739)) ([a47ee38](https://github.com/ReVanced/revanced-patches/commit/a47ee38b1cdd974a959008006ecaf58917addc60))
* **Twitter:** Add `Change link sharing domain` patch ([#3753](https://github.com/ReVanced/revanced-patches/issues/3753)) ([9269a07](https://github.com/ReVanced/revanced-patches/commit/9269a076b674ecdcf478bca842238f6e30869f44))
* **Willhaben:** Add `Hide ads` patch ([#3740](https://github.com/ReVanced/revanced-patches/issues/3740)) ([1fe3a52](https://github.com/ReVanced/revanced-patches/commit/1fe3a523e99ccfe556d88800686e34ac6ed77b2c))
* **YouTube - Hide layout components:** Add option to hide Yoodles (YouTube Doodles) ([#3743](https://github.com/ReVanced/revanced-patches/issues/3743)) ([b8c8916](https://github.com/ReVanced/revanced-patches/commit/b8c89164cf3911ac3842df9b0d2ec42b52213505))
* **YouTube - Hide Shorts components:** Add option to hide `Use template`, `Upcoming`, `Green screen` buttons ([#3752](https://github.com/ReVanced/revanced-patches/issues/3752)) ([f71c406](https://github.com/ReVanced/revanced-patches/commit/f71c4068bc646d02954b59fac4756f1419c55dbe))
* **YouTube - Hide Shorts components:** Add option to hide like fountain ([#3731](https://github.com/ReVanced/revanced-patches/issues/3731)) ([00a99dd](https://github.com/ReVanced/revanced-patches/commit/00a99dd13be6e5c44fa691d74c92b23ce6ba659d))
* **YouTube - Hide Shorts components:** Hide `Hashtag` button ([#3787](https://github.com/ReVanced/revanced-patches/issues/3787)) ([828a634](https://github.com/ReVanced/revanced-patches/commit/828a634667c4005a90f3e469ad2c5d69387f0760))
* **YouTube:** Support versions `19.25` and `19.34` ([#3629](https://github.com/ReVanced/revanced-patches/issues/3629)) ([049e7f0](https://github.com/ReVanced/revanced-patches/commit/049e7f081358d2e1bf87d30e87b01c61b5eeafcc))
### Performance Improvements
* **YouTube - GmsCore support:** Improve performance by using hashsets ([2c5d390](https://github.com/ReVanced/revanced-patches/commit/2c5d390fb1275dc3da5a3b912e221b7d594a1561))
# [4.17.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.12...v4.17.0-dev.13) (2024-10-19)
### Bug Fixes
* **YouTube - GmsCore support:** Add more replacements ([4d39770](https://github.com/ReVanced/revanced-patches/commit/4d39770602b39b6cb399eb0d8c52947b6ebafbb0))
* **YouTube - GmsCore support:** Remove unclear patch changes ([021d858](https://github.com/ReVanced/revanced-patches/commit/021d8584a7f5a6d1a028c5d18dc91a3b988b2884))
### Performance Improvements
* **YouTube - GmsCore support:** Improve performance by using hashsets ([2c5d390](https://github.com/ReVanced/revanced-patches/commit/2c5d390fb1275dc3da5a3b912e221b7d594a1561))
# [4.17.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.11...v4.17.0-dev.12) (2024-10-19)
### Features
* **YouTube - Hide Shorts components:** Hide `Hashtag` button ([#3787](https://github.com/ReVanced/revanced-patches/issues/3787)) ([828a634](https://github.com/ReVanced/revanced-patches/commit/828a634667c4005a90f3e469ad2c5d69387f0760))
# [4.17.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.10...v4.17.0-dev.11) (2024-10-19)
### Features
* **YouTube:** Support versions `19.25` and `19.34` ([#3629](https://github.com/ReVanced/revanced-patches/issues/3629)) ([049e7f0](https://github.com/ReVanced/revanced-patches/commit/049e7f081358d2e1bf87d30e87b01c61b5eeafcc))
# [4.17.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.9...v4.17.0-dev.10) (2024-10-17)
### Features
* **Facebook:** Add `Hide sponsored stories` patch ([#3627](https://github.com/ReVanced/revanced-patches/issues/3627)) ([214c72b](https://github.com/ReVanced/revanced-patches/commit/214c72baeb7f87f21cd2ca34301ab11fa0ff1a4f))
# [4.17.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.8...v4.17.0-dev.9) (2024-10-17)
### Features
* **Sync for Reddit:** Add `Fix video downloads` patch ([#3739](https://github.com/ReVanced/revanced-patches/issues/3739)) ([a47ee38](https://github.com/ReVanced/revanced-patches/commit/a47ee38b1cdd974a959008006ecaf58917addc60))
# [4.17.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.7...v4.17.0-dev.8) (2024-10-17)

File diff suppressed because it is too large Load Diff

View File

@@ -1,155 +0,0 @@
import org.gradle.kotlin.dsl.support.listFilesOrdered
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin)
alias(libs.plugins.binary.compatibility.validator)
`maven-publish`
signing
}
group = "app.revanced"
repositories {
mavenCentral()
mavenLocal()
google()
maven {
// A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials {
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
implementation(libs.revanced.patcher)
implementation(libs.smali)
// TODO: Required because build fails without it. Find a way to remove this dependency.
implementation(libs.guava)
// Used in JsonGenerator.
implementation(libs.gson)
// Android API stubs defined here.
compileOnly(project(":stub"))
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
java {
targetCompatibility = JavaVersion.VERSION_11
}
tasks {
withType(Jar::class) {
exclude("app/revanced/meta")
manifest {
attributes["Name"] = "ReVanced Patches"
attributes["Description"] = "Patches for ReVanced."
attributes["Version"] = version
attributes["Timestamp"] = System.currentTimeMillis().toString()
attributes["Source"] = "git@github.com:revanced/revanced-patches.git"
attributes["Author"] = "ReVanced"
attributes["Contact"] = "contact@revanced.app"
attributes["Origin"] = "https://revanced.app"
attributes["License"] = "GNU General Public License v3.0"
}
}
register("buildDexJar") {
description = "Build and add a DEX to the JAR file"
group = "build"
dependsOn(build)
doLast {
val d8 = File(System.getenv("ANDROID_HOME")).resolve("build-tools")
.listFilesOrdered().last().resolve("d8").absolutePath
val patchesJar = configurations.archives.get().allArtifacts.files.files.first().absolutePath
val workingDirectory = layout.buildDirectory.dir("libs").get().asFile
exec {
workingDir = workingDirectory
commandLine = listOf(d8, "--release", patchesJar)
}
exec {
workingDir = workingDirectory
commandLine = listOf("zip", "-u", patchesJar, "classes.dex")
}
}
}
register<JavaExec>("generatePatchesFiles") {
description = "Generate patches files"
dependsOn(build)
classpath = sourceSets["main"].runtimeClasspath
mainClass.set("app.revanced.generator.MainKt")
}
// Needed by gradle-semantic-release-plugin.
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
publish {
dependsOn("buildDexJar")
dependsOn("generatePatchesFiles")
}
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patches")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("revanced-patches-publication") {
from(components["java"])
pom {
name = "ReVanced Patches"
description = "Patches for ReVanced."
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-patches.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-patches.git"
url = "https://github.com/revanced/revanced-patches"
}
}
}
}
}
signing {
useGpgCmd()
sign(publishing.publications["revanced-patches-publication"])
}

View File

@@ -3,6 +3,6 @@ api_token_env: "CROWDIN_PERSONAL_TOKEN"
preserve_hierarchy: false
files:
- source: src/main/resources/addresources/values/strings.xml
translation: src/main/resources/addresources/values-%android_code%/strings.xml
- source: patches/src/main/resources/addresources/values/strings.xml
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
skip_untranslated_strings: true

View File

@@ -0,0 +1,5 @@
android.namespace = "app.revanced.extension"
dependencies {
compileOnly(libs.annotation)
}

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>

View File

@@ -0,0 +1,424 @@
package app.revanced.extension.all.connectivity.wifi.spoof;
import android.app.PendingIntent;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.os.Build;
import android.os.Handler;
import androidx.annotation.RequiresApi;
/** @noinspection deprecation, unused */
public class SpoofWifiPatch {
// Used to check what the (real or fake) active network is (take a look at `hasTransport`).
private static ConnectivityManager CONNECTIVITY_MANAGER;
// If Wifi is not enabled, these are types that would pretend to be Wifi for android.net.Network (lower index = higher priority).
// This does not apply to android.net.NetworkInfo, because we can pretend that Wifi is always active there.
//
// VPN should be a fallback, because Reverse Tethering uses VPN.
private static final int[] FAKE_FALLBACK_NETWORKS = { NetworkCapabilities.TRANSPORT_ETHERNET, NetworkCapabilities.TRANSPORT_VPN };
// In order to initialize our own ConnectivityManager, if it isn't initialized yet.
public static Object getSystemService(Context context, String name) {
Object result = context.getSystemService(name);
if (CONNECTIVITY_MANAGER == null) {
if (Context.CONNECTIVITY_SERVICE.equals(name)) {
CONNECTIVITY_MANAGER = (ConnectivityManager) result;
} else {
CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
}
return result;
}
// In order to initialize our own ConnectivityManager, if it isn't initialized yet.
public static Object getSystemService(Context context, Class<?> serviceClass) {
Object result = context.getSystemService(serviceClass);
if (CONNECTIVITY_MANAGER == null) {
if (serviceClass == ConnectivityManager.class) {
CONNECTIVITY_MANAGER = (ConnectivityManager) result;
} else {
CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
}
return result;
}
// Simply always return Wifi as active network.
public static NetworkInfo getActiveNetworkInfo(ConnectivityManager connectivityManager) {
for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return networkInfo;
}
}
return connectivityManager.getActiveNetworkInfo();
}
// Pretend Wifi is always connected.
public static boolean isConnected(NetworkInfo networkInfo) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return true;
}
return networkInfo.isConnected();
}
// Pretend Wifi is always connected.
public static boolean isConnectedOrConnecting(NetworkInfo networkInfo) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return true;
}
return networkInfo.isConnectedOrConnecting();
}
// Pretend Wifi is always available.
public static boolean isAvailable(NetworkInfo networkInfo) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return true;
}
return networkInfo.isAvailable();
}
// Pretend Wifi is always connected.
public static NetworkInfo.State getState(NetworkInfo networkInfo) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return NetworkInfo.State.CONNECTED;
}
return networkInfo.getState();
}
// Pretend Wifi is always connected.
public static NetworkInfo.DetailedState getDetailedState(NetworkInfo networkInfo) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return NetworkInfo.DetailedState.CONNECTED;
}
return networkInfo.getDetailedState();
}
// Pretend Wifi is enabled, so connection isn't metered.
public static boolean isActiveNetworkMetered(ConnectivityManager connectivityManager) {
return false;
}
// Returns the Wifi network, if Wifi is enabled.
// Otherwise if one of our fallbacks has a connection, return them.
// And as a last resort, return the default active network.
public static Network getActiveNetwork(ConnectivityManager connectivityManager) {
Network[] prioritizedNetworks = new Network[FAKE_FALLBACK_NETWORKS.length];
for (Network network : connectivityManager.getAllNetworks()) {
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
if (networkCapabilities == null) {
continue;
}
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return network;
}
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
for (int i = 0; i < FAKE_FALLBACK_NETWORKS.length; i++) {
int transportType = FAKE_FALLBACK_NETWORKS[i];
if (networkCapabilities.hasTransport(transportType)) {
prioritizedNetworks[i] = network;
break;
}
}
}
}
for (Network network : prioritizedNetworks) {
if (network != null) {
return network;
}
}
return connectivityManager.getActiveNetwork();
}
// If the given network is a real or fake Wifi connection, return a Wifi network.
// Otherwise fallback to default implementation.
public static NetworkInfo getNetworkInfo(ConnectivityManager connectivityManager, Network network) {
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) {
for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) {
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
return networkInfo;
}
}
}
return connectivityManager.getNetworkInfo(network);
}
// If we are checking if the NetworkCapabilities use Wifi, return yes if
// - it is a real Wifi connection,
// - or the NetworkCapabilities are from a network pretending being a Wifi network.
// Otherwise fallback to default implementation.
public static boolean hasTransport(NetworkCapabilities networkCapabilities, int transportType) {
if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return true;
}
if (CONNECTIVITY_MANAGER != null) {
Network activeNetwork = getActiveNetwork(CONNECTIVITY_MANAGER);
NetworkCapabilities activeNetworkCapabilities = CONNECTIVITY_MANAGER.getNetworkCapabilities(activeNetwork);
if (activeNetworkCapabilities != null) {
for (int fallbackTransportType : FAKE_FALLBACK_NETWORKS) {
if (activeNetworkCapabilities.hasTransport(fallbackTransportType) && networkCapabilities.hasTransport(fallbackTransportType)) {
return true;
}
}
}
}
}
return networkCapabilities.hasTransport(transportType);
}
// If the given network is a real or fake Wifi connection, pretend it has a connection (and some other things).
public static boolean hasCapability(NetworkCapabilities networkCapabilities, int capability) {
if (hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI) && (
capability == NetworkCapabilities.NET_CAPABILITY_INTERNET
|| capability == NetworkCapabilities.NET_CAPABILITY_FOREGROUND
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_METERED
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_VPN
|| capability == NetworkCapabilities.NET_CAPABILITY_TRUSTED
|| capability == NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
return true;
}
return networkCapabilities.hasCapability(capability);
}
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
@RequiresApi(api = Build.VERSION_CODES.S)
public static void registerBestMatchingNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.of(handler),
() -> connectivityManager.registerBestMatchingNetworkCallback(request, networkCallback, handler)
);
}
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
@RequiresApi(api = Build.VERSION_CODES.N)
public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) {
Utils.networkCallback(
connectivityManager,
Utils.Option.empty(),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.empty(),
() -> connectivityManager.registerDefaultNetworkCallback(networkCallback)
);
}
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
@RequiresApi(api = Build.VERSION_CODES.O)
public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
Utils.networkCallback(
connectivityManager,
Utils.Option.empty(),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.of(handler),
() -> connectivityManager.registerDefaultNetworkCallback(networkCallback, handler)
);
}
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.empty(),
() -> connectivityManager.registerNetworkCallback(request, networkCallback)
);
}
// If it waits for Wifi connectivity, pretend it is fulfilled immediately.
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.empty(),
Utils.Option.of(operation),
Utils.Option.empty(),
() -> connectivityManager.registerNetworkCallback(request, operation)
);
}
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
@RequiresApi(api = Build.VERSION_CODES.O)
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.of(handler),
() -> connectivityManager.registerNetworkCallback(request, networkCallback, handler)
);
}
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.empty(),
() -> connectivityManager.requestNetwork(request, networkCallback)
);
}
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
@RequiresApi(api = Build.VERSION_CODES.O)
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, int timeoutMs) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.empty(),
() -> connectivityManager.requestNetwork(request, networkCallback, timeoutMs)
);
}
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
@RequiresApi(api = Build.VERSION_CODES.O)
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.of(handler),
() -> connectivityManager.requestNetwork(request, networkCallback, handler)
);
}
// If it requests Wifi connectivity, pretend it is fulfilled immediately.
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.empty(),
Utils.Option.of(operation),
Utils.Option.empty(),
() -> connectivityManager.requestNetwork(request, operation)
);
}
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
@RequiresApi(api = Build.VERSION_CODES.O)
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler, int timeoutMs) {
Utils.networkCallback(
connectivityManager,
Utils.Option.of(request),
Utils.Option.of(networkCallback),
Utils.Option.empty(),
Utils.Option.of(handler),
() -> connectivityManager.requestNetwork(request, networkCallback, handler, timeoutMs)
);
}
public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) {
try {
connectivityManager.unregisterNetworkCallback(networkCallback);
} catch (IllegalArgumentException ignore) {
// ignore: NetworkCallback was not registered
}
}
public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, PendingIntent operation) {
try {
connectivityManager.unregisterNetworkCallback(operation);
} catch (IllegalArgumentException ignore) {
// ignore: PendingIntent was not registered
}
}
private static class Utils {
private static class Option<T> {
private final T value;
private final boolean isPresent;
private Option(T value, boolean isPresent) {
this.value = value;
this.isPresent = isPresent;
}
private static <T> Option<T> of(T value) {
return new Option<>(value, true);
}
private static <T> Option<T> empty() {
return new Option<>(null, false);
}
}
private static void networkCallback(
ConnectivityManager connectivityManager,
Option<NetworkRequest> request,
Option<ConnectivityManager.NetworkCallback> networkCallback,
Option<PendingIntent> operation,
Option<Handler> handler,
Runnable fallback
) {
if(!request.isPresent || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && request.value != null && requestsWifiNetwork(request.value))) {
Runnable runnable = null;
if (networkCallback.isPresent && networkCallback.value != null) {
Network network = activeWifiNetwork(connectivityManager);
if (network != null) {
runnable = () -> networkCallback.value.onAvailable(network);
}
} else if (operation.isPresent && operation.value != null) {
runnable = () -> {
try {
operation.value.send();
} catch (PendingIntent.CanceledException ignore) {}
};
}
if (runnable != null) {
if (handler.isPresent) {
if (handler.value != null) {
handler.value.post(runnable);
return;
}
} else {
runnable.run();
return;
}
}
}
fallback.run();
}
// Returns an active (maybe fake) Wifi network if there is one, otherwise null.
private static Network activeWifiNetwork(ConnectivityManager connectivityManager) {
Network network = getActiveNetwork(connectivityManager);
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) {
return network;
}
return null;
}
// Whether a Wifi network with connection is requested.
@RequiresApi(api = Build.VERSION_CODES.P)
private static boolean requestsWifiNetwork(NetworkRequest request) {
return request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
&& (request.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|| request.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED));
}
}
}

View File

@@ -0,0 +1,5 @@
android.namespace = "app.revanced.extension"
dependencies {
compileOnly(libs.annotation)
}

View File

@@ -0,0 +1,21 @@
package app.revanced.extension.all.screencapture.removerestriction;
import android.media.AudioAttributes;
import android.os.Build;
import androidx.annotation.RequiresApi;
public final class RemoveScreencaptureRestrictionPatch {
// Member of AudioAttributes.Builder
@RequiresApi(api = Build.VERSION_CODES.Q)
public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) {
builder.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL);
return builder;
}
// Member of AudioManager static class
public static void setAllowedCapturePolicy(final int capturePolicy) {
// Ignore request
}
}

View File

@@ -0,0 +1 @@
android.namespace = "app.revanced.extension"

View File

@@ -0,0 +1,15 @@
package app.revanced.extension.all.screenshot.removerestriction;
import android.view.Window;
import android.view.WindowManager;
public class RemoveScreenshotRestrictionPatch {
public static void addFlags(Window window, int flags) {
window.addFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE);
}
public static void setFlags(Window window, int flags, int mask) {
window.setFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE, mask & ~WindowManager.LayoutParams.FLAG_SECURE);
}
}

View File

@@ -0,0 +1,4 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:boostforreddit:stub"))
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,26 @@
package app.revanced.extension.boostforreddit;
import com.rubenmayayo.reddit.ui.activities.WebViewActivity;
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
/**
* @noinspection unused
*/
public class FixSLinksPatch extends BaseFixSLinksPatch {
static {
INSTANCE = new FixSLinksPatch();
}
private FixSLinksPatch() {
webViewActivityClass = WebViewActivity.class;
}
public static boolean patchResolveSLink(String link) {
return INSTANCE.resolveSLink(link);
}
public static void patchSetAccessToken(String accessToken) {
INSTANCE.setAccessToken(accessToken);
}
}

View File

@@ -0,0 +1,17 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
}
android {
namespace = "app.revanced.extension"
compileSdk = 33
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,6 @@
package com.rubenmayayo.reddit.ui.activities;
import android.app.Activity;
public class WebViewActivity extends Activity {
}

9
extensions/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,9 @@
-dontobfuscate
-dontoptimize
-keepattributes *
-keep class app.revanced.** {
*;
}
-keep class com.google.** {
*;
}

View File

@@ -0,0 +1,3 @@
dependencies {
compileOnly(project(":extensions:reddit:stub"))
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,23 @@
package app.revanced.extension.patches;
import com.reddit.domain.model.ILink;
import java.util.ArrayList;
import java.util.List;
public final class FilterPromotedLinksPatch {
/**
* Filters list from promoted links.
**/
public static List<?> filterChildren(final Iterable<?> links) {
final List<Object> filteredList = new ArrayList<>();
for (Object item : links) {
if (item instanceof ILink && ((ILink) item).getPromoted()) continue;
filteredList.add(item);
}
return filteredList;
}
}

View File

@@ -0,0 +1,17 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
}
android {
namespace = "app.revanced.extension"
compileSdk = 33
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,7 @@
package com.reddit.domain.model;
public class ILink {
public boolean getPromoted() {
throw new UnsupportedOperationException("Stub");
}
}

View File

@@ -0,0 +1,3 @@
dependencies {
implementation(project(":extensions:shared:library"))
}

View File

@@ -0,0 +1,21 @@
plugins {
id("com.android.library")
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 23
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
compileOnly(libs.annotation)
}

View File

@@ -0,0 +1,158 @@
package app.revanced.extension.shared;
import static app.revanced.extension.shared.StringRef.str;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.RequiresApi;
import java.net.MalformedURLException;
import java.net.URL;
/**
* @noinspection unused
*/
public class GmsCoreSupport {
public static final String ORIGINAL_UNPATCHED_PACKAGE_NAME = "com.google.android.youtube";
private static final String GMS_CORE_PACKAGE_NAME
= getGmsCoreVendorGroupId() + ".android.gms";
private static final Uri GMS_CORE_PROVIDER
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
private static final String DONT_KILL_MY_APP_LINK
= "https://dontkillmyapp.com";
private static void open(String queryOrLink) {
Intent intent;
try {
// Check if queryOrLink is a valid URL.
new URL(queryOrLink);
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
} catch (MalformedURLException e) {
intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.putExtra(SearchManager.QUERY, queryOrLink);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Utils.getContext().startActivity(intent);
// Gracefully exit, otherwise the broken app will continue to run.
System.exit(0);
}
private static void showBatteryOptimizationDialog(Activity context,
String dialogMessageRef,
String positiveButtonStringRef,
DialogInterface.OnClickListener onPositiveClickListener) {
// Do not set cancelable to false, to allow using back button to skip the action,
// just in case the check can never be satisfied.
var dialog = new AlertDialog.Builder(context)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageRef))
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
.create();
Utils.showDialog(context, dialog);
}
/**
* 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.
// GmsCore Support changes the package name, but with a mounted installation
// all manifest changes are ignored and the original package name is used.
if (context.getPackageName().equals(ORIGINAL_UNPATCHED_PACKAGE_NAME)) {
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
// Cannot use localize text here, since the app will load
// resources from the unpatched app and all patch strings are missing.
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
// Do not exit. If the app exits before launch completes (and without
// opening another activity), then on some devices such as Pixel phone Android 10
// no toast will be shown and the app will continually be relaunched
// with the appearance of a hung app.
}
// Verify GmsCore is installed.
try {
PackageManager manager = context.getPackageManager();
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printInfo(() -> "GmsCore was not found");
// Cannot show a dialog and must show a toast,
// because on some installations the app crashes before a dialog can be displayed.
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
open(getGmsCoreDownload());
return;
}
// Check if GmsCore is running in the background.
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
if (client == null) {
Logger.printInfo(() -> "GmsCore is not running in the background");
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
"gms_core_dialog_open_website_text",
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
return;
}
}
// Check if GmsCore is whitelisted from battery optimizations.
if (batteryOptimizationsEnabled(context)) {
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
"gms_core_dialog_continue_text",
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
}
} catch (Exception ex) {
Logger.printException(() -> "checkGmsCore failure", ex);
}
}
@SuppressLint("BatteryLife") // Permission is part of GmsCore
private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null));
activity.startActivityForResult(intent, 0);
}
/**
* @return If GmsCore is not whitelisted from battery optimizations.
*/
private static boolean batteryOptimizationsEnabled(Context context) {
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
}
private static String getGmsCoreDownload() {
final var vendorGroupId = getGmsCoreVendorGroupId();
//noinspection SwitchStatementWithTooFewBranches
switch (vendorGroupId) {
case "app.revanced":
return "https://github.com/revanced/gmscore/releases/latest";
default:
return vendorGroupId + ".android.gms";
}
}
// Modified by a patch. Do not touch.
private static String getGmsCoreVendorGroupId() {
return "app.revanced";
}
}

View File

@@ -0,0 +1,156 @@
package app.revanced.extension.shared;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.settings.BaseSettings;
import java.io.PrintWriter;
import java.io.StringWriter;
import static app.revanced.extension.shared.settings.BaseSettings.*;
public class Logger {
/**
* Log messages using lambdas.
*/
@FunctionalInterface
public interface LogMessage {
@NonNull
String buildMessageString();
/**
* @return For outer classes, this returns {@link Class#getSimpleName()}.
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
* <br>
* For example, each of these classes return 'SomethingView':
* <code>
* com.company.SomethingView
* com.company.SomethingView$StaticClass
* com.company.SomethingView$1
* </code>
*/
private String findOuterClassSimpleName() {
var selfClass = this.getClass();
String fullClassName = selfClass.getName();
final int dollarSignIndex = fullClassName.indexOf('$');
if (dollarSignIndex < 0) {
return selfClass.getSimpleName(); // Already an outer class.
}
// Class is inner, static, or anonymous.
// Parse the simple name full name.
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
}
}
private static final String REVANCED_LOG_PREFIX = "revanced: ";
/**
* Logs debug messages under the outer class name of the code calling this method.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message) {
printDebug(message, null);
}
/**
* Logs debug messages under the outer class name of the code calling this method.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) {
String logMessage = message.buildMessageString();
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
if (DEBUG_STACKTRACE.get()) {
var builder = new StringBuilder(logMessage);
var sw = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(sw));
builder.append('\n').append(sw);
logMessage = builder.toString();
}
if (ex == null) {
Log.d(logTag, logMessage);
} else {
Log.d(logTag, logMessage, ex);
}
}
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
public static void printInfo(@NonNull LogMessage message) {
printInfo(message, null);
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
String logMessage = message.buildMessageString();
if (ex == null) {
Log.i(logTag, logMessage);
} else {
Log.i(logTag, logMessage, ex);
}
}
/**
* Logs exceptions under the outer class name of the code calling this method.
*/
public static void printException(@NonNull LogMessage message) {
printException(message, null);
}
/**
* Logs exceptions under the outer class name of the code calling this method.
* <p>
* If the calling code is showing it's own error toast,
* instead use {@link #printInfo(LogMessage, Exception)}
*
* @param message log message
* @param ex exception (optional)
*/
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
String messageString = message.buildMessageString();
String outerClassSimpleName = message.findOuterClassSimpleName();
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
if (ex == null) {
Log.e(logMessage, messageString);
} else {
Log.e(logMessage, messageString, ex);
}
if (DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
}
}
/**
* 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(@NonNull Class<?> callingClass, @NonNull String message) {
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
}
/**
* 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(@NonNull Class<?> callingClass, @NonNull String message,
@Nullable Exception ex) {
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
}
}

View File

@@ -0,0 +1,122 @@
package app.revanced.extension.shared;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class StringRef {
private static Resources resources;
private static String packageName;
// must use a thread safe map, as this class is used both on and off the main thread
private static final Map<String, StringRef> strings = Collections.synchronizedMap(new HashMap<>());
/**
* Returns a cached instance.
* Should be used if the same String could be loaded more than once.
*
* @param id string resource name/id
* @see #sf(String)
*/
@NonNull
public static StringRef sfc(@NonNull String id) {
StringRef ref = strings.get(id);
if (ref == null) {
ref = new StringRef(id);
strings.put(id, ref);
}
return ref;
}
/**
* Creates a new instance, but does not cache the value.
* Should be used for Strings that are loaded exactly once.
*
* @param id string resource name/id
* @see #sfc(String)
*/
@NonNull
public static StringRef sf(@NonNull String id) {
return new StringRef(id);
}
/**
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code>
*
* @param id string resource name/id
* @return String value from string.xml
*/
@NonNull
public static String str(@NonNull String id) {
return sfc(id).toString();
}
/**
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code> and formats the string
* with given args.
*
* @param id string resource name/id
* @param args the args to format the string with
* @return String value from string.xml formatted with given args
*/
@NonNull
public static String str(@NonNull String id, Object... args) {
return String.format(str(id), args);
}
/**
* Creates a StringRef object that'll not change it's value
*
* @param value value which toString() method returns when invoked on returned object
* @return Unique StringRef instance, its value will never change
*/
@NonNull
public static StringRef constant(@NonNull String value) {
final StringRef ref = new StringRef(value);
ref.resolved = true;
return ref;
}
/**
* Shorthand for <code>constant("")</code>
* Its value always resolves to empty string
*/
@NonNull
public static final StringRef empty = constant("");
@NonNull
private String value;
private boolean resolved;
public StringRef(@NonNull String resName) {
this.value = resName;
}
@Override
@NonNull
public String toString() {
if (!resolved) {
if (resources == null || packageName == null) {
Context context = Utils.getContext();
resources = context.getResources();
packageName = context.getPackageName();
}
resolved = true;
if (resources != null) {
final int identifier = resources.getIdentifier(value, "string", packageName);
if (identifier == 0)
Logger.printException(() -> "Resource not found: " + value);
else
value = resources.getString(identifier);
} else {
Logger.printException(() -> "Could not resolve resources!");
}
}
return value;
}
}

View File

@@ -0,0 +1,770 @@
package app.revanced.extension.shared;
import android.annotation.SuppressLint;
import android.app.*;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.Bidi;
import java.util.*;
import java.util.regex.Pattern;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
public class Utils {
@SuppressLint("StaticFieldLeak")
private static Context context;
private static String versionName;
private Utils() {
} // utility class
/**
* Injection point.
*
* @return The manifest 'Version' entry of the patches.jar used during patching.
*/
@SuppressWarnings("SameReturnValue")
public static String getPatchesReleaseVersion() {
return ""; // Value is replaced during patching.
}
/**
* @return The version name of the app, such as 19.11.43
*/
public static String getAppVersionName() {
if (versionName == null) {
try {
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageInfo = packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
);
} else {
packageInfo = packageManager.getPackageInfo(
packageName,
0
);
}
versionName = packageInfo.versionName;
} catch (Exception ex) {
Logger.printException(() -> "Failed to get package info", ex);
versionName = "Unknown";
}
}
return versionName;
}
/**
* Hide a view by setting its layout height and width to 1dp.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
/**
* Hide a view by setting its layout height and width to 0dp.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
if (condition) {
hideViewByLayoutParams(view);
return true;
}
return false;
}
/**
* Hide a view by setting its visibility to GONE.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
if (hideViewUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
/**
* Hide a view by setting its visibility to GONE.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static boolean hideViewUnderCondition(boolean condition, View view) {
if (condition) {
view.setVisibility(View.GONE);
return true;
}
return false;
}
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) {
if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
if (setting) {
ViewParent parent = view.getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(view);
return true;
}
}
return false;
}
/**
* General purpose pool for network calls and other background tasks.
* All tasks run at max thread priority.
*/
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
3, // 3 threads always ready to go
Integer.MAX_VALUE,
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
TimeUnit.SECONDS,
new SynchronousQueue<>(),
r -> { // ThreadFactory
Thread t = new Thread(r);
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
return t;
});
public static void runOnBackgroundThread(@NonNull Runnable task) {
backgroundThreadPool.execute(task);
}
@NonNull
public static <T> Future<T> submitOnBackgroundThread(@NonNull Callable<T> call) {
return backgroundThreadPool.submit(call);
}
/**
* Simulates a delay by doing meaningless calculations.
* Used for debugging to verify UI timeout logic.
*/
@SuppressWarnings("UnusedReturnValue")
public static long doNothingForDuration(long amountOfTimeToWaste) {
final long timeCalculationStarted = System.currentTimeMillis();
Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
long meaninglessValue = 0;
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
}
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
// leaving an empty loop that hammers on the System.currentTimeMillis native call
return meaninglessValue;
}
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
return indexOfFirstFound(value, targets) >= 0;
}
public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) {
for (String string : targets) {
if (!string.isEmpty()) {
final int indexOf = value.indexOf(string);
if (indexOf >= 0) return indexOf;
}
}
return -1;
}
/**
* @return zero, if the resource is not found
*/
@SuppressLint("DiscouragedApi")
public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) {
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
}
/**
* @return zero, if the resource is not found
*/
public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) {
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
}
public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
}
@NonNull
public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
}
public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
//noinspection deprecation
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
}
public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
}
public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
}
public interface MatchFilter<T> {
boolean matches(T object);
}
/**
* Includes sub children.
*
* @noinspection unchecked
*/
public static <R extends View> R getChildViewByResourceName(@NonNull View view, @NonNull String str) {
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
if (child != null) {
return (R) child;
}
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
}
/**
* @param searchRecursively If children ViewGroups should also be
* recursively searched using depth first search.
* @return The first child view that matches the filter.
*/
@Nullable
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively,
@NonNull MatchFilter<View> filter) {
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
View childAt = viewGroup.getChildAt(i);
if (filter.matches(childAt)) {
//noinspection unchecked
return (T) childAt;
}
// Must do recursive after filter check, in case the filter is looking for a ViewGroup.
if (searchRecursively && childAt instanceof ViewGroup) {
T match = getChildView((ViewGroup) childAt, true, filter);
if (match != null) return match;
}
}
return null;
}
@Nullable
public static ViewParent getParentView(@NonNull View view, int nthParent) {
ViewParent parent = view.getParent();
int currentDepth = 0;
while (++currentDepth < nthParent && parent != null) {
parent = parent.getParent();
}
if (currentDepth == nthParent) {
return parent;
}
final int currentDepthLog = currentDepth;
Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent
+ " and instead found at: " + currentDepthLog + " view: " + view);
return null;
}
public static void restartApp(@NonNull Context context) {
String packageName = context.getPackageName();
Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
// Required for API 34 and later
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
mainIntent.setPackage(packageName);
context.startActivity(mainIntent);
System.exit(0);
}
public static Context getContext() {
if (context == null) {
Logger.initializationException(Utils.class, "Context is null, returning null!", null);
}
return context;
}
public static void setContext(Context appContext) {
context = appContext;
// In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies.
// Calling the regular printDebug method here can cause a Settings context null pointer exception,
// even though the context is already set before the call.
//
// The initialization logger methods do not directly or indirectly
// reference the Context or any Settings and are unaffected by this problem.
//
// Info level also helps debug if a patch hook is called before
// the context is set since debug logging is off by default.
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
}
public static void setClipboard(@NonNull String text) {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
clipboard.setPrimaryClip(clip);
}
public static boolean isTablet() {
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
}
@Nullable
private static Boolean isRightToLeftTextLayout;
/**
* If the device language uses right to left text layout (hebrew, arabic, etc)
*/
public static boolean isRightToLeftTextLayout() {
if (isRightToLeftTextLayout == null) {
String displayLanguage = Locale.getDefault().getDisplayLanguage();
isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
}
return isRightToLeftTextLayout;
}
/**
* @return if the text contains at least 1 number character,
* including any unicode numbers such as Arabic.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean containsNumber(@NonNull CharSequence text) {
for (int index = 0, length = text.length(); index < length;) {
final int codePoint = Character.codePointAt(text, index);
if (Character.isDigit(codePoint)) {
return true;
}
index += Character.charCount(codePoint);
}
return false;
}
/**
* Ignore this class. It must be public to satisfy Android requirements.
*/
@SuppressWarnings("deprecation")
public static final class DialogFragmentWrapper extends DialogFragment {
private Dialog dialog;
@Nullable
private DialogFragmentOnStartAction onStartAction;
@Override
public void onSaveInstanceState(Bundle outState) {
// Do not call super method to prevent state saving.
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return dialog;
}
@Override
public void onStart() {
try {
super.onStart();
if (onStartAction != null) {
onStartAction.onStart((AlertDialog) getDialog());
}
} catch (Exception ex) {
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
}
}
}
/**
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
*/
@FunctionalInterface
public interface DialogFragmentOnStartAction {
void onStart(AlertDialog dialog);
}
public static void showDialog(Activity activity, AlertDialog dialog) {
showDialog(activity, dialog, true, null);
}
/**
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
* Calling this will always display the dialog on top of all other dialogs
* previously called using this method.
* <br>
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
*<br>
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
*<br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/
@SuppressWarnings("deprecation")
public static void showDialog(Activity activity,
AlertDialog dialog,
boolean isCancelable,
@Nullable DialogFragmentOnStartAction onStartAction) {
verifyOnMainThread();
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
fragment.dialog = dialog;
fragment.onStartAction = onStartAction;
fragment.setCancelable(isCancelable);
fragment.show(activity.getFragmentManager(), null);
}
/**
* Safe to call from any thread
*/
public static void showToastShort(@NonNull String messageToToast) {
showToast(messageToToast, Toast.LENGTH_SHORT);
}
/**
* Safe to call from any thread
*/
public static void showToastLong(@NonNull String messageToToast) {
showToast(messageToToast, Toast.LENGTH_LONG);
}
private static void showToast(@NonNull String messageToToast, int toastDuration) {
Objects.requireNonNull(messageToToast);
runOnMainThreadNowOrLater(() -> {
if (context == null) {
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
} else {
Logger.printDebug(() -> "Showing toast: " + messageToToast);
Toast.makeText(context, messageToToast, toastDuration).show();
}
}
);
}
/**
* Automatically logs any exceptions the runnable throws.
*
* @see #runOnMainThreadNowOrLater(Runnable)
*/
public static void runOnMainThread(@NonNull Runnable runnable) {
runOnMainThreadDelayed(runnable, 0);
}
/**
* Automatically logs any exceptions the runnable throws
*/
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
Runnable loggingRunnable = () -> {
try {
runnable.run();
} catch (Exception ex) {
Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex);
}
};
new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
}
/**
* If called from the main thread, the code is run immediately.<p>
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
*/
public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
if (isCurrentlyOnMainThread()) {
runnable.run();
} else {
runOnMainThread(runnable);
}
}
/**
* @return if the calling thread is on the main thread
*/
public static boolean isCurrentlyOnMainThread() {
return Looper.getMainLooper().isCurrentThread();
}
/**
* @throws IllegalStateException if the calling thread is _off_ the main thread
*/
public static void verifyOnMainThread() throws IllegalStateException {
if (!isCurrentlyOnMainThread()) {
throw new IllegalStateException("Must call _on_ the main thread");
}
}
/**
* @throws IllegalStateException if the calling thread is _on_ the main thread
*/
public static void verifyOffMainThread() throws IllegalStateException {
if (isCurrentlyOnMainThread()) {
throw new IllegalStateException("Must call _off_ the main thread");
}
}
public enum NetworkType {
NONE,
MOBILE,
OTHER,
}
public static boolean isNetworkConnected() {
NetworkType networkType = getNetworkType();
return networkType == NetworkType.MOBILE
|| networkType == NetworkType.OTHER;
}
@SuppressLint("MissingPermission") // permission already included in YouTube
public static NetworkType getNetworkType() {
Context networkContext = getContext();
if (networkContext == null) {
return NetworkType.NONE;
}
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
var networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected()) {
return NetworkType.NONE;
}
var type = networkInfo.getType();
return (type == ConnectivityManager.TYPE_MOBILE)
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
}
/**
* Hide a view by setting its layout params to 0x0
* @param view The view to hide.
*/
public static void hideViewByLayoutParams(View view) {
if (view instanceof LinearLayout) {
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams);
} else if (view instanceof FrameLayout) {
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams2);
} else if (view instanceof RelativeLayout) {
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams3);
} else if (view instanceof Toolbar) {
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0);
view.setLayoutParams(layoutParams4);
} else if (view instanceof ViewGroup) {
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0);
view.setLayoutParams(layoutParams5);
} else {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.width = 0;
params.height = 0;
view.setLayoutParams(params);
}
}
/**
* {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles.
*/
private enum Sort {
/**
* Sort by the localized preference title.
*/
BY_TITLE("_sort_by_title"),
/**
* Sort by the preference keys.
*/
BY_KEY("_sort_by_key"),
/**
* Unspecified sorting.
*/
UNSORTED("_sort_by_unsorted");
final String keySuffix;
Sort(String keySuffix) {
this.keySuffix = keySuffix;
}
@NonNull
static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) {
if (key != null) {
for (Sort sort : values()) {
if (key.endsWith(sort.keySuffix)) {
return sort;
}
}
}
return defaultSort;
}
}
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
/**
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
*/
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
if (original == null) return "";
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
}
/**
* Sort a PreferenceGroup and all it's sub groups by title or key.
*
* Sort order is determined by the preferences key {@link Sort} suffix.
*
* If a preference has no key or no {@link Sort} suffix,
* then the preferences are left unsorted.
*/
@SuppressWarnings("deprecation")
public static void sortPreferenceGroups(@NonNull PreferenceGroup group) {
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
SortedMap<String, Preference> preferences = new TreeMap<>();
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference preference = group.getPreference(i);
final Sort preferenceSort;
if (preference instanceof PreferenceGroup) {
sortPreferenceGroups((PreferenceGroup) preference);
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
} else {
// Allow individual preferences to set a key sorting.
// Used to force a preference to the top or bottom of a group.
preferenceSort = Sort.fromKey(preference.getKey(), groupSort);
}
final String sortValue;
switch (preferenceSort) {
case BY_TITLE:
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
break;
case BY_KEY:
sortValue = preference.getKey();
break;
case UNSORTED:
continue; // Keep original sorting.
default:
throw new IllegalStateException();
}
preferences.put(sortValue, preference);
}
int index = 0;
for (Preference pref : preferences.values()) {
int order = index++;
// Move any screens, intents, and the one off About preference to the top.
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|| pref.getIntent() != null) {
// Arbitrary high number.
order -= 1000;
}
pref.setOrder(order);
}
}
/**
* Set all preferences to multiline titles if the device is not using an English variant.
* The English strings are heavily scrutinized and all titles fit on screen
* except 2 or 3 preference strings and those do not affect readability.
*
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
* and visually it looks better to clip the text and keep all titles 1 line.
*/
@SuppressWarnings("deprecation")
public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
String deviceLanguage = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
if (deviceLanguage.equals("en")) {
return;
}
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference pref = group.getPreference(i);
pref.setSingleLineTitle(false);
if (pref instanceof PreferenceGroup) {
setPreferenceTitlesToMultiLineIfNeeded((PreferenceGroup) pref);
}
}
}
/**
* If {@link Fragment} uses [Android library] rather than [AndroidX library],
* the Dialog theme corresponding to [Android library] should be used.
* <p>
* If not, the following issues will occur:
* <a href="https://github.com/ReVanced/revanced-patches/issues/3061">ReVanced/revanced-patches#3061</a>
* <p>
* To prevent these issues, apply the Dialog theme corresponding to [Android library].
*/
public static void setEditTextDialogTheme(AlertDialog.Builder builder) {
final int editTextDialogStyle = getResourceIdentifier(
"revanced_edit_text_dialog_style", "style");
if (editTextDialogStyle != 0) {
builder.getContext().setTheme(editTextDialogStyle);
}
}
}

View File

@@ -0,0 +1,164 @@
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 android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.text.Html;
import android.widget.Button;
import androidx.annotation.Nullable;
import java.util.Collection;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15;
private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10;
private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app");
/**
* @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed.
*/
@Nullable
protected abstract Boolean check();
protected abstract String failureReason();
/**
* Specifies a sorting order for displaying the checks that failed.
* A lower value indicates to show first before other checks.
*/
public abstract int uiSortingValue();
/**
* For debugging and development only.
* Forces all checks to be performed and the check failed dialog to be shown.
* Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
* set to -1.
*/
static boolean debugAlwaysShowWarning() {
final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
if (alwaysShowWarning) {
Logger.printInfo(() -> "Debug forcing environment check warning to show");
}
return alwaysShowWarning;
}
static boolean shouldRun() {
return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
}
static void disableForever() {
Logger.printInfo(() -> "Environment checks disabled forever");
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
}
@SuppressLint("NewApi")
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
final var reasons = new StringBuilder();
reasons.append("<ul>");
for (var check : failedChecks) {
// Add a non breaking space to fix bullet points spacing issue.
reasons.append("<li>&nbsp;").append(check.failureReason());
}
reasons.append("</ul>");
var message = Html.fromHtml(
str("revanced_check_environment_failed_message", reasons.toString()),
FROM_HTML_MODE_COMPACT
);
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);
// 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);
dialog.dismiss();
}
).create();
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
boolean hasRun;
@Override
public void onStart(AlertDialog 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) {
return;
}
hasRun = true;
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
openWebsiteButton.setEnabled(false);
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
dismissButton.setEnabled(false);
getCountdownRunnable(dismissButton, 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) {
return new Runnable() {
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
@Override
public void run() {
Utils.verifyOnMainThread();
if (secondsRemaining > 0) {
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
openWebsiteButton.setEnabled(true);
}
secondsRemaining--;
Utils.runOnMainThreadDelayed(this, 1000);
} else {
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
dismissButton.setEnabled(true);
}
}
};
}
}

View File

@@ -0,0 +1,341 @@
package app.revanced.extension.shared.checks;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.checks.Check.debugAlwaysShowWarning;
import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
/**
* This class is used to check if the app was patched by the user
* and not downloaded pre-patched, because pre-patched apps are difficult to trust.
* <br>
* Various indicators help to detect if the app was patched by the user.
*/
@SuppressWarnings("unused")
public final class CheckEnvironmentPatch {
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
private enum InstallationType {
/**
* CLI patching, manual installation of a previously patched using adb,
* or root installation if stock app is first installed using adb.
*/
ADB((String) null),
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
MANAGER("app.revanced.manager.flutter",
"app.revanced.manager",
"app.revanced.manager.debug");
@Nullable
static InstallationType installTypeFromPackageName(@Nullable String packageName) {
for (InstallationType type : values()) {
for (String installPackageName : type.packageNames) {
if (Objects.equals(installPackageName, packageName)) {
return type;
}
}
}
return null;
}
/**
* Array elements can be null.
*/
final String[] packageNames;
InstallationType(String... packageNames) {
this.packageNames = packageNames;
}
}
/**
* Check if the app is installed by the manager, the app store, or through adb/CLI.
* <br>
* Does not conclusively
* If the app is installed by the manager or the app store, it is likely, the app was patched using the manager,
* or installed manually via ADB (in the case of ReVanced CLI for example).
* <br>
* If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched
* and installed by the browser or another unknown app.
*/
private static class CheckExpectedInstaller extends Check {
@Nullable
InstallationType installerFound;
@NonNull
@Override
protected Boolean check() {
final var context = Utils.getContext();
final var installerPackageName =
context.getPackageManager().getInstallerPackageName(context.getPackageName());
Logger.printInfo(() -> "Installed by: " + installerPackageName);
installerFound = InstallationType.installTypeFromPackageName(installerPackageName);
final boolean passed = (installerFound != null);
Logger.printInfo(() -> passed
? "Apk was not installed from an unknown source"
: "Apk was installed from an unknown source");
return passed;
}
@Override
protected String failureReason() {
return str("revanced_check_environment_manager_not_expected_installer");
}
@Override
public int uiSortingValue() {
return -100; // Show first.
}
}
/**
* Check if the build properties are the same as during the patch.
* <br>
* If the build properties are the same as during the patch, it is likely, the app was patched on the same device.
* <br>
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
*/
private static class CheckWasPatchedOnSameDevice extends Check {
@SuppressLint({"NewApi", "HardwareIds"})
@Override
protected Boolean check() {
if (PATCH_BOARD.isEmpty()) {
// Did not patch with Manager, and cannot conclusively say where this was from.
Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device");
return null;
}
//noinspection deprecation
final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) &
buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) &
buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) &
buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) &
buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) &
buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) &
buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) &
buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) &
buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) &
buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) &
buildFieldEqualsHash("ID", Build.ID, PATCH_ID) &
buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) &
buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) &
buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) &
buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) &
buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) &
buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) &
buildFieldEqualsHash("USER", Build.USER, PATCH_USER);
Logger.printInfo(() -> passed
? "Device hardware signature matches current device"
: "Device hardware signature does not match current device");
return passed;
}
@Override
protected String failureReason() {
return str("revanced_check_environment_not_same_patching_device");
}
@Override
public int uiSortingValue() {
return 0; // Show in the middle.
}
}
/**
* Check if the app was installed within the last 30 minutes after being patched.
* <br>
* If the app was installed within the last 30 minutes, it is likely, the app was patched by the user.
* <br>
* If the app was installed much later than the patch time, it is likely the app was
* downloaded pre-patched or the user waited too long to install the app.
*/
private static class CheckIsNearPatchTime extends Check {
/**
* How soon after patching the app must be installed to pass.
*/
static final int INSTALL_AFTER_PATCHING_DURATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes.
/**
* Milliseconds between the time the app was patched, and when it was installed/updated.
*/
long durationBetweenPatchingAndInstallation;
@NonNull
@Override
protected Boolean check() {
try {
Context context = Utils.getContext();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
// Duration since initial install or last update, which ever is sooner.
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
Logger.printInfo(() -> "App was installed/updated: "
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
if (durationBetweenPatchingAndInstallation < 0) {
// Patch time is in the future and clearly wrong.
return false;
}
if (durationBetweenPatchingAndInstallation < INSTALL_AFTER_PATCHING_DURATION_THRESHOLD) {
return true;
}
} catch (PackageManager.NameNotFoundException ex) {
Logger.printException(() -> "Package name not found exception", ex); // Will never happen.
}
// User installed more than 30 minutes after patching.
return false;
}
@Override
protected String failureReason() {
if (durationBetweenPatchingAndInstallation < 0) {
// Could happen if the user has their device clock incorrectly set in the past,
// but assume that isn't the case and the apk was patched on a device with the wrong system time.
return str("revanced_check_environment_not_near_patch_time_invalid");
}
// If patched over 1 day ago, show how old this pre-patched apk is.
// Showing the age can help convey it's better to patch yourself and know it's the latest.
final long oneDay = 24 * 60 * 60 * 1000;
final long daysSincePatching = durationBetweenPatchingAndInstallation / oneDay;
if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings.
return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching);
}
return str("revanced_check_environment_not_near_patch_time");
}
@Override
public int uiSortingValue() {
return 100; // Show last.
}
}
/**
* Injection point.
*/
public static void check(Activity context) {
// If the warning was already issued twice, or if the check was successful in the past,
// do not run the checks again.
if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
Logger.printDebug(() -> "Environment checks are disabled");
return;
}
Utils.runOnBackgroundThread(() -> {
try {
Logger.printInfo(() -> "Running environment checks");
List<Check> failedChecks = new ArrayList<>();
CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice();
Boolean hardwareCheckPassed = sameHardware.check();
if (hardwareCheckPassed != null) {
if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Patched on the same device using Manager,
// and no further checks are needed.
Check.disableForever();
return;
}
failedChecks.add(sameHardware);
}
CheckExpectedInstaller installerCheck = new CheckExpectedInstaller();
if (installerCheck.check() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// If the installer package is Manager but this code is reached,
// that means it must not be the right Manager otherwise the hardware hash
// signatures would be present and this check would not have run.
if (installerCheck.installerFound == InstallationType.MANAGER) {
failedChecks.add(installerCheck);
// Also could not have been patched on this device.
failedChecks.add(sameHardware);
} else if (failedChecks.isEmpty()) {
// ADB install of CLI build. Allow even if patched a long time ago.
Check.disableForever();
return;
}
} else {
failedChecks.add(installerCheck);
}
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
Boolean timeCheckPassed = nearPatchTime.check();
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Allow installing recently patched apks,
// even if the install source is not Manager or ADB.
Check.disableForever();
return;
} else {
failedChecks.add(nearPatchTime);
}
if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Show all failures for debugging layout.
failedChecks = Arrays.asList(
sameHardware,
nearPatchTime,
installerCheck
);
}
//noinspection ComparatorCombinators
Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue());
Check.issueWarning(
context,
failedChecks
);
} catch (Exception ex) {
Logger.printException(() -> "check failure", ex);
}
});
}
private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) {
try {
final var sha1 = MessageDigest.getInstance("SHA-1")
.digest(buildFieldValue.getBytes(StandardCharsets.UTF_8));
// Must be careful to use same base64 encoding Kotlin uses.
String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1);
final boolean equals = runtimeHash.equals(hash);
if (!equals) {
Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue
+ "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'");
}
return equals;
} catch (NoSuchAlgorithmException ex) {
Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen.
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
package app.revanced.extension.shared.checks;
/**
* Fields are set by the patch. Do not modify.
* Fields are not final, because the compiler is inlining them.
*
* @noinspection CanBeFinal
*/
final class PatchInfo {
static long PATCH_TIME = 0L;
final static class Build {
static String PATCH_BOARD = "";
static String PATCH_BOOTLOADER = "";
static String PATCH_BRAND = "";
static String PATCH_CPU_ABI = "";
static String PATCH_CPU_ABI2 = "";
static String PATCH_DEVICE = "";
static String PATCH_DISPLAY = "";
static String PATCH_FINGERPRINT = "";
static String PATCH_HARDWARE = "";
static String PATCH_HOST = "";
static String PATCH_ID = "";
static String PATCH_MANUFACTURER = "";
static String PATCH_MODEL = "";
static String PATCH_PRODUCT = "";
static String PATCH_RADIO = "";
static String PATCH_TAGS = "";
static String PATCH_TYPE = "";
static String PATCH_USER = "";
}
}

View File

@@ -0,0 +1,208 @@
package app.revanced.extension.shared.fixes.slink;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Objects;
import static app.revanced.extension.shared.Utils.getContext;
/**
* Base class to implement /s/ link resolution in 3rd party Reddit apps.
* <br>
* <br>
* Usage:
* <br>
* <br>
* An implementation of this class must have two static methods that are called by the app:
* <ul>
* <li>public static boolean patchResolveSLink(String link)</li>
* <li>public static void patchSetAccessToken(String accessToken)</li>
* </ul>
* The static methods must call the instance methods of the base class.
* <br>
* The singleton pattern can be used to access the instance of the class:
* <pre>
* {@code
* {
* INSTANCE = new FixSLinksPatch();
* }
* }
* </pre>
* Set the app's web view activity class as a fallback to open /s/ links if the resolution fails:
* <pre>
* {@code
* private FixSLinksPatch() {
* webViewActivityClass = WebViewActivity.class;
* }
* }
* </pre>
* Hook the app's navigation handler to call this method before doing any of its own resolution:
* <pre>
* {@code
* public static boolean patchResolveSLink(Context context, String link) {
* return INSTANCE.resolveSLink(context, link);
* }
* }
* </pre>
* If this method returns true, the app should early return and not do any of its own resolution.
* <br>
* <br>
* Hook the app's access token so that this class can use it to resolve /s/ links:
* <pre>
* {@code
* public static void patchSetAccessToken(String accessToken) {
* INSTANCE.setAccessToken(access_token);
* }
* }
* </pre>
*/
public abstract class BaseFixSLinksPatch {
/**
* The class of the activity used to open links in a web view if resolving them fails.
*/
protected Class<? extends Activity> webViewActivityClass;
/**
* The access token used to resolve the /s/ link.
*/
protected String accessToken;
/**
* The URL that was trying to be resolved before the access token was set.
* If this is not null, the URL will be resolved right after the access token is set.
*/
protected String pendingUrl;
/**
* The singleton instance of the class.
*/
protected static BaseFixSLinksPatch INSTANCE;
public boolean resolveSLink(String link) {
switch (resolveLink(link)) {
case ACCESS_TOKEN_START: {
pendingUrl = link;
return true;
}
case DO_NOTHING:
return true;
default:
return false;
}
}
private ResolveResult resolveLink(String link) {
Context context = getContext();
if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) {
// A link ends with #bypass if it failed to resolve below.
// resolveLink is called with the same link again but this time with #bypass
// so that the link is opened in the app browser instead of trying to resolve it again.
if (link.endsWith("#bypass")) {
openInAppBrowser(context, link);
return ResolveResult.DO_NOTHING;
}
Logger.printDebug(() -> "Resolving " + link);
if (accessToken == null) {
// This is not optimal.
// However, an accessToken is necessary to make an authenticated request to Reddit.
// in case Reddit has banned the IP - e.g. VPN.
Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
context.startActivity(startIntent);
return ResolveResult.ACCESS_TOKEN_START;
}
Utils.runOnBackgroundThread(() -> {
String bypassLink = link + "#bypass";
String finalLocation = bypassLink;
try {
HttpURLConnection connection = getHttpURLConnection(link, accessToken);
connection.connect();
String location = connection.getHeaderField("location");
connection.disconnect();
Objects.requireNonNull(location, "Location is null");
finalLocation = location;
Logger.printDebug(() -> "Resolved " + link + " to " + location);
} catch (SocketTimeoutException e) {
Logger.printException(() -> "Timeout when trying to resolve " + link, e);
finalLocation = bypassLink;
} catch (Exception e) {
Logger.printException(() -> "Failed to resolve " + link, e);
finalLocation = bypassLink;
} finally {
Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation));
startIntent.setPackage(context.getPackageName());
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(startIntent);
}
});
return ResolveResult.DO_NOTHING;
}
return ResolveResult.CONTINUE;
}
public void setAccessToken(String accessToken) {
Logger.printDebug(() -> "Setting access token");
this.accessToken = accessToken;
// In case a link was trying to be resolved before access token was set.
// The link is resolved now, after the access token is set.
if (pendingUrl != null) {
String link = pendingUrl;
pendingUrl = null;
Logger.printDebug(() -> "Opening pending URL");
resolveLink(link);
}
}
private void openInAppBrowser(Context context, String link) {
Intent intent = new Intent(context, webViewActivityClass);
intent.putExtra("url", link);
context.startActivity(intent);
}
@NonNull
private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException {
URL url = new URL(link);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("HEAD");
connection.setConnectTimeout(2000);
connection.setReadTimeout(2000);
if (accessToken != null) {
Logger.printDebug(() -> "Setting access token to make /s/ request");
connection.setRequestProperty("Authorization", "Bearer " + accessToken);
} else {
Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null");
}
return connection;
}
}

View File

@@ -0,0 +1,10 @@
package app.revanced.extension.shared.fixes.slink;
public enum ResolveResult {
// Let app handle rest of stuff
CONTINUE,
// Start app, to make it cache its access_token
ACCESS_TOKEN_START,
// Don't do anything - we started resolving
DO_NOTHING
}

View File

@@ -0,0 +1,145 @@
package app.revanced.extension.shared.requests;
import app.revanced.extension.shared.Utils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class Requester {
private Requester() {
}
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
return getConnectionFromCompiledRoute(apiUrl, route.compile(params));
}
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
String url = apiUrl + route.getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// Request data is in the URL parameters and no body is sent.
// The calling code must set a length if using a request body.
connection.setFixedLengthStreamingMode(0);
connection.setRequestMethod(route.getMethod().name());
String agentString = System.getProperty("http.agent")
+ "; ReVanced/" + Utils.getAppVersionName()
+ " (" + Utils.getPatchesReleaseVersion() + ")";
connection.setRequestProperty("User-Agent", agentString);
return connection;
}
/**
* Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
*/
private static String parseInputStreamAndClose(InputStream inputStream) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
StringBuilder jsonBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
jsonBuilder.append(line);
jsonBuilder.append('\n');
}
return jsonBuilder.toString();
}
}
/**
* Parse the {@link HttpURLConnection} response as a String.
* This does not close the url connection. If further requests to this host are unlikely
* in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}.
*/
public static String parseString(HttpURLConnection connection) throws IOException {
return parseInputStreamAndClose(connection.getInputStream());
}
/**
* Parse the {@link HttpURLConnection} response as a String, and disconnect.
*
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
*
* @see #parseString(HttpURLConnection)
*/
public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException {
String result = parseString(connection);
connection.disconnect();
return result;
}
/**
* Parse the {@link HttpURLConnection} error stream as a String.
* If the server sent no error response data, this returns an empty string.
*/
public static String parseErrorString(HttpURLConnection connection) throws IOException {
InputStream errorStream = connection.getErrorStream();
if (errorStream == null) {
return "";
}
return parseInputStreamAndClose(errorStream);
}
/**
* Parse the {@link HttpURLConnection} error stream as a String, and disconnect.
* If the server sent no error response data, this returns an empty string.
*
* Should only be used if other requests to the server are unlikely in the near future.
*
* @see #parseErrorString(HttpURLConnection)
*/
public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException {
String result = parseErrorString(connection);
connection.disconnect();
return result;
}
/**
* Parse the {@link HttpURLConnection} response into a JSONObject.
* This does not close the url connection. If further requests to this host are unlikely
* in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}.
*/
public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException {
return new JSONObject(parseString(connection));
}
/**
* Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
*
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
*
* @see #parseJSONObject(HttpURLConnection)
*/
public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
JSONObject object = parseJSONObject(connection);
connection.disconnect();
return object;
}
/**
* Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
* This does not close the url connection. If further requests to this host are unlikely
* in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}.
*/
public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException {
return new JSONArray(parseString(connection));
}
/**
* Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
*
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
*
* @see #parseJSONArray(HttpURLConnection)
*/
public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
JSONArray array = parseJSONArray(connection);
connection.disconnect();
return array;
}
}

View File

@@ -0,0 +1,66 @@
package app.revanced.extension.shared.requests;
public class Route {
private final String route;
private final Method method;
private final int paramCount;
public Route(Method method, String route) {
this.method = method;
this.route = route;
this.paramCount = countMatches(route, '{');
if (paramCount != countMatches(route, '}'))
throw new IllegalArgumentException("Not enough parameters");
}
public Method getMethod() {
return method;
}
public CompiledRoute compile(String... params) {
if (params.length != paramCount)
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
"Expected: " + paramCount + ", provided: " + params.length);
StringBuilder compiledRoute = new StringBuilder(route);
for (int i = 0; i < paramCount; i++) {
int paramStart = compiledRoute.indexOf("{");
int paramEnd = compiledRoute.indexOf("}");
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
}
return new CompiledRoute(this, compiledRoute.toString());
}
public static class CompiledRoute {
private final Route baseRoute;
private final String compiledRoute;
private CompiledRoute(Route baseRoute, String compiledRoute) {
this.baseRoute = baseRoute;
this.compiledRoute = compiledRoute;
}
public String getCompiledRoute() {
return compiledRoute;
}
public Method getMethod() {
return baseRoute.method;
}
}
private int countMatches(CharSequence seq, char c) {
int count = 0;
for (int i = 0; i < seq.length(); i++) {
if (seq.charAt(i) == c)
count++;
}
return count;
}
public enum Method {
GET,
POST
}
}

View File

@@ -0,0 +1,19 @@
package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent;
/**
* Settings shared across multiple apps.
*
* To ensure this class is loaded when the UI is created, app specific setting bundles should extend
* or reference this class.
*/
public class BaseSettings {
public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false);
}

View File

@@ -0,0 +1,79 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class BooleanSetting extends Setting<Boolean> {
public BooleanSetting(String key, Boolean defaultValue) {
super(key, defaultValue);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public BooleanSetting(String key, Boolean defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Boolean)} was intnded.
*/
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
setting.value = Objects.requireNonNull(newValue);
}
@Override
protected void load() {
value = preferences.getBoolean(key, defaultValue);
}
@Override
protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getBoolean(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Boolean.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Boolean newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveBoolean(key, newValue);
}
@NonNull
@Override
public Boolean get() {
return value;
}
}

View File

@@ -0,0 +1,117 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Locale;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
/**
* If an Enum value is removed or changed, any saved or imported data using the
* non-existent value will be reverted to the default value
* (the event is logged, but no user error is displayed).
*
* All saved JSON text is converted to lowercase to keep the output less obnoxious.
*/
@SuppressWarnings("unused")
public class EnumSetting<T extends Enum<?>> extends Setting<T> {
public EnumSetting(String key, T defaultValue) {
super(key, defaultValue);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public EnumSetting(String key, T defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public EnumSetting(String key, T defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getEnum(key, defaultValue);
}
@Override
protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException {
String enumName = json.getString(importExportKey);
try {
return getEnumFromString(enumName);
} catch (IllegalArgumentException ex) {
// Info level to allow removing enum values in the future without showing any user errors.
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex);
return defaultValue;
}
}
@Override
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
// Use lowercase to keep the output less ugly.
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
}
@NonNull
private T getEnumFromString(String enumName) {
//noinspection ConstantConditions
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
if (value.name().equalsIgnoreCase(enumName)) {
// noinspection unchecked
return (T) value;
}
}
throw new IllegalArgumentException("Unknown enum value: " + enumName);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = getEnumFromString(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull T newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveEnumAsString(key, newValue);
}
@NonNull
@Override
public T get() {
return value;
}
/**
* Availability based on if this setting is currently set to any of the provided types.
*/
@SafeVarargs
public final Setting.Availability availability(@NonNull T... types) {
return () -> {
T currentEnumType = get();
for (T enumType : types) {
if (currentEnumType == enumType) return true;
}
return false;
};
}
}

View File

@@ -0,0 +1,69 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class FloatSetting extends Setting<Float> {
public FloatSetting(String key, Float defaultValue) {
super(key, defaultValue);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public FloatSetting(String key, Float defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public FloatSetting(String key, Float defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getFloatString(key, defaultValue);
}
@Override
protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return (float) json.getDouble(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Float.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Float newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveFloatString(key, newValue);
}
@NonNull
@Override
public Float get() {
return value;
}
}

View File

@@ -0,0 +1,69 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class IntegerSetting extends Setting<Integer> {
public IntegerSetting(String key, Integer defaultValue) {
super(key, defaultValue);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public IntegerSetting(String key, Integer defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getIntegerString(key, defaultValue);
}
@Override
protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getInt(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Integer.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Integer newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveIntegerString(key, newValue);
}
@NonNull
@Override
public Integer get() {
return value;
}
}

View File

@@ -0,0 +1,69 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class LongSetting extends Setting<Long> {
public LongSetting(String key, Long defaultValue) {
super(key, defaultValue);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public LongSetting(String key, Long defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public LongSetting(String key, Long defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getLongString(key, defaultValue);
}
@Override
protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getLong(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Long.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Long newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveLongString(key, newValue);
}
@NonNull
@Override
public Long get() {
return value;
}
}

View File

@@ -0,0 +1,460 @@
package app.revanced.extension.shared.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.StringRef;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
import org.jetbrains.annotations.NotNull;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
import static app.revanced.extension.shared.StringRef.str;
@SuppressWarnings("unused")
public abstract class Setting<T> {
/**
* Indicates if a {@link Setting} is available to edit and use.
* Typically this is dependent upon other BooleanSetting(s) set to 'true',
* but this can be used to call into extension code and check other conditions.
*/
public interface Availability {
boolean isAvailable();
}
/**
* Availability based on a single parent setting being enabled.
*/
@NonNull
public static Availability parent(@NonNull BooleanSetting parent) {
return parent::get;
}
/**
* Availability based on all parents being enabled.
*/
@NonNull
public static Availability parentsAll(@NonNull BooleanSetting... parents) {
return () -> {
for (BooleanSetting parent : parents) {
if (!parent.get()) return false;
}
return true;
};
}
/**
* Availability based on any parent being enabled.
*/
@NonNull
public static Availability parentsAny(@NonNull BooleanSetting... parents) {
return () -> {
for (BooleanSetting parent : parents) {
if (parent.get()) return true;
}
return false;
};
}
/**
* Callback for importing/exporting settings.
*/
public interface ImportExportCallback {
/**
* Called after all settings have been imported.
*/
void settingsImported(@Nullable Context context);
/**
* Called after all settings have been exported.
*/
void settingsExported(@Nullable Context context);
}
private static final List<ImportExportCallback> importExportCallbacks = new ArrayList<>();
/**
* Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}.
*/
public static void addImportExportCallback(@NonNull ImportExportCallback callback) {
importExportCallbacks.add(Objects.requireNonNull(callback));
}
/**
* All settings that were instantiated.
* When a new setting is created, it is automatically added to this list.
*/
private static final List<Setting<?>> SETTINGS = new ArrayList<>();
/**
* Map of setting path to setting object.
*/
private static final Map<String, Setting<?>> PATH_TO_SETTINGS = new HashMap<>();
/**
* Preference all instances are saved to.
*/
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
@Nullable
public static Setting<?> getSettingFromPath(@NonNull String str) {
return PATH_TO_SETTINGS.get(str);
}
/**
* @return All settings that have been created.
*/
@NonNull
public static List<Setting<?>> allLoadedSettings() {
return Collections.unmodifiableList(SETTINGS);
}
/**
* @return All settings that have been created, sorted by keys.
*/
@NonNull
private static List<Setting<?>> allLoadedSettingsSorted() {
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
return allLoadedSettings();
}
/**
* The key used to store the value in the shared preferences.
*/
@NonNull
public final String key;
/**
* The default value of the setting.
*/
@NonNull
public final T defaultValue;
/**
* If the app should be rebooted, if this setting is changed
*/
public final boolean rebootApp;
/**
* If this setting should be included when importing/exporting settings.
*/
public final boolean includeWithImportExport;
/**
* If this setting is available to edit and use.
* Not to be confused with it's status returned from {@link #get()}.
*/
@Nullable
private final Availability availability;
/**
* Confirmation message to display, if the user tries to change the setting from the default value.
* Currently this works only for Boolean setting types.
*/
@Nullable
public final StringRef userDialogMessage;
// Must be volatile, as some settings are read/write from different threads.
// Of note, the object value is persistently stored using SharedPreferences (which is thread safe).
/**
* The value of the setting.
*/
@NonNull
protected volatile T value;
public Setting(String key, T defaultValue) {
this(key, defaultValue, false, true, null, null);
}
public Setting(String key, T defaultValue, boolean rebootApp) {
this(key, defaultValue, rebootApp, true, null, null);
}
public Setting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
this(key, defaultValue, rebootApp, includeWithImportExport, null, null);
}
public Setting(String key, T defaultValue, String userDialogMessage) {
this(key, defaultValue, false, true, userDialogMessage, null);
}
public Setting(String key, T defaultValue, Availability availability) {
this(key, defaultValue, false, true, null, availability);
}
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
this(key, defaultValue, rebootApp, true, userDialogMessage, null);
}
public Setting(String key, T defaultValue, boolean rebootApp, Availability availability) {
this(key, defaultValue, rebootApp, true, null, availability);
}
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
this(key, defaultValue, rebootApp, true, userDialogMessage, availability);
}
/**
* A setting backed by a shared preference.
*
* @param key The key used to store the value in the shared preferences.
* @param defaultValue The default value of the setting.
* @param rebootApp If the app should be rebooted, if this setting is changed.
* @param includeWithImportExport If this setting should be shown in the import/export dialog.
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
* @param availability Condition that must be true, for this setting to be available to configure.
*/
public Setting(@NonNull String key,
@NonNull T defaultValue,
boolean rebootApp,
boolean includeWithImportExport,
@Nullable String userDialogMessage,
@Nullable Availability availability
) {
this.key = Objects.requireNonNull(key);
this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
this.rebootApp = rebootApp;
this.includeWithImportExport = includeWithImportExport;
this.userDialogMessage = (userDialogMessage == null) ? null : new StringRef(userDialogMessage);
this.availability = availability;
SETTINGS.add(this);
if (PATH_TO_SETTINGS.put(key, this) != null) {
// Debug setting may not be created yet so using Logger may cause an initialization crash.
// Show a toast instead.
Utils.showToastLong(this.getClass().getSimpleName()
+ " error: Duplicate Setting key found: " + key);
}
load();
}
/**
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
*/
public static <T> void migrateOldSettingToNew(@NonNull Setting<T> oldSetting, @NonNull Setting<T> newSetting) {
if (oldSetting == newSetting) throw new IllegalArgumentException();
if (!oldSetting.isSetToDefault()) {
Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting);
newSetting.save(oldSetting.value);
oldSetting.resetToDefault();
}
}
/**
* Migrate an old Setting value previously stored in a different SharedPreference.
*
* This method will be deleted in the future.
*/
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do.
}
Object newValue = setting.get();
final Object migratedValue;
if (setting instanceof BooleanSetting) {
migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue);
} else if (setting instanceof IntegerSetting) {
migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue);
} else if (setting instanceof LongSetting) {
migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue);
} else if (setting instanceof FloatSetting) {
migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue);
} else if (setting instanceof StringSetting) {
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
} else {
Logger.printException(() -> "Unknown setting: " + setting);
// Remove otherwise it'll show a toast on every launch
oldPrefs.preferences.edit().remove(settingKey).apply();
return;
}
oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting.
if (migratedValue.equals(newValue)) {
Logger.printDebug(() -> "Value does not need migrating: " + settingKey);
return; // Old value is already equal to the new setting value.
}
Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey);
//noinspection unchecked
setting.save(migratedValue);
}
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Object)} was intended.
*/
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
setting.setValueFromString(newValue);
}
/**
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
*/
protected abstract void setValueFromString(@NonNull String newValue);
/**
* Load and set the value of {@link #value}.
*/
protected abstract void load();
/**
* Persistently saves the value.
*/
public abstract void save(@NonNull T newValue);
@NonNull
public abstract T get();
/**
* Identical to calling {@link #save(Object)} using {@link #defaultValue}.
*/
public void resetToDefault() {
save(defaultValue);
}
/**
* @return if this setting can be configured and used.
*/
public boolean isAvailable() {
return availability == null || availability.isAvailable();
}
/**
* @return if the currently set value is the same as {@link #defaultValue}
*/
public boolean isSetToDefault() {
return value.equals(defaultValue);
}
@NotNull
@Override
public String toString() {
return key + "=" + get();
}
// region Import / export
/**
* If a setting path has this prefix, then remove it before importing/exporting.
*/
private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_";
/**
* The path, minus any 'revanced' prefix to keep json concise.
*/
private String getImportExportKey() {
if (key.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) {
return key.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length());
}
return key;
}
/**
* @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
* @return the value stored using the import/export key. Do not set any values in this method.
*/
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
/**
* Saves this instance to JSON.
* <p>
* To keep the JSON simple and readable,
* subclasses should not write out any embedded types (such as JSON Array or Dictionaries).
* <p>
* If this instance is not a type supported natively by JSON (ie: it's not a String/Integer/Float/Long),
* then subclasses can override this method and write out a String value representing the value.
*/
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
json.put(importExportKey, value);
}
@NonNull
public static String exportToJson(@Nullable Context alertDialogContext) {
try {
JSONObject json = new JSONObject();
for (Setting<?> setting : allLoadedSettingsSorted()) {
String importExportKey = setting.getImportExportKey();
if (json.has(importExportKey)) {
throw new IllegalArgumentException("duplicate key found: " + importExportKey);
}
final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI.
//noinspection ConstantValue
if (setting.includeWithImportExport && (!setting.isSetToDefault() || exportDefaultValues)) {
setting.writeToJSON(json, importExportKey);
}
}
for (ImportExportCallback callback : importExportCallbacks) {
callback.settingsExported(alertDialogContext);
}
if (json.length() == 0) {
return "";
}
String export = json.toString(0);
// Remove the outer JSON braces to make the output more compact,
// and leave less chance of the user forgetting to copy it
return export.substring(2, export.length() - 2);
} catch (JSONException e) {
Logger.printException(() -> "Export failure", e); // should never happen
return "";
}
}
/**
* @return if any settings that require a reboot were changed.
*/
public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) {
try {
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
}
JSONObject json = new JSONObject(settingsJsonString);
boolean rebootSettingChanged = false;
int numberOfSettingsImported = 0;
for (Setting setting : SETTINGS) {
String key = setting.getImportExportKey();
if (json.has(key)) {
Object value = setting.readFromJSON(json, key);
if (!setting.get().equals(value)) {
rebootSettingChanged |= setting.rebootApp;
//noinspection unchecked
setting.save(value);
}
numberOfSettingsImported++;
} else if (setting.includeWithImportExport && !setting.isSetToDefault()) {
Logger.printDebug(() -> "Resetting to default: " + setting);
rebootSettingChanged |= setting.rebootApp;
setting.resetToDefault();
}
}
for (ImportExportCallback callback : importExportCallbacks) {
callback.settingsImported(alertDialogContext);
}
Utils.showToastLong(numberOfSettingsImported == 0
? str("revanced_settings_import_reset")
: str("revanced_settings_import_success", numberOfSettingsImported));
return rebootSettingChanged;
} catch (JSONException | IllegalArgumentException ex) {
Utils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage()));
Logger.printInfo(() -> "", ex);
} catch (Exception ex) {
Logger.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen
}
return false;
}
// End import / export
}

View File

@@ -0,0 +1,69 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class StringSetting extends Setting<String> {
public StringSetting(String key, String defaultValue) {
super(key, defaultValue);
}
public StringSetting(String key, String defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public StringSetting(String key, String defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public StringSetting(String key, String defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getString(key, defaultValue);
}
@Override
protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getString(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Objects.requireNonNull(newValue);
}
@Override
public void save(@NonNull String newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveString(key, newValue);
}
@NonNull
@Override
public String get() {
return value;
}
}

View File

@@ -0,0 +1,280 @@
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.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.*;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting;
@SuppressWarnings("deprecation")
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/**
* Indicates that if a preference changes,
* to apply the change from the Setting to the UI component.
*/
public static boolean settingImportInProgress;
/**
* Confirm and restart dialog button text and title.
* Set by subclasses if Strings cannot be added as a resource.
*/
@Nullable
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle;
/**
* Used to prevent showing reboot dialog, if user cancels a setting user dialog.
*/
private boolean showingUserDialogMessage;
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
Setting<?> setting = Setting.getSettingFromPath(str);
if (setting == null) {
return;
}
Preference pref = findPreference(str);
if (pref == null) {
return;
}
Logger.printDebug(() -> "Preference changed: " + setting.key);
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different.
updateUIAvailability();
if (settingImportInProgress) {
return;
}
if (!showingUserDialogMessage) {
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting);
} else if (setting.rebootApp) {
showRestartDialog(getContext());
}
}
} catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
};
/**
* Initialize this instance, and do any custom behavior.
* <p>
* To ensure all {@link Setting} instances are correctly synced to the UI,
* it is important that subclasses make a call or otherwise reference their Settings class bundle
* so all app specific {@link Setting} instances are loaded before this method returns.
*/
protected void initialize() {
final var identifier = Utils.getResourceIdentifier("revanced_prefs", "xml");
if (identifier == 0) return;
addPreferencesFromResource(identifier);
PreferenceScreen screen = getPreferenceScreen();
Utils.sortPreferenceGroups(screen);
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
}
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) {
Utils.verifyOnMainThread();
final var context = getContext();
if (confirmDialogTitle == null) {
confirmDialogTitle = str("revanced_settings_confirm_user_dialog_title");
}
showingUserDialogMessage = true;
new AlertDialog.Builder(context)
.setTitle(confirmDialogTitle)
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
})
.setOnDismissListener(dialog -> {
showingUserDialogMessage = false;
})
.setCancelable(false)
.show();
}
/**
* Updates all Preferences values and their availability using the current values in {@link Setting}.
*/
protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true,true);
}
/**
* Updates Preferences availability only using the status of {@link Setting}.
*/
protected void updateUIAvailability() {
updatePreferenceScreen(getPreferenceScreen(), false, false);
}
/**
* Syncs all UI Preferences to any {@link Setting} they represent.
*/
private void updatePreferenceScreen(@NonNull PreferenceScreen screen,
boolean syncSettingValue,
boolean applySettingToPreference) {
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
// but there are many more Settings than UI preferences so it's more efficient to only check
// the Preferences.
for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) {
Preference pref = screen.getPreference(i);
if (pref instanceof PreferenceScreen) {
updatePreferenceScreen((PreferenceScreen) pref, syncSettingValue, applySettingToPreference);
} else if (pref.hasKey()) {
String key = pref.getKey();
Setting<?> setting = Setting.getSettingFromPath(key);
if (setting != null) {
updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
} else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference
|| pref instanceof EditTextPreference || pref instanceof ListPreference)) {
// Probably a typo in the patches preference declaration.
Logger.printException(() -> "Preference key has no setting: " + key);
}
}
}
}
/**
* Handles syncing a UI Preference with the {@link Setting} that backs it.
* If needed, subclasses can override this to handle additional UI Preference types.
*
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/
protected void syncSettingWithPreference(@NonNull Preference pref,
@NonNull Setting<?> setting,
boolean applySettingToPreference) {
if (pref instanceof SwitchPreference) {
SwitchPreference switchPref = (SwitchPreference) pref;
BooleanSetting boolSetting = (BooleanSetting) setting;
if (applySettingToPreference) {
switchPref.setChecked(boolSetting.get());
} else {
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
}
} else if (pref instanceof EditTextPreference) {
EditTextPreference editPreference = (EditTextPreference) pref;
if (applySettingToPreference) {
editPreference.setText(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, editPreference.getText());
}
} else if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
if (applySettingToPreference) {
listPref.setValue(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, listPref.getValue());
}
updateListPreferenceSummary(listPref, setting);
} else {
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
}
}
/**
* Updates a UI Preference with the {@link Setting} that backs it.
*
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/
private void updatePreference(@NonNull Preference pref, @NonNull Setting<?> setting,
boolean syncSetting, boolean applySettingToPreference) {
if (!syncSetting && applySettingToPreference) {
throw new IllegalArgumentException();
}
if (syncSetting) {
syncSettingWithPreference(pref, setting, applySettingToPreference);
}
updatePreferenceAvailability(pref, setting);
}
protected void updatePreferenceAvailability(@NonNull Preference pref, @NonNull Setting<?> setting) {
pref.setEnabled(setting.isAvailable());
}
protected void updateListPreferenceSummary(ListPreference listPreference, Setting<?> setting) {
String objectStringValue = setting.get().toString();
final int entryIndex = listPreference.findIndexOfValue(objectStringValue);
if (entryIndex >= 0) {
listPreference.setSummary(listPreference.getEntries()[entryIndex]);
} else {
// Value is not an available option.
// User manually edited import data, or options changed and current selection is no longer available.
// Still show the value in the summary, so it's clear that something is selected.
listPreference.setSummary(objectStringValue);
}
}
public static void showRestartDialog(@NonNull final Context context) {
Utils.verifyOnMainThread();
if (restartDialogTitle == null) {
restartDialogTitle = str("revanced_settings_restart_title");
}
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();
}
@SuppressLint("ResourceType")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setSharedPreferencesName(Setting.preferences.name);
// Must initialize before adding change listener,
// otherwise the syncing of Setting -> UI
// causes a callback to the listener even though nothing changed.
initialize();
updateUIToSettingValues();
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
} catch (Exception ex) {
Logger.printException(() -> "onCreate() failure", ex);
}
}
@Override
public void onDestroy() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
super.onDestroy();
}
}

View File

@@ -0,0 +1,100 @@
package app.revanced.extension.shared.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Build;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import static app.revanced.extension.shared.StringRef.str;
@SuppressWarnings({"unused", "deprecation"})
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
private String existingSettings;
private void init() {
setSelectable(true);
EditText editText = getEditText();
editText.setTextIsSelectable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
editText.setAutofillHints((String) null);
}
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap.
setOnPreferenceClickListener(this);
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ImportExportPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
existingSettings = Setting.exportToJson(getContext());
getEditText().setText(existingSettings);
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
return true;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
// Show the user the settings in JSON format.
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
Utils.setClipboard(getEditText().getText().toString());
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
importSettings(builder.getContext(), getEditText().getText().toString());
});
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
private void importSettings(Context context, String replacementSettings) {
try {
if (replacementSettings.equals(existingSettings)) {
return;
}
AbstractPreferenceFragment.settingImportInProgress = true;
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
if (rebootNeeded) {
AbstractPreferenceFragment.showRestartDialog(getContext());
}
} catch (Exception ex) {
Logger.printException(() -> "importSettings failure", ex);
} finally {
AbstractPreferenceFragment.settingImportInProgress = false;
}
}
}

View File

@@ -0,0 +1,361 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
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.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
/**
* Opens a dialog showing official links.
*/
@SuppressWarnings({"unused", "deprecation"})
public class ReVancedAboutPreference extends Preference {
private static String useNonBreakingHyphens(String text) {
// Replace any dashes with non breaking dashes, so the English text 'pre-release'
// and the dev release number does not break and cover two lines.
return text.replace("-", "&#8209;"); // #8209 = non breaking hyphen.
}
private static String getColorHexString(int color) {
return String.format("#%06X", (0x00FFFFFF & color));
}
protected boolean isDarkModeEnabled() {
Configuration config = getContext().getResources().getConfiguration();
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
}
/**
* 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.
*
* @return A localized string to display for the key.
*/
protected String getString(String key, Object ... args) {
return str(key, args);
}
private String createDialogHtml(WebLink[] aboutLinks) {
final boolean isNetworkConnected = Utils.isNetworkConnected();
StringBuilder builder = new StringBuilder();
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());
// Apply light/dark mode colors.
builder.append(String.format(
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
backgroundColorHex, foregroundColorHex, foregroundColorHex));
if (isNetworkConnected) {
builder.append("<img style=\"width: 100px; height: 100px;\" "
// Hide the image if it does not load.
+ "onerror=\"this.style.display='none';\" "
+ "src=\"").append(AboutLinksRoutes.aboutLogoUrl).append("\" />");
}
String patchesVersion = Utils.getPatchesReleaseVersion();
// Add the title.
builder.append("<h1>")
.append("ReVanced")
.append("</h1>");
builder.append("<p>")
// Replace hyphens with non breaking dashes so the version number does not break lines.
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_body", patchesVersion)))
.append("</p>");
// Add a disclaimer if using a dev release.
if (patchesVersion.contains("dev")) {
builder.append("<h3>")
// English text 'Pre-release' can break lines.
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_dev_header")))
.append("</h3>");
builder.append("<p>")
.append(getString("revanced_settings_about_links_dev_body"))
.append("</p>");
}
builder.append("<h2 style=\"margin-top: 30px;\">")
.append(getString("revanced_settings_about_links_header"))
.append("</h2>");
builder.append("<div>");
for (WebLink link : aboutLinks) {
builder.append("<div style=\"margin-bottom: 20px;\">");
builder.append(String.format("<a href=\"%s\">%s</a>", link.url, link.name));
builder.append("</div>");
}
builder.append("</div>");
builder.append("</body></html>");
return builder.toString();
}
{
setOnPreferenceClickListener(pref -> {
// Show a progress spinner if the social links are not fetched yet.
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
// Show a progress spinner, but only if the api fetch takes more than a half a second.
final long delayToShowProgressSpinner = 500;
ProgressDialog progress = new ProgressDialog(getContext());
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
Handler handler = new Handler(Looper.getMainLooper());
Runnable showDialogRunnable = progress::show;
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
Utils.runOnBackgroundThread(() ->
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
} else {
// No network call required and can run now.
fetchLinksAndShowDialog(null, null, null);
}
return false;
});
}
private void fetchLinksAndShowDialog(@Nullable Handler handler,
Runnable showDialogRunnable,
@Nullable ProgressDialog progress) {
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
String htmlDialog = createDialogHtml(links);
// Enable to randomly force a delay to debug the spinner logic.
final boolean debugSpinnerDelayLogic = false;
//noinspection ConstantConditions
if (debugSpinnerDelayLogic && handler != null && Math.random() < 0.5f) {
Utils.doNothingForDuration((long) (Math.random() * 4000));
}
Utils.runOnMainThreadNowOrLater(() -> {
if (handler != null) {
handler.removeCallbacks(showDialogRunnable);
}
if (progress != null) {
progress.dismiss();
}
new WebViewDialog(getContext(), htmlDialog).show();
});
}
public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReVancedAboutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReVancedAboutPreference(Context context) {
super(context);
}
}
/**
* Displays html content as a dialog. Any links a user taps on are opened in an external browser.
*/
class WebViewDialog extends Dialog {
private final String htmlContent;
public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) {
super(context);
this.htmlContent = htmlContent;
}
// JS required to hide any broken images. No remote javascript is ever loaded.
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
WebView webView = new WebView(getContext());
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new OpenLinksExternallyWebClient());
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
setContentView(webView);
}
private class OpenLinksExternallyWebClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
getContext().startActivity(intent);
} catch (Exception ex) {
Logger.printException(() -> "Open link failure", ex);
}
// Dismiss the about dialog using a delay,
// otherwise without a delay the UI looks hectic with the dialog dismissing
// to show the settings while simultaneously a web browser is opening.
Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500);
return true;
}
}
}
class WebLink {
final boolean preferred;
String name;
final String url;
WebLink(JSONObject json) throws JSONException {
this(json.getBoolean("preferred"),
json.getString("name"),
json.getString("url")
);
}
WebLink(boolean preferred, String name, String url) {
this.preferred = preferred;
this.name = name;
this.url = url;
}
@NonNull
@Override
public String toString() {
return "WebLink{" +
"preferred=" + preferred +
", name='" + name + '\'' +
", url='" + url + '\'' +
'}';
}
}
class AboutLinksRoutes {
/**
* Backup icon url if the API call fails.
*/
public static volatile String aboutLogoUrl = "https://revanced.app/favicon.ico";
/**
* Links to use if fetch links api call fails.
*/
private static final WebLink[] NO_CONNECTION_STATIC_LINKS = {
new WebLink(true, "ReVanced.app", "https://revanced.app")
};
private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v4";
private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/about").compile();
@Nullable
private static volatile WebLink[] fetchedLinks;
static boolean hasFetchedLinks() {
return fetchedLinks != null;
}
static WebLink[] fetchAboutLinks() {
try {
if (hasFetchedLinks()) return fetchedLinks;
// Check if there is no internet connection.
if (!Utils.isNetworkConnected()) return NO_CONNECTION_STATIC_LINKS;
HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(SOCIAL_LINKS_PROVIDER, GET_SOCIAL);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
Logger.printDebug(() -> "Fetching social links from: " + connection.getURL());
// 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);
return NO_CONNECTION_STATIC_LINKS;
}
JSONObject json = Requester.parseJSONObjectAndDisconnect(connection);
aboutLogoUrl = json.getJSONObject("branding").getString("logo");
List<WebLink> links = new ArrayList<>();
JSONArray donations = json.getJSONObject("donations").getJSONArray("links");
for (int i = 0, length = donations.length(); i < length; i++) {
WebLink link = new WebLink(donations.getJSONObject(i));
if (link.preferred) {
// This could be localized, but TikTok does not support localized resources.
// All link names returned by the api are also non localized.
link.name = "Donate";
links.add(link);
}
}
JSONArray socials = json.getJSONArray("socials");
for (int i = 0, length = socials.length(); i < length; i++) {
WebLink link = new WebLink(socials.getJSONObject(i));
links.add(link);
}
Logger.printDebug(() -> "links: " + links);
return fetchedLinks = links.toArray(new WebLink[0]);
} catch (SocketTimeoutException ex) {
Logger.printInfo(() -> "Could not fetch social links", ex); // No toast.
} catch (JSONException ex) {
Logger.printException(() -> "Could not parse about information", ex);
} catch (Exception ex) {
Logger.printException(() -> "Failed to get about information", ex);
}
return NO_CONNECTION_STATIC_LINKS;
}
}

View File

@@ -0,0 +1,67 @@
package app.revanced.extension.shared.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.Logger;
import java.util.Objects;
import static app.revanced.extension.shared.StringRef.str;
@SuppressWarnings({"unused", "deprecation"})
public class ResettableEditTextPreference extends EditTextPreference {
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ResettableEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ResettableEditTextPreference(Context context) {
super(context);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
Utils.setEditTextDialogTheme(builder);
Setting<?> setting = Setting.getSettingFromPath(getKey());
if (setting != null) {
builder.setNeutralButton(str("revanced_settings_reset"), null);
}
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
// 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 {
Setting<?> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey()));
String defaultStringValue = 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);
}
});
}
}

View File

@@ -0,0 +1,190 @@
package app.revanced.extension.shared.settings.preference;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceFragment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import java.util.Objects;
/**
* Shared categories, and helper methods.
*
* The various save methods store numbers as Strings,
* which is required if using {@link PreferenceFragment}.
*
* If saved numbers will not be used with a preference fragment,
* then store the primitive numbers using the {@link #preferences} itself.
*/
public class SharedPrefCategory {
@NonNull
public final String name;
@NonNull
public final SharedPreferences preferences;
public SharedPrefCategory(@NonNull String name) {
this.name = Objects.requireNonNull(name);
preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE);
}
private void removeConflictingPreferenceKeyValue(@NonNull String key) {
Logger.printException(() -> "Found conflicting preference: " + key);
removeKey(key);
}
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
}
/**
* Removes any preference data type that has the specified key.
*/
public void removeKey(@NonNull String key) {
preferences.edit().remove(Objects.requireNonNull(key)).apply();
}
public void saveBoolean(@NonNull String key, boolean value) {
preferences.edit().putBoolean(key, value).apply();
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveEnumAsString(@NonNull String key, @Nullable Enum<?> value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveIntegerString(@NonNull String key, @Nullable Integer value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveLongString(@NonNull String key, @Nullable Long value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveFloatString(@NonNull String key, @Nullable Float value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveString(@NonNull String key, @Nullable String value) {
saveObjectAsString(key, value);
}
@NonNull
public String getString(@NonNull String key, @NonNull String _default) {
Objects.requireNonNull(_default);
try {
return preferences.getString(key, _default);
} catch (ClassCastException ex) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
return _default;
}
}
@NonNull
public <T extends Enum<?>> T getEnum(@NonNull String key, @NonNull T _default) {
Objects.requireNonNull(_default);
try {
String enumName = preferences.getString(key, null);
if (enumName != null) {
try {
// noinspection unchecked
return (T) Enum.valueOf(_default.getClass(), enumName);
} catch (IllegalArgumentException ex) {
// Info level to allow removing enum values in the future without showing any user errors.
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName);
removeKey(key);
}
}
} catch (ClassCastException ex) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
}
return _default;
}
public boolean getBoolean(@NonNull String key, boolean _default) {
try {
return preferences.getBoolean(key, _default);
} catch (ClassCastException ex) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
return _default;
}
}
@NonNull
public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) {
try {
String value = preferences.getString(key, null);
if (value != null) {
return Integer.valueOf(value);
}
} catch (ClassCastException | NumberFormatException ex) {
try {
// Old data previously stored as primitive.
return preferences.getInt(key, _default);
} catch (ClassCastException ex2) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
}
}
return _default;
}
@NonNull
public Long getLongString(@NonNull String key, @NonNull Long _default) {
try {
String value = preferences.getString(key, null);
if (value != null) {
return Long.valueOf(value);
}
} catch (ClassCastException | NumberFormatException ex) {
try {
return preferences.getLong(key, _default);
} catch (ClassCastException ex2) {
removeConflictingPreferenceKeyValue(key);
}
}
return _default;
}
@NonNull
public Float getFloatString(@NonNull String key, @NonNull Float _default) {
try {
String value = preferences.getString(key, null);
if (value != null) {
return Float.valueOf(value);
}
} catch (ClassCastException | NumberFormatException ex) {
try {
return preferences.getFloat(key, _default);
} catch (ClassCastException ex2) {
removeConflictingPreferenceKeyValue(key);
}
}
return _default;
}
@NonNull
@Override
public String toString() {
return name;
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,5 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:syncforreddit:stub"))
compileOnly(libs.annotation)
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,77 @@
package app.revanced.extension.syncforreddit;
import android.util.Pair;
import androidx.annotation.Nullable;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
/**
* @noinspection unused
*/
public class FixRedditVideoDownloadPatch {
private static @Nullable Pair<Integer, String> getBestMpEntry(Element element) {
var representations = element.getElementsByTagName("Representation");
var entries = new ArrayList<Pair<Integer, String>>();
for (int i = 0; i < representations.getLength(); i++) {
Element representation = (Element) representations.item(i);
var bandwidthStr = representation.getAttribute("bandwidth");
try {
var bandwidth = Integer.parseInt(bandwidthStr);
var baseUrl = representation.getElementsByTagName("BaseURL").item(0);
if (baseUrl != null) {
entries.add(new Pair<>(bandwidth, baseUrl.getTextContent()));
}
} catch (NumberFormatException ignored) {
}
}
if (entries.isEmpty()) {
return null;
}
Collections.sort(entries, (e1, e2) -> e2.first - e1.first);
return entries.get(0);
}
private static String[] parse(byte[] data) throws ParserConfigurationException, IOException, SAXException {
var adaptionSets = DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream(data))
.getElementsByTagName("AdaptationSet");
String videoUrl = null;
String audioUrl = null;
for (int i = 0; i < adaptionSets.getLength(); i++) {
Element element = (Element) adaptionSets.item(i);
var contentType = element.getAttribute("contentType");
var bestEntry = getBestMpEntry(element);
if (bestEntry == null) continue;
if (contentType.equalsIgnoreCase("video")) {
videoUrl = bestEntry.second;
} else if (contentType.equalsIgnoreCase("audio")) {
audioUrl = bestEntry.second;
}
}
return new String[]{videoUrl, audioUrl};
}
public static String[] getLinks(byte[] data) {
try {
return parse(data);
} catch (ParserConfigurationException | IOException | SAXException e) {
return new String[]{null, null};
}
}
}

View File

@@ -0,0 +1,24 @@
package app.revanced.extension.syncforreddit;
import com.laurencedawson.reddit_sync.ui.activities.WebViewActivity;
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
/** @noinspection unused*/
public class FixSLinksPatch extends BaseFixSLinksPatch {
static {
INSTANCE = new FixSLinksPatch();
}
private FixSLinksPatch() {
webViewActivityClass = WebViewActivity.class;
}
public static boolean patchResolveSLink(String link) {
return INSTANCE.resolveSLink(link);
}
public static void patchSetAccessToken(String accessToken) {
INSTANCE.setAccessToken(accessToken);
}
}

View File

@@ -0,0 +1,17 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
}
android {
namespace = "app.revanced.extension"
compileSdk = 33
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,6 @@
package com.laurencedawson.reddit_sync.ui.activities;
import android.app.Activity;
public class WebViewActivity extends Activity {
}

View File

@@ -0,0 +1,5 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:tiktok:stub"))
compileOnly(libs.annotation)
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,25 @@
package app.revanced.extension.tiktok;
import app.revanced.extension.shared.settings.StringSetting;
public class Utils {
// Edit: This could be handled using a custom Setting<Long[]> class
// that saves its value to preferences and JSON using the formatted String created here.
public static long[] parseMinMax(StringSetting setting) {
final String[] minMax = setting.get().split("-");
if (minMax.length == 2) {
try {
final long min = Long.parseLong(minMax[0]);
final long max = Long.parseLong(minMax[1]);
if (min <= max && min >= 0) return new long[]{min, max};
} catch (NumberFormatException ignored) {
}
}
setting.save("0-" + Long.MAX_VALUE);
return new long[]{0L, Long.MAX_VALUE};
}
}

View File

@@ -0,0 +1,13 @@
package app.revanced.extension.tiktok.cleardisplay;
import app.revanced.extension.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class RememberClearDisplayPatch {
public static boolean getClearDisplayState() {
return Settings.CLEAR_DISPLAY.get();
}
public static void rememberClearDisplayState(boolean newState) {
Settings.CLEAR_DISPLAY.save(newState);
}
}

View File

@@ -0,0 +1,14 @@
package app.revanced.extension.tiktok.download;
import app.revanced.extension.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class DownloadsPatch {
public static String getDownloadPath() {
return Settings.DOWNLOAD_PATH.get();
}
public static boolean shouldRemoveWatermark() {
return Settings.DOWNLOAD_WATERMARK.get();
}
}

View File

@@ -0,0 +1,16 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class AdsFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.REMOVE_ADS.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.isAd() || item.isWithPromotionalMusic();
}
}

View File

@@ -0,0 +1,34 @@
package app.revanced.extension.tiktok.feedfilter;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.FeedItemList;
import java.util.Iterator;
import java.util.List;
public final class FeedItemsFilter {
private static final List<IFilter> FILTERS = List.of(
new AdsFilter(),
new LiveFilter(),
new StoryFilter(),
new ImageVideoFilter(),
new ViewCountFilter(),
new LikeCountFilter()
);
public static void filter(FeedItemList feedItemList) {
Iterator<Aweme> feedItemListIterator = feedItemList.items.iterator();
while (feedItemListIterator.hasNext()) {
Aweme item = feedItemListIterator.next();
if (item == null) continue;
for (IFilter filter : FILTERS) {
boolean enabled = filter.getEnabled();
if (enabled && filter.getFiltered(item)) {
feedItemListIterator.remove();
break;
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
package app.revanced.extension.tiktok.feedfilter;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public interface IFilter {
boolean getEnabled();
boolean getFiltered(Aweme item);
}

View File

@@ -0,0 +1,16 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class ImageVideoFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.HIDE_IMAGE.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.isImage() || item.isPhotoMode();
}
}

View File

@@ -0,0 +1,32 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
import static app.revanced.extension.tiktok.Utils.parseMinMax;
public final class LikeCountFilter implements IFilter {
final long minLike;
final long maxLike;
LikeCountFilter() {
long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES);
minLike = minMax[0];
maxLike = minMax[1];
}
@Override
public boolean getEnabled() {
return true;
}
@Override
public boolean getFiltered(Aweme item) {
AwemeStatistics statistics = item.getStatistics();
if (statistics == null) return false;
long likeCount = statistics.getDiggCount();
return likeCount < minLike || likeCount > maxLike;
}
}

View File

@@ -0,0 +1,16 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class LiveFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.HIDE_LIVE.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.isLive() || item.isLiveReplay();
}
}

View File

@@ -0,0 +1,16 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class StoryFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.HIDE_STORY.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.getIsTikTokStory();
}
}

View File

@@ -0,0 +1,32 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
import static app.revanced.extension.tiktok.Utils.parseMinMax;
public class ViewCountFilter implements IFilter {
final long minView;
final long maxView;
ViewCountFilter() {
long[] minMax = parseMinMax(Settings.MIN_MAX_VIEWS);
minView = minMax[0];
maxView = minMax[1];
}
@Override
public boolean getEnabled() {
return true;
}
@Override
public boolean getFiltered(Aweme item) {
AwemeStatistics statistics = item.getStatistics();
if (statistics == null) return false;
long playCount = statistics.getPlayCount();
return playCount < minView || playCount > maxView;
}
}

View File

@@ -0,0 +1,82 @@
package app.revanced.extension.tiktok.settings;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.tiktok.settings.preference.ReVancedPreferenceFragment;
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Hooks AdPersonalizationActivity.
* <p>
* This class is responsible for injecting our own fragment by replacing the AdPersonalizationActivity.
*
* @noinspection unused
*/
public class AdPersonalizationActivityHook {
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
try {
Class<?> entryClazz = Class.forName(entryClazzName);
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
return entryConstructor.newInstance(buttonInfo);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
InstantiationException e) {
throw new RuntimeException(e);
}
}
/***
* Initialize the settings menu.
* @param base The activity to initialize the settings menu on.
* @return Whether the settings menu should be initialized.
*/
public static boolean initialize(AdPersonalizationActivity base) {
Bundle extras = base.getIntent().getExtras();
if (extras != null && !extras.getBoolean("revanced", false)) return false;
SettingsStatus.load();
LinearLayout linearLayout = new LinearLayout(base);
linearLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setFitsSystemWindows(true);
linearLayout.setTransitionGroup(true);
FrameLayout fragment = new FrameLayout(base);
fragment.setLayoutParams(new FrameLayout.LayoutParams(-1, -1));
int fragmentId = View.generateViewId();
fragment.setId(fragmentId);
linearLayout.addView(fragment);
base.setContentView(linearLayout);
PreferenceFragment preferenceFragment = new ReVancedPreferenceFragment();
base.getFragmentManager().beginTransaction().replace(fragmentId, preferenceFragment).commit();
return true;
}
private static void startSettingsActivity() {
Context appContext = Utils.getContext();
if (appContext != null) {
Intent intent = new Intent(appContext, AdPersonalizationActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("revanced", true);
appContext.startActivity(intent);
} else {
Logger.printDebug(() -> "Utils.getContext() return null");
}
}
}

View File

@@ -0,0 +1,26 @@
package app.revanced.extension.tiktok.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.StringSetting;
public class Settings extends BaseSettings {
public static final BooleanSetting REMOVE_ADS = new BooleanSetting("remove_ads", TRUE, true);
public static final BooleanSetting HIDE_LIVE = new BooleanSetting("hide_live", FALSE, true);
public static final BooleanSetting HIDE_STORY = new BooleanSetting("hide_story", FALSE, true);
public static final BooleanSetting HIDE_IMAGE = new BooleanSetting("hide_image", FALSE, true);
public static final StringSetting MIN_MAX_VIEWS = new StringSetting("min_max_views", "0-" + Long.MAX_VALUE, true);
public static final StringSetting MIN_MAX_LIKES = new StringSetting("min_max_likes", "0-" + Long.MAX_VALUE, true);
public static final StringSetting DOWNLOAD_PATH = new StringSetting("down_path", "DCIM/TikTok");
public static final BooleanSetting DOWNLOAD_WATERMARK = new BooleanSetting("down_watermark", TRUE);
public static final BooleanSetting CLEAR_DISPLAY = new BooleanSetting("clear_display", FALSE);
public static final FloatSetting REMEMBERED_SPEED = new FloatSetting("REMEMBERED_SPEED", 1.0f);
public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", TRUE, true);
public static final StringSetting SIM_SPOOF_ISO = new StringSetting("simspoof_iso", "us");
public static final StringSetting SIMSPOOF_MCCMNC = new StringSetting("simspoof_mccmnc", "310160");
public static final StringSetting SIMSPOOF_OP_NAME = new StringSetting("simspoof_op_name", "T-Mobile");
}

View File

@@ -0,0 +1,23 @@
package app.revanced.extension.tiktok.settings;
public class SettingsStatus {
public static boolean feedFilterEnabled = false;
public static boolean downloadEnabled = false;
public static boolean simSpoofEnabled = false;
public static void enableFeedFilter() {
feedFilterEnabled = true;
}
public static void enableDownload() {
downloadEnabled = true;
}
public static void enableSimSpoof() {
simSpoofEnabled = true;
}
public static void load() {
}
}

View File

@@ -0,0 +1,124 @@
package app.revanced.extension.tiktok.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Environment;
import android.preference.DialogPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import app.revanced.extension.shared.settings.StringSetting;
@SuppressWarnings("deprecation")
public class DownloadPathPreference extends DialogPreference {
private final Context context;
private final String[] entryValues = {"DCIM", "Movies", "Pictures"};
private String mValue;
private boolean mValueSet;
private int mediaPathIndex;
private String childDownloadPath;
public DownloadPathPreference(Context context, String title, StringSetting setting) {
super(context);
this.context = context;
this.setTitle(title);
this.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.get());
this.setKey(setting.key);
this.setValue(setting.get());
}
public String getValue() {
return this.mValue;
}
public void setValue(String value) {
final boolean changed = !TextUtils.equals(mValue, value);
if (changed || !mValueSet) {
mValue = value;
mValueSet = true;
persistString(value);
if (changed) {
notifyDependencyChange(shouldDisableDependents());
notifyChanged();
}
}
}
@Override
protected View onCreateDialogView() {
String currentMedia = getValue().split("/")[0];
childDownloadPath = getValue().substring(getValue().indexOf("/") + 1);
mediaPathIndex = findIndexOf(currentMedia);
LinearLayout dialogView = new LinearLayout(context);
RadioGroup mediaPath = new RadioGroup(context);
mediaPath.setLayoutParams(new RadioGroup.LayoutParams(-1, -2));
for (String entryValue : entryValues) {
RadioButton radioButton = new RadioButton(context);
radioButton.setText(entryValue);
radioButton.setId(View.generateViewId());
mediaPath.addView(radioButton);
}
mediaPath.setOnCheckedChangeListener((radioGroup, id) -> {
RadioButton radioButton = radioGroup.findViewById(id);
mediaPathIndex = findIndexOf(radioButton.getText().toString());
});
mediaPath.check(mediaPath.getChildAt(mediaPathIndex).getId());
EditText downloadPath = new EditText(context);
downloadPath.setInputType(InputType.TYPE_CLASS_TEXT);
downloadPath.setText(childDownloadPath);
downloadPath.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
childDownloadPath = editable.toString();
}
});
dialogView.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
dialogView.setOrientation(LinearLayout.VERTICAL);
dialogView.addView(mediaPath);
dialogView.addView(downloadPath);
return dialogView;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle("Download Path");
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNegativeButton(android.R.string.cancel, null);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult && mediaPathIndex >= 0) {
String newValue = entryValues[mediaPathIndex] + "/" + childDownloadPath;
setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + newValue);
setValue(newValue);
}
}
private int findIndexOf(String str) {
for (int i = 0; i < entryValues.length; i++) {
if (str.equals(entryValues[i])) return i;
}
return -1;
}
}

View File

@@ -0,0 +1,17 @@
package app.revanced.extension.tiktok.settings.preference;
import android.content.Context;
import android.preference.EditTextPreference;
import app.revanced.extension.shared.settings.StringSetting;
public class InputTextPreference extends EditTextPreference {
public InputTextPreference(Context context, String title, String summary, StringSetting setting) {
super(context);
this.setTitle(title);
this.setSummary(summary);
this.setKey(setting.key);
this.setText(setting.get());
}
}

View File

@@ -0,0 +1,130 @@
package app.revanced.extension.tiktok.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.preference.DialogPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import app.revanced.extension.shared.settings.StringSetting;
@SuppressWarnings("deprecation")
public class RangeValuePreference extends DialogPreference {
private final Context context;
private String minValue;
private String maxValue;
private String mValue;
private boolean mValueSet;
public RangeValuePreference(Context context, String title, String summary, StringSetting setting) {
super(context);
this.context = context;
setTitle(title);
setSummary(summary);
setKey(setting.key);
setValue(setting.get());
}
public void setValue(String value) {
final boolean changed = !TextUtils.equals(mValue, value);
if (changed || !mValueSet) {
mValue = value;
mValueSet = true;
persistString(value);
if (changed) {
notifyDependencyChange(shouldDisableDependents());
notifyChanged();
}
}
}
public String getValue() {
return mValue;
}
@Override
protected View onCreateDialogView() {
minValue = getValue().split("-")[0];
maxValue = getValue().split("-")[1];
LinearLayout dialogView = new LinearLayout(context);
dialogView.setOrientation(LinearLayout.VERTICAL);
LinearLayout minView = new LinearLayout(context);
minView.setOrientation(LinearLayout.HORIZONTAL);
TextView min = new TextView(context);
min.setText("Min: ");
minView.addView(min);
EditText minEditText = new EditText(context);
minEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
minEditText.setText(minValue);
minView.addView(minEditText);
dialogView.addView(minView);
LinearLayout maxView = new LinearLayout(context);
maxView.setOrientation(LinearLayout.HORIZONTAL);
TextView max = new TextView(context);
max.setText("Max: ");
maxView.addView(max);
EditText maxEditText = new EditText(context);
maxEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
maxEditText.setText(maxValue);
maxView.addView(maxEditText);
dialogView.addView(maxView);
minEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
minValue = editable.toString();
}
});
maxEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
maxValue = editable.toString();
}
});
return dialogView;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNegativeButton(android.R.string.cancel, null);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
String newValue = minValue + "-" + maxValue;
setValue(newValue);
}
}
}

View File

@@ -0,0 +1,54 @@
package app.revanced.extension.tiktok.settings.preference;
import android.preference.Preference;
import android.preference.PreferenceScreen;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.extension.tiktok.settings.preference.categories.DownloadsPreferenceCategory;
import app.revanced.extension.tiktok.settings.preference.categories.FeedFilterPreferenceCategory;
import app.revanced.extension.tiktok.settings.preference.categories.ExtensionPreferenceCategory;
import app.revanced.extension.tiktok.settings.preference.categories.SimSpoofPreferenceCategory;
import org.jetbrains.annotations.NotNull;
/**
* Preference fragment for ReVanced settings
*/
@SuppressWarnings("deprecation")
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
@Override
protected void syncSettingWithPreference(@NonNull @NotNull Preference pref,
@NonNull @NotNull Setting<?> setting,
boolean applySettingToPreference) {
if (pref instanceof RangeValuePreference) {
RangeValuePreference rangeValuePref = (RangeValuePreference) pref;
Setting.privateSetValueFromString(setting, rangeValuePref.getValue());
} else if (pref instanceof DownloadPathPreference) {
DownloadPathPreference downloadPathPref = (DownloadPathPreference) pref;
Setting.privateSetValueFromString(setting, downloadPathPref.getValue());
} else {
super.syncSettingWithPreference(pref, setting, applySettingToPreference);
}
}
@Override
protected void initialize() {
final var context = getContext();
// Currently no resources can be compiled for TikTok (fails with aapt error).
// So all TikTok Strings are hard coded in the extension.
restartDialogTitle = "Refresh and restart";
restartDialogButtonText = "Restart";
confirmDialogTitle = "Do you wish to proceed?";
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
// Custom categories reference app specific Settings class.
new FeedFilterPreferenceCategory(context, preferenceScreen);
new DownloadsPreferenceCategory(context, preferenceScreen);
new SimSpoofPreferenceCategory(context, preferenceScreen);
new ExtensionPreferenceCategory(context, preferenceScreen);
}
}

View File

@@ -0,0 +1,55 @@
package app.revanced.extension.tiktok.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import java.util.Map;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
/**
* Because resources cannot be added to TikTok,
* these strings are copied from the shared strings.xml file.
*
* Changes here must also be made in strings.xml
*/
private final Map<String, String> aboutStrings = Map.of(
"revanced_settings_about_links_body", "You are using ReVanced Patches version <i>%s</i>",
"revanced_settings_about_links_dev_header", "Note",
"revanced_settings_about_links_dev_body", "This version is a pre-release and you may experience unexpected issues",
"revanced_settings_about_links_header", "Official links"
);
{
//noinspection deprecation
setTitle("About");
}
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReVancedTikTokAboutPreference(Context context) {
super(context);
}
@Override
protected String getString(String key, Object ... args) {
String format = aboutStrings.get(key);
if (format == null) {
Logger.printException(() -> "Unknown key: " + key);
return "";
}
return String.format(format, args);
}
}

View File

@@ -0,0 +1,17 @@
package app.revanced.extension.tiktok.settings.preference;
import android.content.Context;
import android.preference.SwitchPreference;
import app.revanced.extension.shared.settings.BooleanSetting;
@SuppressWarnings("deprecation")
public class TogglePreference extends SwitchPreference {
public TogglePreference(Context context, String title, String summary, BooleanSetting setting) {
super(context);
this.setTitle(title);
this.setSummary(summary);
this.setKey(setting.key);
this.setChecked(setting.get());
}
}

View File

@@ -0,0 +1,22 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
@SuppressWarnings("deprecation")
public abstract class ConditionalPreferenceCategory extends PreferenceCategory {
public ConditionalPreferenceCategory(Context context, PreferenceScreen screen) {
super(context);
if (getSettingsStatus()) {
screen.addPreference(this);
addPreferences(context);
}
}
public abstract boolean getSettingsStatus();
public abstract void addPreferences(Context context);
}

View File

@@ -0,0 +1,35 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.tiktok.settings.Settings;
import app.revanced.extension.tiktok.settings.SettingsStatus;
import app.revanced.extension.tiktok.settings.preference.DownloadPathPreference;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class DownloadsPreferenceCategory extends ConditionalPreferenceCategory {
public DownloadsPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Downloads");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.downloadEnabled;
}
@Override
public void addPreferences(Context context) {
addPreference(new DownloadPathPreference(
context,
"Download path",
Settings.DOWNLOAD_PATH
));
addPreference(new TogglePreference(
context,
"Remove watermark", "",
Settings.DOWNLOAD_WATERMARK
));
}
}

View File

@@ -0,0 +1,32 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.tiktok.settings.preference.ReVancedTikTokAboutPreference;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory {
public ExtensionPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Miscellaneous");
}
@Override
public boolean getSettingsStatus() {
return true;
}
@Override
public void addPreferences(Context context) {
addPreference(new ReVancedTikTokAboutPreference(context));
addPreference(new TogglePreference(context,
"Enable debug log",
"Show extension debug log.",
BaseSettings.DEBUG
));
}
}

View File

@@ -0,0 +1,55 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.tiktok.settings.preference.RangeValuePreference;
import app.revanced.extension.tiktok.settings.Settings;
import app.revanced.extension.tiktok.settings.SettingsStatus;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory {
public FeedFilterPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Feed filter");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.feedFilterEnabled;
}
@Override
public void addPreferences(Context context) {
addPreference(new TogglePreference(
context,
"Remove feed ads", "Remove ads from feed.",
Settings.REMOVE_ADS
));
addPreference(new TogglePreference(
context,
"Hide livestreams", "Hide livestreams from feed.",
Settings.HIDE_LIVE
));
addPreference(new TogglePreference(
context,
"Hide story", "Hide story from feed.",
Settings.HIDE_STORY
));
addPreference(new TogglePreference(
context,
"Hide image video", "Hide image video from feed.",
Settings.HIDE_IMAGE
));
addPreference(new RangeValuePreference(
context,
"Min/Max views", "The minimum or maximum views of a video to show.",
Settings.MIN_MAX_VIEWS
));
addPreference(new RangeValuePreference(
context,
"Min/Max likes", "The minimum or maximum likes of a video to show.",
Settings.MIN_MAX_LIKES
));
}
}

View File

@@ -0,0 +1,47 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.tiktok.settings.Settings;
import app.revanced.extension.tiktok.settings.SettingsStatus;
import app.revanced.extension.tiktok.settings.preference.InputTextPreference;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class SimSpoofPreferenceCategory extends ConditionalPreferenceCategory {
public SimSpoofPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Bypass regional restriction");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.simSpoofEnabled;
}
@Override
public void addPreferences(Context context) {
addPreference(new TogglePreference(
context,
"Fake sim card info",
"Bypass regional restriction by fake sim card information.",
Settings.SIM_SPOOF
));
addPreference(new InputTextPreference(
context,
"Country ISO", "us, uk, jp, ...",
Settings.SIM_SPOOF_ISO
));
addPreference(new InputTextPreference(
context,
"Operator mcc+mnc", "mcc+mnc",
Settings.SIMSPOOF_MCCMNC
));
addPreference(new InputTextPreference(
context,
"Operator name", "Name of the operator.",
Settings.SIMSPOOF_OP_NAME
));
}
}

View File

@@ -0,0 +1,13 @@
package app.revanced.extension.tiktok.speed;
import app.revanced.extension.tiktok.settings.Settings;
public class PlaybackSpeedPatch {
public static void rememberPlaybackSpeed(float newSpeed) {
Settings.REMEMBERED_SPEED.save(newSpeed);
}
public static float getPlaybackSpeed() {
return Settings.REMEMBERED_SPEED.get();
}
}

View File

@@ -0,0 +1,37 @@
package app.revanced.extension.tiktok.spoof.sim;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class SpoofSimPatch {
private static final boolean ENABLED = Settings.SIM_SPOOF.get();
public static String getCountryIso(String value) {
if (ENABLED) {
String iso = Settings.SIM_SPOOF_ISO.get();
Logger.printDebug(() -> "Spoofing sim ISO from: " + value + " to: " + iso);
return iso;
}
return value;
}
public static String getOperator(String value) {
if (ENABLED) {
String mcc_mnc = Settings.SIMSPOOF_MCCMNC.get();
Logger.printDebug(() -> "Spoofing sim MCC-MNC from: " + value + " to: " + mcc_mnc);
return mcc_mnc;
}
return value;
}
public static String getOperatorName(String value) {
if (ENABLED) {
String operator = Settings.SIMSPOOF_OP_NAME.get();
Logger.printDebug(() -> "Spoofing sim operator from: " + value + " to: " + operator);
return operator;
}
return value;
}
}

View File

@@ -0,0 +1,17 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
}
android {
namespace = "app.revanced.extension"
compileSdk = 33
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,6 @@
package com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization;
import android.app.Activity;
//Dummy class
public class AdPersonalizationActivity extends Activity { }

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