Compare commits

..

37 Commits

Author SHA1 Message Date
semantic-release-bot
59f9e4328f chore: Release v5.50.0-dev.3 [skip ci]
# [5.50.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.2...v5.50.0-dev.3) (2026-01-26)

### Features

* **Kleinanzeigen:** Add `Hide ads` patch ([#6533](https://github.com/ReVanced/revanced-patches/issues/6533)) ([bd6e544](bd6e544007))
2026-01-26 14:24:11 +00:00
xehpuk
bd6e544007 feat(Kleinanzeigen): Add Hide ads patch (#6533)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-26 15:20:59 +01:00
semantic-release-bot
dbdd2a67ff chore: Release v5.50.0-dev.2 [skip ci]
# [5.50.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.1...v5.50.0-dev.2) (2026-01-25)

### Bug Fixes

* **Letterboxd - Hide ads:** Fix patch by returning the correct return type ([#6527](https://github.com/ReVanced/revanced-patches/issues/6527)) ([80c34b9](80c34b9d74))
2026-01-25 10:08:33 +00:00
xehpuk
80c34b9d74 fix(Letterboxd - Hide ads): Fix patch by returning the correct return type (#6527) 2026-01-25 11:03:06 +01:00
semantic-release-bot
debf297d9e chore: Release v5.50.0-dev.1 [skip ci]
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.49.0-dev.1...v5.50.0-dev.1) (2026-01-25)

### Bug Fixes

* **Strava:** Fix `Add media download` patch ([#6526](https://github.com/ReVanced/revanced-patches/issues/6526)) ([dc9e68b](dc9e68ba57))

### Features

* **Nothing X:** Add `Show K1 token(s)` patch ([#6490](https://github.com/ReVanced/revanced-patches/issues/6490)) ([421cb28](421cb2899e))
* **Strava:** Add `Hide distractions` patch ([#6479](https://github.com/ReVanced/revanced-patches/issues/6479)) ([66b0852](66b0852f8f))
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([5edd9dc](5edd9dccae))
2026-01-25 00:53:06 +00:00
xehpuk
dc9e68ba57 fix(Strava): Fix Add media download patch (#6526) 2026-01-25 01:49:33 +01:00
semantic-release-bot
ebea32fe87 chore: Release v5.50.0-dev.1 [skip ci]
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.49.0-dev.1...v5.50.0-dev.1) (2026-01-22)

### Features

* **Nothing X:** Add `Show K1 token(s)` patch ([#6490](https://github.com/ReVanced/revanced-patches/issues/6490)) ([421cb28](421cb2899e))
* **Strava:** Add `Hide distractions` patch ([#6479](https://github.com/ReVanced/revanced-patches/issues/6479)) ([66b0852](66b0852f8f))
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([5edd9dc](5edd9dccae))
2026-01-22 19:21:45 +00:00
xehpuk
66b0852f8f feat(Strava): Add Hide distractions patch (#6479)
Co-authored-by: Pun Butrach <pun.butrach@gmail.com>
Co-authored-by: ekaunt <62402760+ekaunt@users.noreply.github.com>
Co-authored-by: bengross <bengross@vecta.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-22 19:56:27 +01:00
Pa1NarK
421cb2899e feat(Nothing X): Add Show K1 token(s) patch (#6490)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-22 19:17:45 +01:00
semantic-release-bot
97e74157fa chore: Release v5.50.0-dev.1 [skip ci]
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.0...v5.50.0-dev.1) (2026-01-22)

### Features

* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([89645dc](89645dcc2e))
2026-01-22 19:10:39 +01:00
Johannes Obermeier
5edd9dccae feat(YouTube Music): Add Unlock Android Auto Media Browser patch (#6477)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
(cherry picked from commit 89645dcc2e13603b8f2fedb5e16231cb396e5965)
2026-01-22 19:04:09 +01:00
semantic-release-bot
9c18e1e649 chore: Release v5.49.0-dev.1 [skip ci]
# [5.49.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.1-dev.1...v5.49.0-dev.1) (2026-01-22)

### Features

* **YouTube Music:** Add `Hide layout components` patch ([#6365](https://github.com/ReVanced/revanced-patches/issues/6365)) ([71ce823](71ce8230a9))
2026-01-22 17:43:08 +00:00
rospino74
71ce8230a9 feat(YouTube Music): Add Hide layout components patch (#6365)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-22 18:36:37 +01:00
github-actions[bot]
156441d3cf chore: Sync translations (#6516)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-01-22 18:21:28 +01:00
oSumAtrIX
634f47ef84 ci: Set necessary env vars for running Gradle task 2026-01-22 17:37:44 +01:00
oSumAtrIX
eeb133325e ci: Properly implement Crowdin strings processing 2026-01-22 17:37:44 +01:00
semantic-release-bot
e8d58ca9af chore: Release v5.48.1-dev.1 [skip ci]
## [5.48.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.0...v5.48.1-dev.1) (2026-01-21)

### Bug Fixes

* Disable `Prevent screenshot detection` by default ([#6511](https://github.com/ReVanced/revanced-patches/issues/6511)) ([5b5c502](5b5c50254d))
2026-01-21 17:49:31 +00:00
Sayanth
5b5c50254d fix: Disable Prevent screenshot detection by default (#6511) 2026-01-21 18:44:04 +01:00
semantic-release-bot
ef052c0d8f chore: Release v5.48.0 [skip ci]
# [5.48.0](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0) (2026-01-19)

### Bug Fixes

* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](eecc44b956))
* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](44e7dbcf4d))
* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](789f0a5628))
* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](71c6cb569e))
* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](a429824bb7))
* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](87247590de))
* **YouTube - Hide layout components:** Hide new type of crowdfunding box ([#6380](https://github.com/ReVanced/revanced-patches/issues/6380)) ([dc69f24](dc69f2433e))

### Features

* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](4cc315952d))
* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](83c0127ebb))
* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](6312fe8d60))
* **Instagram - Hides navigation buttons:** Add more buttons to hide ([#6390](https://github.com/ReVanced/revanced-patches/issues/6390)) ([6bb6281](6bb6281149))
* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](8725a49ba3))
* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](18c0b04f0c))
* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](3401467a6d))
* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](d25dcfe49a))
* **ProtonVPN:** Add `Unlock split tunneling` patch ([#6353](https://github.com/ReVanced/revanced-patches/issues/6353)) ([e0f3346](e0f33468e6))
* **SBS On Demand:** Add `Remove ads` patch ([#6378](https://github.com/ReVanced/revanced-patches/issues/6378)) ([315931c](315931cbf8))
* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](4c4ba1c78c))
* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](778d13ce8b))
* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](c47beae213))
* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](f5cbb31724))
* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](8f3f4c95bb))
* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](b42ae27ce6))
* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](19f146c01d))
2026-01-19 09:28:58 +00:00
oSumAtrIX
d9fa580222 chore: Merge branch dev to main (#6385) 2026-01-19 10:25:41 +01:00
semantic-release-bot
182224c79d chore: Release v5.48.0-dev.13 [skip ci]
# [5.48.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.12...v5.48.0-dev.13) (2026-01-19)

### Features

* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](83c0127ebb))
2026-01-19 09:21:21 +00:00
xehpuk
83c0127ebb feat: Add Prevent screenshot detection patch (#6482)
Co-authored-by: Swakshan <56347042+Swakshan@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-19 10:16:30 +01:00
semantic-release-bot
3762f1de08 chore: Release v5.48.0-dev.12 [skip ci]
# [5.48.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.11...v5.48.0-dev.12) (2026-01-19)

### Features

* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](18c0b04f0c))
* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](4c4ba1c78c))
2026-01-19 09:06:41 +00:00
Swakshan
18c0b04f0c feat(Instagram): Add Remove build expired popup patch (#6488)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-19 10:01:42 +01:00
xehpuk
4c4ba1c78c feat(Strava): Add Add 'Give Kudos' button to 'Group Activity' patch (#6475)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-19 10:00:25 +01:00
semantic-release-bot
7cef24a5e9 chore: Release v5.48.0-dev.11 [skip ci]
# [5.48.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.10...v5.48.0-dev.11) (2026-01-19)

### Features

* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](8725a49ba3))
2026-01-19 08:56:06 +00:00
Swakshan
8725a49ba3 feat(Instagram): Add Hide highlights tray patch (#6489)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-19 09:52:36 +01:00
semantic-release-bot
8b6360e34f chore: Release v5.48.0-dev.10 [skip ci]
# [5.48.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.9...v5.48.0-dev.10) (2026-01-19)

### Bug Fixes

* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](eecc44b956))
* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](87247590de))

### Features

* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](3401467a6d))
* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](778d13ce8b))
* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](19f146c01d))
2026-01-19 08:40:58 +00:00
Pun Butrach
a10c51f160 ci: Include require environment variables in release step (#6493) 2026-01-19 09:38:25 +01:00
scruz
eecc44b956 fix(Boost for Reddit - Fix missing audio in video downloads): Make it work again by reflecting Reddits latest changes (#6500) 2026-01-18 16:21:59 +01:00
oSumAtrIX
3401467a6d feat(Instagram): Disable Disable Reels scrolling by default 2026-01-15 23:30:17 +01:00
Swakshan
87247590de fix(Instagram): Sanitize sharing links (#6483) 2026-01-13 20:10:25 +01:00
xehpuk
41e2590584 chore(Strava): Restructure media package (#6480) 2026-01-13 15:46:49 +01:00
xehpuk
778d13ce8b feat(Strava): Add Add media download patch (#6449)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-12 23:28:15 +01:00
ekaunt
19f146c01d feat(YouTube): Add Pause on audio interrupt patch (#6464)
Co-authored-by: bengross <bengross@vecta.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2026-01-12 23:18:09 +01:00
Pun Butrach
12b819d20e ci: Schedule Crowdin to runs weekly instead of every 12 hours (#6466) 2026-01-12 02:40:19 +01:00
Pun Butrach
004b5908db build: Use Gradle credentials system (#6467) 2026-01-11 16:59:48 +01:00
936 changed files with 74572 additions and 71566 deletions

View File

@@ -25,7 +25,8 @@ jobs:
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid --no-daemon
- name: Upload artifacts

View File

@@ -2,7 +2,7 @@ name: Pull strings
on:
schedule:
- cron: "0 */12 * * *"
- cron: "0 0 * * 0"
workflow_dispatch:
jobs:

View File

@@ -16,10 +16,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v5
- name: Preprocess strings
- name: Process strings
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew clean preprocessCrowdinStrings
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew processStringsForCrowdin
- name: Push strings
uses: crowdin/github-action@v2

View File

@@ -31,7 +31,8 @@ jobs:
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid clean
- name: Setup Node.js
@@ -55,6 +56,8 @@ jobs:
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
- name: Attest
if: steps.release.outputs.new_release_published == 'true'

View File

@@ -1,3 +1,132 @@
# [5.50.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.2...v5.50.0-dev.3) (2026-01-26)
### Features
* **Kleinanzeigen:** Add `Hide ads` patch ([#6533](https://github.com/ReVanced/revanced-patches/issues/6533)) ([bd6e544](https://github.com/ReVanced/revanced-patches/commit/bd6e544007d539ac2eb890d9bdcb6850435f96cb))
# [5.50.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.1...v5.50.0-dev.2) (2026-01-25)
### Bug Fixes
* **Letterboxd - Hide ads:** Fix patch by returning the correct return type ([#6527](https://github.com/ReVanced/revanced-patches/issues/6527)) ([80c34b9](https://github.com/ReVanced/revanced-patches/commit/80c34b9d74a42018a0cd52b4a584ee71206bf963))
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.49.0-dev.1...v5.50.0-dev.1) (2026-01-25)
### Bug Fixes
* **Strava:** Fix `Add media download` patch ([#6526](https://github.com/ReVanced/revanced-patches/issues/6526)) ([dc9e68b](https://github.com/ReVanced/revanced-patches/commit/dc9e68ba574dd9f35cd742cb63193c5d875addde))
### Features
* **Nothing X:** Add `Show K1 token(s)` patch ([#6490](https://github.com/ReVanced/revanced-patches/issues/6490)) ([421cb28](https://github.com/ReVanced/revanced-patches/commit/421cb2899ef5c0f100fb8007bae8b89137d0e41c))
* **Strava:** Add `Hide distractions` patch ([#6479](https://github.com/ReVanced/revanced-patches/issues/6479)) ([66b0852](https://github.com/ReVanced/revanced-patches/commit/66b0852f8fa57c82b09997337a304374883d8ba5))
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([5edd9dc](https://github.com/ReVanced/revanced-patches/commit/5edd9dccae3b1ab4edf19771a771812e3c9ccf80))
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.49.0-dev.1...v5.50.0-dev.1) (2026-01-22)
### Features
* **Nothing X:** Add `Show K1 token(s)` patch ([#6490](https://github.com/ReVanced/revanced-patches/issues/6490)) ([421cb28](https://github.com/ReVanced/revanced-patches/commit/421cb2899ef5c0f100fb8007bae8b89137d0e41c))
* **Strava:** Add `Hide distractions` patch ([#6479](https://github.com/ReVanced/revanced-patches/issues/6479)) ([66b0852](https://github.com/ReVanced/revanced-patches/commit/66b0852f8fa57c82b09997337a304374883d8ba5))
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([5edd9dc](https://github.com/ReVanced/revanced-patches/commit/5edd9dccae3b1ab4edf19771a771812e3c9ccf80))
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.0...v5.50.0-dev.1) (2026-01-22)
### Features
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([89645dc](https://github.com/ReVanced/revanced-patches/commit/89645dcc2e13603b8f2fedb5e16231cb396e5965))
# [5.49.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.1-dev.1...v5.49.0-dev.1) (2026-01-22)
### Features
* **YouTube Music:** Add `Hide layout components` patch ([#6365](https://github.com/ReVanced/revanced-patches/issues/6365)) ([71ce823](https://github.com/ReVanced/revanced-patches/commit/71ce8230a959dcaf2d8cd5dad1a4f21b88819aa0))
## [5.48.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.0...v5.48.1-dev.1) (2026-01-21)
### Bug Fixes
* Disable `Prevent screenshot detection` by default ([#6511](https://github.com/ReVanced/revanced-patches/issues/6511)) ([5b5c502](https://github.com/ReVanced/revanced-patches/commit/5b5c50254d533faa0e04d542f4859cbef610713e))
# [5.48.0](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0) (2026-01-19)
### Bug Fixes
* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](https://github.com/ReVanced/revanced-patches/commit/eecc44b9567bf2ca72ac99e0dafa483a6803c0f9))
* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](https://github.com/ReVanced/revanced-patches/commit/44e7dbcf4d7eaf94dd0164baba847d3e19250154))
* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8))
* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](https://github.com/ReVanced/revanced-patches/commit/71c6cb569ebf7b93cf73ee391839e5220557ce7c))
* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](https://github.com/ReVanced/revanced-patches/commit/a429824bb77b49aea14b0b54f2204ae24d5209a1))
* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](https://github.com/ReVanced/revanced-patches/commit/87247590de3db74680cb02ba1d87bf683b2269e2))
* **YouTube - Hide layout components:** Hide new type of crowdfunding box ([#6380](https://github.com/ReVanced/revanced-patches/issues/6380)) ([dc69f24](https://github.com/ReVanced/revanced-patches/commit/dc69f2433e2650654e2dffdd76b0b0c8a52bf515))
### Features
* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305))
* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](https://github.com/ReVanced/revanced-patches/commit/83c0127ebb8f53ab8a067758619faaac5596c145))
* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c))
* **Instagram - Hides navigation buttons:** Add more buttons to hide ([#6390](https://github.com/ReVanced/revanced-patches/issues/6390)) ([6bb6281](https://github.com/ReVanced/revanced-patches/commit/6bb62811493da04812cc3e392e68d874f95cbef9))
* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](https://github.com/ReVanced/revanced-patches/commit/8725a49ba3a06fee0280ffcf4be62cd960cd301e))
* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](https://github.com/ReVanced/revanced-patches/commit/18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48))
* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](https://github.com/ReVanced/revanced-patches/commit/3401467a6d49fc75b6757a15e5c848330c1b7307))
* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc))
* **ProtonVPN:** Add `Unlock split tunneling` patch ([#6353](https://github.com/ReVanced/revanced-patches/issues/6353)) ([e0f3346](https://github.com/ReVanced/revanced-patches/commit/e0f33468e6e96b9f10cf35ec67622d6488528c90))
* **SBS On Demand:** Add `Remove ads` patch ([#6378](https://github.com/ReVanced/revanced-patches/issues/6378)) ([315931c](https://github.com/ReVanced/revanced-patches/commit/315931cbf8f61cd4b3a54ace1ff03685d748614c))
* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](https://github.com/ReVanced/revanced-patches/commit/4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f))
* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](https://github.com/ReVanced/revanced-patches/commit/778d13ce8b28ca6df3a665530320e4a21a27ae44))
* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](https://github.com/ReVanced/revanced-patches/commit/c47beae21376dd17ab8bc09afe73e9094481bde9))
* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](https://github.com/ReVanced/revanced-patches/commit/f5cbb31724d15f7e939b96ee0186fd0a108f9fdc))
* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](https://github.com/ReVanced/revanced-patches/commit/8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc))
* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567))
* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](https://github.com/ReVanced/revanced-patches/commit/19f146c01dc381b3cccd61e61ba4901872ff12d8))
# [5.48.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.12...v5.48.0-dev.13) (2026-01-19)
### Features
* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](https://github.com/ReVanced/revanced-patches/commit/83c0127ebb8f53ab8a067758619faaac5596c145))
# [5.48.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.11...v5.48.0-dev.12) (2026-01-19)
### Features
* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](https://github.com/ReVanced/revanced-patches/commit/18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48))
* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](https://github.com/ReVanced/revanced-patches/commit/4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f))
# [5.48.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.10...v5.48.0-dev.11) (2026-01-19)
### Features
* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](https://github.com/ReVanced/revanced-patches/commit/8725a49ba3a06fee0280ffcf4be62cd960cd301e))
# [5.48.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.9...v5.48.0-dev.10) (2026-01-19)
### Bug Fixes
* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](https://github.com/ReVanced/revanced-patches/commit/eecc44b9567bf2ca72ac99e0dafa483a6803c0f9))
* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](https://github.com/ReVanced/revanced-patches/commit/87247590de3db74680cb02ba1d87bf683b2269e2))
### Features
* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](https://github.com/ReVanced/revanced-patches/commit/3401467a6d49fc75b6757a15e5c848330c1b7307))
* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](https://github.com/ReVanced/revanced-patches/commit/778d13ce8b28ca6df3a665530320e4a21a27ae44))
* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](https://github.com/ReVanced/revanced-patches/commit/19f146c01dc381b3cccd61e61ba4901872ff12d8))
# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08)

View File

@@ -11,7 +11,6 @@ import android.widget.Toolbar;
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
import app.revanced.extension.music.settings.search.MusicSearchViewController;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseActivityHook;
@@ -47,7 +46,15 @@ public class MusicActivityHook extends BaseActivityHook {
// Override the default YouTube Music theme to increase start padding of list items.
// Custom style located in resources/music/values/style.xml
activity.setTheme(Utils.getResourceIdentifierOrThrow(
ResourceType.STYLE, "Theme.ReVanced.YouTubeMusic.Settings"));
"Theme.ReVanced.YouTubeMusic.Settings", "style"));
}
/**
* Returns the resource ID for the YouTube Music settings layout.
*/
@Override
protected int getContentViewResourceId() {
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
}
/**

View File

@@ -4,12 +4,12 @@ import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.spoof.ClientType;
public class Settings extends BaseSettings {
public class Settings extends YouTubeAndMusicSettings {
// Ads
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);

View File

@@ -0,0 +1,15 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nothingx:stub"))
}
android {
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

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

View File

@@ -0,0 +1,590 @@
package app.revanced.extension.nothingx.patches;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Application;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Patches to expose the K1 token for Nothing X app to enable pairing with GadgetBridge.
*/
@SuppressWarnings("unused")
public class ShowK1TokensPatch {
private static final String TAG = "ReVanced";
private static final String PACKAGE_NAME = "com.nothing.smartcenter";
private static final String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e";
private static final String PREFS_NAME = "revanced_nothingx_prefs";
private static final String KEY_DONT_SHOW_DIALOG = "dont_show_k1_dialog";
// Colors
private static final int COLOR_BG = 0xFF1E1E1E;
private static final int COLOR_CARD = 0xFF2D2D2D;
private static final int COLOR_TEXT_PRIMARY = 0xFFFFFFFF;
private static final int COLOR_TEXT_SECONDARY = 0xFFB0B0B0;
private static final int COLOR_ACCENT = 0xFFFF9500;
private static final int COLOR_TOKEN_BG = 0xFF3A3A3A;
private static final int COLOR_BUTTON_POSITIVE = 0xFFFF9500;
private static final int COLOR_BUTTON_NEGATIVE = 0xFFFF6B6B;
// Match standalone K1: k1:, K1:, k1>, etc.
private static final Pattern K1_STANDALONE_PATTERN = Pattern.compile("(?i)(?:k1\\s*[:>]\\s*)([0-9a-f]{32})");
// Match combined r3+k1: format (64 chars = r3(32) + k1(32))
private static final Pattern K1_COMBINED_PATTERN = Pattern.compile("(?i)r3\\+k1\\s*:\\s*([0-9a-f]{64})");
private static volatile boolean k1Logged = false;
private static volatile boolean lifecycleCallbacksRegistered = false;
private static Context appContext;
/**
* Get K1 tokens from database and log files.
* Call this after the app initializes.
*
* @param context Application context
*/
public static void showK1Tokens(Context context) {
if (k1Logged) {
return;
}
appContext = context.getApplicationContext();
Set<String> allTokens = new LinkedHashSet<>();
// First try to get from database.
String dbToken = getK1TokensFromDatabase();
if (dbToken != null) {
allTokens.add(dbToken);
}
// Then get from log files.
Set<String> logTokens = getK1TokensFromLogFiles();
allTokens.addAll(logTokens);
if (allTokens.isEmpty()) {
return;
}
// Log all found tokens.
int index = 1;
for (String token : allTokens) {
Log.i(TAG, "#" + index++ + ": " + token.toUpperCase());
}
// Register lifecycle callbacks to show dialog when an Activity is ready.
registerLifecycleCallbacks(allTokens);
k1Logged = true;
}
/**
* Register ActivityLifecycleCallbacks to show dialog when first Activity resumes.
*
* @param tokens Set of K1 tokens to display
*/
private static void registerLifecycleCallbacks(Set<String> tokens) {
if (lifecycleCallbacksRegistered || !(appContext instanceof Application)) {
return;
}
Application application = (Application) appContext;
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
// Check if user chose not to show dialog.
SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
if (prefs.getBoolean(KEY_DONT_SHOW_DIALOG, false)) {
application.unregisterActivityLifecycleCallbacks(this);
lifecycleCallbacksRegistered = false;
return;
}
// Show dialog on first Activity resume.
if (tokens != null && !tokens.isEmpty()) {
activity.runOnUiThread(() -> showK1TokensDialog(activity, tokens));
// Unregister after showing
application.unregisterActivityLifecycleCallbacks(this);
lifecycleCallbacksRegistered = false;
}
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
lifecycleCallbacksRegistered = true;
}
/**
* Show dialog with K1 tokens.
*
* @param activity Activity context
* @param tokens Set of K1 tokens
*/
private static void showK1TokensDialog(Activity activity, Set<String> tokens) {
try {
// Create main container.
LinearLayout mainLayout = new LinearLayout(activity);
mainLayout.setOrientation(LinearLayout.VERTICAL);
mainLayout.setBackgroundColor(COLOR_BG);
mainLayout.setPadding(dpToPx(activity, 24), dpToPx(activity, 16),
dpToPx(activity, 24), dpToPx(activity, 16));
// Title.
TextView titleView = new TextView(activity);
titleView.setText("K1 Token(s) Found");
titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(COLOR_TEXT_PRIMARY);
titleView.setGravity(Gravity.CENTER);
mainLayout.addView(titleView, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
// Subtitle.
TextView subtitleView = new TextView(activity);
subtitleView.setText(tokens.size() == 1 ? "1 token found • Tap to copy" : tokens.size() + " tokens found • Tap to copy");
subtitleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
subtitleView.setTextColor(COLOR_TEXT_SECONDARY);
subtitleView.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
subtitleParams.topMargin = dpToPx(activity, 4);
subtitleParams.bottomMargin = dpToPx(activity, 16);
mainLayout.addView(subtitleView, subtitleParams);
// Scrollable content.
ScrollView scrollView = new ScrollView(activity);
scrollView.setVerticalScrollBarEnabled(false);
LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
0,
1.0f
);
scrollParams.topMargin = dpToPx(activity, 8);
scrollParams.bottomMargin = dpToPx(activity, 16);
mainLayout.addView(scrollView, scrollParams);
LinearLayout tokensContainer = new LinearLayout(activity);
tokensContainer.setOrientation(LinearLayout.VERTICAL);
scrollView.addView(tokensContainer);
// Add each token as a card.
boolean singleToken = tokens.size() == 1;
int index = 1;
for (String token : tokens) {
LinearLayout tokenCard = createTokenCard(activity, token, index++, singleToken);
LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
cardParams.bottomMargin = dpToPx(activity, 12);
tokensContainer.addView(tokenCard, cardParams);
}
// Info text.
TextView infoView = new TextView(activity);
infoView.setText(tokens.size() == 1 ? "Tap the token to copy it" : "Tap any token to copy it");
infoView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
infoView.setTextColor(COLOR_TEXT_SECONDARY);
infoView.setGravity(Gravity.CENTER);
infoView.setAlpha(0.7f);
LinearLayout.LayoutParams infoParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
infoParams.topMargin = dpToPx(activity, 8);
mainLayout.addView(infoView, infoParams);
// Button row.
LinearLayout buttonRow = new LinearLayout(activity);
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
buttonRow.setGravity(Gravity.END);
LinearLayout.LayoutParams buttonRowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
buttonRowParams.topMargin = dpToPx(activity, 16);
mainLayout.addView(buttonRow, buttonRowParams);
// "Don't show again" button.
Button dontShowButton = new Button(activity);
dontShowButton.setText("Don't show again");
dontShowButton.setTextColor(Color.WHITE);
dontShowButton.setBackgroundColor(Color.TRANSPARENT);
dontShowButton.setAllCaps(false);
dontShowButton.setTypeface(Typeface.DEFAULT);
dontShowButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
dontShowButton.setPadding(dpToPx(activity, 16), dpToPx(activity, 8),
dpToPx(activity, 16), dpToPx(activity, 8));
LinearLayout.LayoutParams dontShowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
dontShowParams.rightMargin = dpToPx(activity, 8);
buttonRow.addView(dontShowButton, dontShowParams);
// "OK" button.
Button okButton = new Button(activity);
okButton.setText("OK");
okButton.setTextColor(Color.BLACK);
okButton.setBackgroundColor(COLOR_BUTTON_POSITIVE);
okButton.setAllCaps(false);
okButton.setTypeface(Typeface.DEFAULT_BOLD);
okButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
okButton.setPadding(dpToPx(activity, 24), dpToPx(activity, 12),
dpToPx(activity, 24), dpToPx(activity, 12));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
okButton.setElevation(dpToPx(activity, 4));
}
buttonRow.addView(okButton, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
));
// Build dialog.
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setView(mainLayout);
final AlertDialog dialog = builder.create();
// Style the dialog with dark background.
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
}
dialog.show();
// Set button click listeners after dialog is created.
dontShowButton.setOnClickListener(v -> {
SharedPreferences prefs = activity.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, true).apply();
Toast.makeText(activity, "Dialog disabled. Clear app data to re-enable.",
Toast.LENGTH_SHORT).show();
dialog.dismiss();
});
okButton.setOnClickListener(v -> {
dialog.dismiss();
});
} catch (Exception e) {
Log.e(TAG, "Failed to show K1 dialog", e);
}
}
/**
* Create a card view for a single token.
*/
private static LinearLayout createTokenCard(Activity activity, String token, int index, boolean singleToken) {
LinearLayout card = new LinearLayout(activity);
card.setOrientation(LinearLayout.VERTICAL);
card.setBackgroundColor(COLOR_TOKEN_BG);
card.setPadding(dpToPx(activity, 16), dpToPx(activity, 12),
dpToPx(activity, 16), dpToPx(activity, 12));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
card.setElevation(dpToPx(activity, 2));
}
card.setClickable(true);
card.setFocusable(true);
// Token label (only show if multiple tokens).
if (!singleToken) {
TextView labelView = new TextView(activity);
labelView.setText("Token #" + index);
labelView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12);
labelView.setTextColor(COLOR_ACCENT);
labelView.setTypeface(Typeface.DEFAULT_BOLD);
card.addView(labelView);
}
// Token value.
TextView tokenView = new TextView(activity);
tokenView.setText(token.toUpperCase());
tokenView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
tokenView.setTextColor(COLOR_TEXT_PRIMARY);
tokenView.setTypeface(Typeface.MONOSPACE);
tokenView.setLetterSpacing(0.05f);
LinearLayout.LayoutParams tokenParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
if (!singleToken) {
tokenParams.topMargin = dpToPx(activity, 8);
}
card.addView(tokenView, tokenParams);
// Click to copy.
card.setOnClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard != null) {
clipboard.setText(token.toUpperCase());
Toast.makeText(activity, "Token copied!", Toast.LENGTH_SHORT).show();
}
});
return card;
}
/**
* Convert dp to pixels.
*/
private static int dpToPx(Context context, float dp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
context.getResources().getDisplayMetrics()
);
}
/**
* Get K1 tokens from log files.
* Prioritizes pairing K1 tokens over reconnect tokens.
*/
private static Set<String> getK1TokensFromLogFiles() {
Set<String> pairingTokens = new LinkedHashSet<>();
Set<String> reconnectTokens = new LinkedHashSet<>();
try {
File logDir = new File("/data/data/" + PACKAGE_NAME + "/files/log");
if (!logDir.exists() || !logDir.isDirectory()) {
return pairingTokens;
}
File[] logFiles = logDir.listFiles((dir, name) ->
name.endsWith(".log") || name.endsWith(".log.") || name.matches(".*\\.log\\.\\d+"));
if (logFiles == null || logFiles.length == 0) {
return pairingTokens;
}
for (File logFile : logFiles) {
try (BufferedReader reader = new BufferedReader(new FileReader(logFile))) {
String line;
while ((line = reader.readLine()) != null) {
// Determine if this is a pairing or reconnect context.
boolean isPairingContext = line.toLowerCase().contains("watchbind");
boolean isReconnectContext = line.toLowerCase().contains("watchreconnect");
String k1Token = null;
// First check for combined r3+k1 format (priority).
Matcher combinedMatcher = K1_COMBINED_PATTERN.matcher(line);
if (combinedMatcher.find()) {
String combined = combinedMatcher.group(1);
if (combined.length() == 64) {
// Second half is the actual K1
k1Token = combined.substring(32).toLowerCase();
}
}
// Then check for standalone K1 format (only if not found in combined).
if (k1Token == null) {
Matcher standaloneMatcher = K1_STANDALONE_PATTERN.matcher(line);
if (standaloneMatcher.find()) {
String token = standaloneMatcher.group(1);
if (token != null && token.length() == 32) {
k1Token = token.toLowerCase();
}
}
}
// Add to appropriate set.
if (k1Token != null) {
if (isPairingContext && !isReconnectContext) {
pairingTokens.add(k1Token);
} else {
reconnectTokens.add(k1Token);
}
}
}
} catch (Exception e) {
// Skip unreadable files.
}
}
} catch (Exception ex) {
// Fail silently.
}
// Return pairing tokens first, add reconnect tokens if no pairing tokens found.
if (!pairingTokens.isEmpty()) {
Log.i(TAG, "Found " + pairingTokens.size() + " pairing K1 token(s)");
return pairingTokens;
}
if (!reconnectTokens.isEmpty()) {
Log.i(TAG, "Found " + reconnectTokens.size() + " reconnect K1 token(s) (may not work for initial pairing)");
}
return reconnectTokens;
}
/**
* Try to get K1 tokens from the database.
*/
private static String getK1TokensFromDatabase() {
try {
File dbDir = new File("/data/data/" + PACKAGE_NAME + "/databases");
if (!dbDir.exists() || !dbDir.isDirectory()) {
return null;
}
File[] dbFiles = dbDir.listFiles((dir, name) ->
name.endsWith(".db") && !name.startsWith("google_app_measurement") && !name.contains("firebase"));
if (dbFiles == null || dbFiles.length == 0) {
return null;
}
for (File dbFile : dbFiles) {
String token = getK1TokensFromDatabase(dbFile);
if (token != null) {
return token;
}
}
return null;
} catch (Exception ex) {
return null;
}
}
/**
* Extract K1 tokens from a database file.
*/
private static String getK1TokensFromDatabase(File dbFile) {
SQLiteDatabase db = null;
try {
db = SQLiteDatabase.openDatabase(dbFile.getPath(), null, SQLiteDatabase.OPEN_READONLY);
// Get all tables.
Cursor cursor = db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
null
);
List<String> tables = new ArrayList<>();
while (cursor.moveToNext()) {
tables.add(cursor.getString(0));
}
cursor.close();
// Scan all columns for 32-char hex strings.
for (String table : tables) {
Cursor schemaCursor = null;
try {
schemaCursor = db.rawQuery("PRAGMA table_info(" + table + ")", null);
List<String> columns = new ArrayList<>();
while (schemaCursor.moveToNext()) {
columns.add(schemaCursor.getString(1));
}
schemaCursor.close();
for (String column : columns) {
Cursor dataCursor = null;
try {
dataCursor = db.query(table, new String[]{column}, null, null, null, null, null);
while (dataCursor.moveToNext()) {
String value = dataCursor.getString(0);
if (value != null && value.length() == 32 && value.matches("[0-9a-fA-F]{32}")) {
// Skip obviously fake tokens (MD5 of empty string).
if (!value.equalsIgnoreCase(EMPTY_MD5)) {
dataCursor.close();
db.close();
return value.toLowerCase();
}
}
}
} catch (Exception e) {
// Skip non-string columns.
} finally {
if (dataCursor != null) {
dataCursor.close();
}
}
}
} catch (Exception e) {
// Continue to next table.
} finally {
if (schemaCursor != null && !schemaCursor.isClosed()) {
schemaCursor.close();
}
}
}
return null;
} catch (Exception ex) {
return null;
} finally {
if (db != null && db.isOpen()) {
db.close();
}
}
}
/**
* Reset the logged flag (useful for testing or re-pairing).
*/
public static void resetK1Logged() {
k1Logged = false;
lifecycleCallbacksRegistered = false;
}
/**
* Reset the "don't show again" preference.
*/
public static void resetDontShowPreference() {
if (appContext != null) {
SharedPreferences prefs = appContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putBoolean(KEY_DONT_SHOW_DIALOG, false).apply();
}
}
}

View File

@@ -0,0 +1,17 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

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

View File

@@ -1,57 +0,0 @@
package app.revanced.extension.shared;
import java.util.HashMap;
import java.util.Map;
public enum ResourceType {
ANIM("anim"),
ANIMATOR("animator"),
ARRAY("array"),
ATTR("attr"),
BOOL("bool"),
COLOR("color"),
DIMEN("dimen"),
DRAWABLE("drawable"),
FONT("font"),
FRACTION("fraction"),
ID("id"),
INTEGER("integer"),
INTERPOLATOR("interpolator"),
LAYOUT("layout"),
MENU("menu"),
MIPMAP("mipmap"),
NAVIGATION("navigation"),
PLURALS("plurals"),
RAW("raw"),
STRING("string"),
STYLE("style"),
STYLEABLE("styleable"),
TRANSITION("transition"),
VALUES("values"),
XML("xml");
private static final Map<String, ResourceType> VALUE_MAP;
static {
ResourceType[] values = values();
VALUE_MAP = new HashMap<>(2 * values.length);
for (ResourceType type : values) {
VALUE_MAP.put(type.value, type);
}
}
public final String value;
public static ResourceType fromValue(String value) {
ResourceType type = VALUE_MAP.get(value);
if (type == null) {
throw new IllegalArgumentException("Unknown resource type: " + value);
}
return type;
}
ResourceType(String value) {
this.value = value;
}
}

View File

@@ -32,11 +32,7 @@ import android.view.Window;
import android.view.WindowManager;
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.ColorInt;
import androidx.annotation.NonNull;
@@ -47,10 +43,8 @@ import java.text.Collator;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
@@ -82,8 +76,6 @@ public class Utils {
@Nullable
private static Boolean isDarkModeEnabled;
private static boolean appIsUsingBoldIcons;
// Cached Collator instance with its locale.
@Nullable
private static Locale cachedCollatorLocale;
@@ -156,12 +148,12 @@ public class Utils {
/**
* Hide a view by setting its layout height and width to 1dp.
*
* @param setting The setting to check for hiding the view.
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewBy0dpUnderCondition(BooleanSetting setting, View view) {
if (hideViewBy0dpUnderCondition(setting.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + setting);
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
@@ -173,47 +165,22 @@ public class Utils {
*/
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
if (condition) {
hideViewBy0dp(view);
hideViewByLayoutParams(view);
return true;
}
return false;
}
/**
* Hide a view by setting its layout params to 0x0
* @param view The view to hide.
*/
public static void hideViewBy0dp(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 {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.width = 0;
params.height = 0;
view.setLayoutParams(params);
}
}
/**
* Hide a view by setting its visibility to GONE.
*
* @param setting The setting to check for hiding the view.
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewUnderCondition(BooleanSetting setting, View view) {
if (hideViewUnderCondition(setting.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + setting);
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
if (hideViewUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
@@ -232,14 +199,14 @@ public class Utils {
return false;
}
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting setting, View view) {
if (hideViewByRemovingFromParentUnderCondition(setting.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + setting);
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 condition, View view) {
if (condition) {
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
if (setting) {
ViewParent parent = view.getParent();
if (parent instanceof ViewGroup parentGroup) {
parentGroup.removeView(view);
@@ -311,13 +278,12 @@ public class Utils {
* @return zero, if the resource is not found.
*/
@SuppressLint("DiscouragedApi")
public static int getResourceIdentifier(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
return context.getResources().getIdentifier(resourceIdentifierName,
type == null ? null : type.value, context.getPackageName());
public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String type) {
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
}
public static int getResourceIdentifierOrThrow(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName);
public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) {
final int resourceId = getResourceIdentifier(context, resourceIdentifierName, type);
if (resourceId == 0) {
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
+ " type: " + type);
@@ -327,44 +293,52 @@ public class Utils {
/**
* @return zero, if the resource is not found.
* @see #getResourceIdentifierOrThrow(ResourceType, String)
* @see #getResourceIdentifierOrThrow(String, String)
*/
public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) {
return getResourceIdentifier(getContext(), type, resourceIdentifierName);
public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String type) {
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
}
/**
* @return zero, if the resource is not found.
* @see #getResourceIdentifier(ResourceType, String)
* @return The resource identifier, or throws an exception if not found.
*/
public static int getResourceIdentifierOrThrow(@Nullable ResourceType type, String resourceIdentifierName) {
return getResourceIdentifierOrThrow(getContext(), type, resourceIdentifierName);
public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String type) {
final int resourceId = getResourceIdentifier(getContext(), resourceIdentifierName, type);
if (resourceId == 0) {
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
+ " type: " + type);
}
return resourceId;
}
public static String getResourceString(int id) throws Resources.NotFoundException {
return getContext().getResources().getString(id);
}
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName));
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
}
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(ResourceType.ANIM, resourceIdentifierName));
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(resourceIdentifierName, "anim"));
}
@ColorInt
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
//noinspection deprecation
return getContext().getResources().getColor(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName));
return getContext().getResources().getColor(getResourceIdentifierOrThrow(resourceIdentifierName, "color"));
}
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
}
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
}
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName));
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(resourceIdentifierName, "array"));
}
public interface MatchFilter<T> {
@@ -375,7 +349,7 @@ public class Utils {
* Includes sub children.
*/
public static <R extends View> R getChildViewByResourceName(View view, String str) {
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str));
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(str, "id"));
//noinspection unchecked
return (R) child;
}
@@ -832,21 +806,6 @@ public class Utils {
window.setBackgroundDrawable(null); // Remove default dialog background
}
/**
* @return If the unpatched app is currently using bold icons.
*/
public static boolean appIsUsingBoldIcons() {
return appIsUsingBoldIcons;
}
/**
* Controls if ReVanced bold icons are shown in various places.
* @param boldIcons If the app is currently using bold icons.
*/
public static void setAppIsUsingBoldIcons(boolean boldIcons) {
appIsUsingBoldIcons = boldIcons;
}
/**
* Sets the theme light color used by the app.
*/
@@ -1208,18 +1167,4 @@ public class Utils {
public static float clamp(float value, float lower, float upper) {
return Math.max(lower, Math.min(value, upper));
}
/**
* @param maxSize The maximum number of elements to keep in the map.
* @return A {@link LinkedHashMap} that automatically evicts the oldest entry
* when the size exceeds {@code maxSize}.
*/
public static <T, V> Map<T, V> createSizeRestrictedMap(int maxSize) {
return new LinkedHashMap<>(2 * maxSize) {
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > maxSize;
}
};
}
}

View File

@@ -23,7 +23,6 @@ import androidx.annotation.Nullable;
import java.util.Collection;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.ui.CustomDialog;
@@ -129,7 +128,7 @@ abstract class Check {
// Add icon to the dialog.
ImageView iconView = new ImageView(activity);
iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_ic_dialog_alert"));
"revanced_ic_dialog_alert", "drawable"));
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
iconView.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(

View File

@@ -15,6 +15,7 @@ import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public abstract class BaseFixRedgifsApiPatch implements Interceptor {
protected static BaseFixRedgifsApiPatch INSTANCE;
public abstract String getDefaultUserAgent();

View File

@@ -13,7 +13,6 @@ import java.util.Locale;
import app.revanced.extension.shared.GmsCoreSupport;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
@@ -66,7 +65,7 @@ public class CustomBrandingPatch {
iconName += "_custom";
}
notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName);
notificationSmallIcon = Utils.getResourceIdentifier(iconName, "drawable");
if (notificationSmallIcon == 0) {
Logger.printException(() -> "Could not load notification small icon");
}

View File

@@ -1,4 +1,4 @@
package app.revanced.extension.youtube.patches.components;
package app.revanced.extension.shared.patches.components;
import static app.revanced.extension.shared.StringRef.str;
@@ -15,13 +15,15 @@ import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
import app.revanced.extension.shared.patches.litho.Filter;
/**
* Allows custom filtering using a path and optionally a proto buffer string.
*/
@SuppressWarnings("unused")
final class CustomFilter extends Filter {
public final class CustomFilter extends Filter {
private static void showInvalidSyntaxToast(@NonNull String expression) {
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
@@ -45,7 +47,7 @@ final class CustomFilter extends Filter {
@NonNull
@SuppressWarnings("ConstantConditions")
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
String rawCustomFilterText = YouTubeAndMusicSettings.CUSTOM_FILTER_STRINGS.get();
if (rawCustomFilterText.isBlank()) {
return Collections.emptyList();
}
@@ -100,7 +102,7 @@ final class CustomFilter extends Filter {
ByteTrieSearch bufferSearch;
CustomFilterGroup(boolean startsWith, @NonNull String path) {
super(Settings.CUSTOM_FILTER, path);
super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
this.startsWith = startsWith;
}
@@ -145,7 +147,7 @@ final class CustomFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// All callbacks are custom filter groups.
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
@@ -159,4 +161,4 @@ final class CustomFilter extends Filter {
return custom.bufferSearch.matches(buffer);
}
}
}

View File

@@ -1,9 +1,12 @@
package app.revanced.extension.youtube.patches.components;
package app.revanced.extension.shared.patches.litho;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
/**
* Filters litho based components.
*
@@ -14,11 +17,11 @@ import java.util.List;
* either an identifier or a path.
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
* or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
*
* All callbacks must be registered before the constructor completes.
*/
abstract class Filter {
public abstract class Filter {
public enum FilterContentType {
IDENTIFIER,
@@ -65,7 +68,7 @@ abstract class Filter {
* @param contentIndex Matched index of the identifier or path.
* @return True if the litho component should be filtered out.
*/
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
return true;
}

View File

@@ -0,0 +1,213 @@
package app.revanced.extension.shared.patches.litho;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BooleanSetting;
public abstract class FilterGroup<T> {
public final static class FilterGroupResult {
private BooleanSetting setting;
private int matchedIndex;
private int matchedLength;
// In the future it might be useful to include which pattern matched,
// but for now that is not needed.
FilterGroupResult() {
this(null, -1, 0);
}
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
setValues(setting, matchedIndex, matchedLength);
}
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
this.setting = setting;
this.matchedIndex = matchedIndex;
this.matchedLength = matchedLength;
}
/**
* A null value if the group has no setting,
* or if no match is returned from {@link FilterGroupList#check(Object)}.
*/
public BooleanSetting getSetting() {
return setting;
}
public boolean isFiltered() {
return matchedIndex >= 0;
}
/**
* Matched index of first pattern that matched, or -1 if nothing matched.
*/
public int getMatchedIndex() {
return matchedIndex;
}
/**
* Length of the matched filter pattern.
*/
public int getMatchedLength() {
return matchedLength;
}
}
protected final BooleanSetting setting;
protected final T[] filters;
/**
* Initialize a new filter group.
*
* @param setting The associated setting.
* @param filters The filters.
*/
@SafeVarargs
public FilterGroup(final BooleanSetting setting, final T... filters) {
this.setting = setting;
this.filters = filters;
if (filters.length == 0) {
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
}
}
public boolean isEnabled() {
return setting == null || setting.get();
}
/**
* @return If {@link FilterGroupList} should include this group when searching.
* By default, all filters are included except non enabled settings that require reboot.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean includeInSearch() {
return isEnabled() || !setting.rebootApp;
}
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
}
public abstract FilterGroupResult check(final T stack);
public static class StringFilterGroup extends FilterGroup<String> {
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
super(setting, filters);
}
@Override
public FilterGroupResult check(final String string) {
int matchedIndex = -1;
int matchedLength = 0;
if (isEnabled()) {
for (String pattern : filters) {
if (!string.isEmpty()) {
final int indexOf = string.indexOf(pattern);
if (indexOf >= 0) {
matchedIndex = indexOf;
matchedLength = pattern.length();
break;
}
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}
/**
* If you have more than 1 filter patterns, then all instances of
* this class should filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
* which uses a prefix tree to give better performance.
*/
public static class ByteArrayFilterGroup extends FilterGroup<byte[]> {
private volatile int[][] failurePatterns;
// Modified implementation from https://stackoverflow.com/a/1507813
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
// Finds the first occurrence of the pattern in the byte array using
// KMP matching algorithm.
int patternLength = pattern.length;
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
while (j > 0 && pattern[j] != data[i]) {
j = failure[j - 1];
}
if (pattern[j] == data[i]) {
j++;
}
if (j == patternLength) {
return i - patternLength + 1;
}
}
return -1;
}
private static int[] createFailurePattern(byte[] pattern) {
// Computes the failure function using a boot-strapping process,
// where the pattern is matched against itself.
final int patternLength = pattern.length;
final int[] failure = new int[patternLength];
for (int i = 1, j = 0; i < patternLength; i++) {
while (j > 0 && pattern[j] != pattern[i]) {
j = failure[j - 1];
}
if (pattern[j] == pattern[i]) {
j++;
}
failure[i] = j;
}
return failure;
}
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
super(setting, filters);
}
/**
* Converts the Strings into byte arrays. Used to search for text in binary data.
*/
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
}
private synchronized void buildFailurePatterns() {
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
Logger.printDebug(() -> "Building failure array for: " + this);
int[][] failurePatterns = new int[filters.length][];
int i = 0;
for (byte[] pattern : filters) {
failurePatterns[i++] = createFailurePattern(pattern);
}
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
}
@Override
public FilterGroupResult check(final byte[] bytes) {
int matchedLength = 0;
int matchedIndex = -1;
if (isEnabled()) {
int[][] failures = failurePatterns;
if (failures == null) {
buildFailurePatterns(); // Lazy load.
failures = failurePatterns;
}
for (int i = 0, length = filters.length; i < length; i++) {
byte[] filter = filters[i];
matchedIndex = indexOf(bytes, filter, failures[i]);
if (matchedIndex >= 0) {
matchedLength = filter.length;
break;
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}
}

View File

@@ -1,21 +1,22 @@
package app.revanced.extension.youtube.patches.components;
package app.revanced.extension.shared.patches.litho;
import androidx.annotation.NonNull;
import java.util.*;
import java.util.function.Consumer;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.TrieSearch;
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
public abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
private final List<T> filterGroups = new ArrayList<>();
private final TrieSearch<V> search = createSearchGraph();
@SafeVarargs
protected final void addAll(final T... groups) {
public final void addAll(final T... groups) {
filterGroups.addAll(Arrays.asList(groups));
for (T group : groups) {
@@ -41,18 +42,7 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
return filterGroups.iterator();
}
@Override
public void forEach(@NonNull Consumer<? super T> action) {
filterGroups.forEach(action);
}
@NonNull
@Override
public Spliterator<T> spliterator() {
return filterGroups.spliterator();
}
protected FilterGroup.FilterGroupResult check(V stack) {
public FilterGroup.FilterGroupResult check(V stack) {
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
search.matches(stack, result);
return result;
@@ -60,21 +50,21 @@ abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<
}
protected abstract TrieSearch<V> createSearchGraph();
}
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
protected StringTrieSearch createSearchGraph() {
return new StringTrieSearch();
public static final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
protected StringTrieSearch createSearchGraph() {
return new StringTrieSearch();
}
}
}
/**
* If searching for a single byte pattern, then it is slightly better to use
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
* than a prefix tree to search for only 1 pattern.
*/
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
protected ByteTrieSearch createSearchGraph() {
return new ByteTrieSearch();
/**
* If searching for a single byte pattern, then it is slightly better to use
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
* than a prefix tree to search for only 1 pattern.
*/
public static final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
protected ByteTrieSearch createSearchGraph() {
return new ByteTrieSearch();
}
}
}

View File

@@ -1,20 +1,17 @@
package app.revanced.extension.youtube.patches.components;
package app.revanced.extension.shared.patches.litho;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
@SuppressWarnings("unused")
public final class LithoFilterPatch {
@@ -41,7 +38,7 @@ public final class LithoFilterPatch {
builder.append(identifier);
builder.append(" Path: ");
builder.append(path);
if (Settings.DEBUG_PROTOBUFFER.get()) {
if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) {
builder.append(" BufferStrings: ");
findAsciiStrings(builder, buffer);
}
@@ -78,15 +75,6 @@ public final class LithoFilterPatch {
}
}
/**
* Placeholder for actual filters.
*/
private static final class DummyFilter extends Filter { }
private static final Filter[] filters = new Filter[] {
new DummyFilter() // Replaced during patching, do not touch.
};
/**
* Litho layout fixed thread pool size override.
* <p>
@@ -104,50 +92,25 @@ public final class LithoFilterPatch {
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
/**
* 20.22+ cannot use the thread buffer, because frequently the buffer is not correct,
* especially for components that are recreated such as dragging off screen then back on screen.
* Instead, parse the identifier found near the start of the buffer and use that to
* identify the correct buffer to use when filtering.
* Placeholder for actual filters.
*/
private static final boolean EXTRACT_IDENTIFIER_FROM_BUFFER = VersionCheckPatch.IS_20_22_OR_GREATER;
private static final class DummyFilter extends Filter { }
/**
* Turns on additional logging, used for development purposes only.
*/
public static final boolean DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER = false;
private static final Filter[] filters = new Filter[] {
new DummyFilter() // Replaced patching, do not touch.
};
/**
* String suffix for components.
* Can be any of: ".eml", ".e-b", ".eml-js", "e-js-b"
*/
private static final String LITHO_COMPONENT_EXTENSION = ".e";
private static final byte[] LITHO_COMPONENT_EXTENSION_BYTES = LITHO_COMPONENT_EXTENSION.getBytes(StandardCharsets.US_ASCII);
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
/**
* Because litho filtering is multi-threaded and the buffer is passed in from a different injection point,
* the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads.
* Used for 20.21 and lower.
*/
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
/**
* Identifier to protocol buffer mapping. Only used for 20.22+.
* Thread local is needed because filtering is multi-threaded and each thread can load
* a different component with the same identifier.
*/
private static final ThreadLocal<Map<String, byte[]>> identifierToBufferThread = new ThreadLocal<>();
/**
* Global shared buffer. Used only if the buffer is not found in the ThreadLocal.
*/
private static final Map<String, byte[]> identifierToBufferGlobal
= Collections.synchronizedMap(createIdentifierToBufferMap());
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
static {
for (Filter filter : filters) {
filterUsingCallbacks(identifierSearchTree, filter,
@@ -199,107 +162,16 @@ public final class LithoFilterPatch {
}
}
private static Map<String, byte[]> createIdentifierToBufferMap() {
// It's unclear how many items should be cached. This is a guess.
return Utils.createSizeRestrictedMap(100);
}
/**
* Helper function that differs from {@link Character#isDigit(char)}
* as this only matches ascii and not unicode numbers.
*/
private static boolean isAsciiNumber(byte character) {
return '0' <= character && character <= '9';
}
private static boolean isAsciiLowerCaseLetter(byte character) {
return 'a' <= character && character <= 'z';
}
/**
* Injection point. Called off the main thread.
* Targets 20.22+
*/
public static void setProtoBuffer(byte[] buffer) {
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
StringBuilder builder = new StringBuilder();
LithoFilterParameters.findAsciiStrings(builder, buffer);
Logger.printDebug(() -> "New buffer: " + builder);
}
// Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of
// unique characters, but it seems to be slower since the extra overhead of checking the
// bad character array negates any performance gain of skipping a few extra subsearches.
int emlIndex = -1;
final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length;
for (int i = 0, lastStartIndex = buffer.length - emlStringLength; i <= lastStartIndex; i++) {
boolean match = true;
for (int j = 0; j < emlStringLength; j++) {
if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) {
match = false;
break;
}
}
if (match) {
emlIndex = i;
break;
}
}
if (emlIndex < 0) {
// Buffer is not used for creating a new litho component.
return;
}
int startIndex = emlIndex - 1;
while (startIndex > 0) {
final byte character = buffer[startIndex];
int startIndexFinal = startIndex;
if (isAsciiLowerCaseLetter(character) || isAsciiNumber(character) || character == '_') {
// Valid character for the first path element.
startIndex--;
} else {
startIndex++;
break;
}
}
// Strip away any numbers on the start of the identifier, which can
// be from random data in the buffer before the identifier starts.
while (true) {
final byte character = buffer[startIndex];
if (isAsciiNumber(character)) {
startIndex++;
} else {
break;
}
}
// Find the pipe character after the identifier.
int endIndex = -1;
for (int i = emlIndex, length = buffer.length; i < length; i++) {
if (buffer[i] == '|') {
endIndex = i;
break;
}
}
if (endIndex < 0) {
Logger.printException(() -> "Could not find buffer identifier");
return;
}
String identifier = new String(buffer, startIndex, endIndex - startIndex, StandardCharsets.US_ASCII);
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
Logger.printDebug(() -> "Found buffer for identifier: " + identifier);
}
identifierToBufferGlobal.put(identifier, buffer);
Map<String, byte[]> map = identifierToBufferThread.get();
if (map == null) {
map = createIdentifierToBufferMap();
identifierToBufferThread.set(map);
}
map.put(identifier, buffer);
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
// The buffer will be cleared from memory after a new buffer is set by the same thread,
// or when the calling thread eventually dies.
bufferThreadLocal.set(buffer);
}
/**
@@ -307,70 +179,46 @@ public final class LithoFilterPatch {
* Targets 20.21 and lower.
*/
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
// The buffer will be cleared from memory after a new buffer is set by the same thread,
// or when the calling thread eventually dies.
if (buffer == null || !buffer.hasArray()) {
// It appears the buffer can be cleared out just before the call to #filter()
// Ignore this null value and retain the last buffer that was set.
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
} else {
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
// The buffer will be cleared from memory after a new buffer is set by the same thread,
// or when the calling thread eventually dies.
bufferThreadLocal.set(buffer.array());
setProtoBuffer(buffer.array());
}
}
/**
* Injection point.
*/
public static boolean isFiltered(String identifier, StringBuilder pathBuilder) {
public static boolean isFiltered(String lithoIdentifier, StringBuilder pathBuilder) {
try {
if (identifier.isEmpty() || pathBuilder.length() == 0) {
if (lithoIdentifier.isEmpty() && pathBuilder.length() == 0) {
return false;
}
byte[] buffer = null;
if (EXTRACT_IDENTIFIER_FROM_BUFFER) {
final int pipeIndex = identifier.indexOf('|');
if (pipeIndex >= 0) {
// If the identifier contains no pipe, then it's not an ".eml" identifier
// and the buffer is not uniquely identified. Typically this only happens
// for subcomponents where buffer filtering is not used.
String identifierKey = identifier.substring(0, pipeIndex);
var map = identifierToBufferThread.get();
if (map != null) {
buffer = map.get(identifierKey);
}
if (buffer == null) {
// Buffer for thread local not found. Use the last buffer found from any thread.
buffer = identifierToBufferGlobal.get(identifierKey);
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER && buffer == null) {
// No buffer is found for some components, such as
// shorts_lockup_cell.eml on channel profiles.
// For now, just ignore this and filter without a buffer.
Logger.printException(() -> "Could not find global buffer for identifier: " + identifier);
}
}
}
} else {
buffer = bufferThreadLocal.get();
}
byte[] buffer = bufferThreadLocal.get();
// Potentially the buffer may have been null or never set up until now.
// Use an empty buffer so the litho id/path filters that do not use a buffer still work.
// Use an empty buffer so the litho id/path filters still work correctly.
if (buffer == null) {
buffer = EMPTY_BYTE_ARRAY;
}
String path = pathBuilder.toString();
LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, buffer);
LithoFilterParameters parameter = new LithoFilterParameters(
lithoIdentifier, pathBuilder.toString(), buffer);
Logger.printDebug(() -> "Searching " + parameter);
return identifierSearchTree.matches(identifier, parameter)
|| pathSearchTree.matches(path, parameter);
if (identifierSearchTree.matches(parameter.identifier, parameter)) {
return true;
}
if (pathSearchTree.matches(parameter.path, parameter)) {
return true;
}
} catch (Exception ex) {
Logger.printException(() -> "isFiltered failure", ex);
}

View File

@@ -13,7 +13,6 @@ import android.widget.TextView;
import android.widget.Toolbar;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
import app.revanced.extension.shared.ui.Dim;
@@ -26,13 +25,13 @@ import app.revanced.extension.shared.ui.Dim;
public abstract class BaseActivityHook extends Activity {
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_settings_fragments");
getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
private static final int ID_REVANCED_TOOLBAR_PARENT =
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_toolbar_parent");
getResourceIdentifierOrThrow("revanced_toolbar_parent", "id");
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_settings_with_toolbar");
getResourceIdentifierOrThrow("revanced_settings_with_toolbar", "layout");
private static final int STRING_REVANCED_SETTINGS_TITLE =
getResourceIdentifierOrThrow(ResourceType.STRING, "revanced_settings_title");
getResourceIdentifierOrThrow("revanced_settings_title", "string");
/**
* Layout parameters for the toolbar, extracted from the dummy toolbar.
@@ -124,18 +123,16 @@ public abstract class BaseActivityHook extends Activity {
toolBarParent.addView(toolbar, 0);
}
/**
* Returns the resource ID for the content view layout.
*/
protected int getContentViewResourceId() {
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
}
/**
* Customizes the activity's theme.
*/
protected abstract void customizeActivityTheme(Activity activity);
/**
* Returns the resource ID for the content view layout.
*/
protected abstract int getContentViewResourceId();
/**
* Returns the background color for the toolbar.
*/

View File

@@ -5,8 +5,6 @@ import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
import static app.revanced.extension.shared.settings.Setting.parent;
import app.revanced.extension.shared.Logger;
/**
* Settings shared across multiple apps.
* <p>
@@ -26,19 +24,10 @@ public class BaseSettings {
* Use the icons declared in the preferences created during patching. If no icons or styles are declared then this setting does nothing.
*/
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
/**
* Do not use this setting directly. Instead use {@link app.revanced.extension.shared.Utils#appIsUsingBoldIcons()}
*/
public static final BooleanSetting SETTINGS_DISABLE_BOLD_ICONS = new BooleanSetting("revanced_settings_disable_bold_icons", FALSE, true);
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "");
/**
* The first time the app was launched with no previous app data (either a clean install, or after wiping app data).
*/
public static final LongSetting FIRST_TIME_APP_LAUNCHED = new LongSetting("revanced_last_time_app_was_launched", -1L, false, false);
//
// Settings shared by YouTube and YouTube Music.
//
@@ -55,13 +44,4 @@ public class BaseSettings {
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true);
public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG));
static {
final long now = System.currentTimeMillis();
if (FIRST_TIME_APP_LAUNCHED.get() < 0) {
Logger.printInfo(() -> "First launch of installation with no prior app data");
FIRST_TIME_APP_LAUNCHED.save(now);
}
}
}

View File

@@ -0,0 +1,14 @@
package app.revanced.extension.shared.settings;
import static app.revanced.extension.shared.settings.Setting.parent;
import static java.lang.Boolean.FALSE;
public class YouTubeAndMusicSettings extends BaseSettings {
// Custom filter
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
// Miscellaneous
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
}

View File

@@ -23,7 +23,6 @@ import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
@@ -104,16 +103,10 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* so all app specific {@link Setting} instances are loaded before this method returns.
*/
protected void initialize() {
String preferenceResourceName;
if (BaseSettings.SHOW_MENU_ICONS.get()) {
preferenceResourceName = Utils.appIsUsingBoldIcons()
? "revanced_prefs_icons_bold"
: "revanced_prefs_icons";
} else {
preferenceResourceName = "revanced_prefs";
}
final var identifier = Utils.getResourceIdentifier(ResourceType.XML, preferenceResourceName);
String preferenceResourceName = BaseSettings.SHOW_MENU_ICONS.get()
? "revanced_prefs_icons"
: "revanced_prefs";
final var identifier = Utils.getResourceIdentifier(preferenceResourceName, "xml");
if (identifier == 0) return;
addPreferencesFromResource(identifier);

View File

@@ -31,7 +31,6 @@ import java.util.Locale;
import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
@@ -82,13 +81,13 @@ public class ColorPickerPreference extends EditTextPreference {
private boolean opacitySliderEnabled = false;
public static final int ID_REVANCED_COLOR_PICKER_VIEW =
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_color_picker_view");
getResourceIdentifierOrThrow("revanced_color_picker_view", "id");
public static final int ID_PREFERENCE_COLOR_DOT =
getResourceIdentifierOrThrow(ResourceType.ID, "preference_color_dot");
getResourceIdentifierOrThrow("preference_color_dot", "id");
public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_dot_widget");
getResourceIdentifierOrThrow("revanced_color_dot_widget", "layout");
public static final int LAYOUT_REVANCED_COLOR_PICKER =
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_picker");
getResourceIdentifierOrThrow("revanced_color_picker", "layout");
/**
* Removes non valid hex characters, converts to all uppercase,

View File

@@ -20,7 +20,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.ui.CustomDialog;
@@ -31,18 +30,14 @@ import app.revanced.extension.shared.ui.CustomDialog;
@SuppressWarnings({"unused", "deprecation"})
public class CustomDialogListPreference extends ListPreference {
public static final int ID_REVANCED_CHECK_ICON = getResourceIdentifierOrThrow(
ResourceType.ID, "revanced_check_icon");
public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER = getResourceIdentifierOrThrow(
ResourceType.ID, "revanced_check_icon_placeholder");
public static final int ID_REVANCED_ITEM_TEXT = getResourceIdentifierOrThrow(
ResourceType.ID, "revanced_item_text");
public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED = getResourceIdentifierOrThrow(
ResourceType.LAYOUT, "revanced_custom_list_item_checked");
public static final int DRAWABLE_CHECKMARK = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_custom_checkmark");
public static final int DRAWABLE_CHECKMARK_BOLD = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_custom_checkmark_bold");
public static final int ID_REVANCED_CHECK_ICON =
getResourceIdentifierOrThrow("revanced_check_icon", "id");
public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER =
getResourceIdentifierOrThrow("revanced_check_icon_placeholder", "id");
public static final int ID_REVANCED_ITEM_TEXT =
getResourceIdentifierOrThrow("revanced_item_text", "id");
public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED =
getResourceIdentifierOrThrow("revanced_custom_list_item_checked", "layout");
private String staticSummary = null;
private CharSequence[] highlightedEntriesForDialog = null;
@@ -130,13 +125,9 @@ public class CustomDialogListPreference extends ListPreference {
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(layoutResourceId, parent, false);
holder = new SubViewDataContainer();
holder.checkIcon = view.findViewById(ID_REVANCED_CHECK_ICON);
holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
holder.itemText = view.findViewById(ID_REVANCED_ITEM_TEXT);
holder.checkIcon = view.findViewById(ID_REVANCED_CHECK_ICON);
holder.checkIcon.setImageResource(Utils.appIsUsingBoldIcons()
? DRAWABLE_CHECKMARK_BOLD
: DRAWABLE_CHECKMARK
);
view.setTag(holder);
} else {
holder = (SubViewDataContainer) view.getTag();

View File

@@ -38,7 +38,6 @@ import java.util.Set;
import java.util.TreeSet;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.patches.EnableDebuggingPatch;
import app.revanced.extension.shared.settings.BaseSettings;
@@ -53,26 +52,25 @@ import app.revanced.extension.shared.ui.Dim;
public class FeatureFlagsManagerPreference extends Preference {
private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_select_all");
getResourceIdentifierOrThrow("revanced_settings_select_all", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_deselect_all");
getResourceIdentifierOrThrow("revanced_settings_deselect_all", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_copy_all");
getResourceIdentifierOrThrow("revanced_settings_copy_all", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_one");
getResourceIdentifierOrThrow("revanced_settings_arrow_right_one", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_double");
getResourceIdentifierOrThrow("revanced_settings_arrow_right_double", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_one");
getResourceIdentifierOrThrow("revanced_settings_arrow_left_one", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_double");
getResourceIdentifierOrThrow("revanced_settings_arrow_left_double", "drawable");
/**
* Flags to hide from the UI.
*/
private static final Set<Long> FLAGS_TO_IGNORE = Set.of(
45386834L, // 'You' tab settings icon.
45685201L // Bold icons. Forcing off interferes with patch changes and YT icons are broken.
45386834L // 'You' tab settings icon.
);
/**

View File

@@ -17,11 +17,9 @@ import android.widget.Toolbar;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseActivityHook;
import app.revanced.extension.shared.ui.Dim;
import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings({"deprecation", "NewApi"})
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
@@ -135,10 +133,8 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
*/
@SuppressLint("UseCompatLoadingForDrawables")
public static Drawable getBackButtonDrawable() {
final int backButtonResource = Utils.getResourceIdentifierOrThrow(ResourceType.DRAWABLE,
Utils.appIsUsingBoldIcons()
? "revanced_settings_toolbar_arrow_left_bold"
: "revanced_settings_toolbar_arrow_left");
final int backButtonResource = Utils.getResourceIdentifierOrThrow(
"revanced_settings_toolbar_arrow_left", "drawable");
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
customizeBackButtonDrawable(drawable);
return drawable;

View File

@@ -16,7 +16,6 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
@@ -39,18 +38,18 @@ public abstract class BaseSearchResultItem {
// Get the corresponding layout resource ID.
public int getLayoutResourceId() {
return switch (this) {
case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
};
}
private static int getResourceIdentifier(String name) {
// Placeholder for actual resource identifier retrieval.
return Utils.getResourceIdentifierOrThrow(ResourceType.LAYOUT, name);
return Utils.getResourceIdentifierOrThrow(name, "layout");
}
}

View File

@@ -1,6 +1,7 @@
package app.revanced.extension.shared.settings.search;
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
import static app.revanced.extension.shared.settings.search.BaseSearchViewController.DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
@@ -32,7 +33,6 @@ import java.lang.reflect.Method;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
@@ -54,15 +54,15 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
protected static final int PAUSE_BETWEEN_BLINKS = 100;
protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_title");
"preference_title", "id");
protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_summary");
"preference_summary", "id");
protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_path");
"preference_path", "id");
protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_switch");
"preference_switch", "id");
protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_color_dot");
"preference_color_dot", "id");
protected static class RegularViewHolder {
TextView titleView;
@@ -275,7 +275,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
holder.titleView.setText(item.highlightedTitle);
holder.summaryView.setText(item.highlightedSummary);
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
holder.iconView.setImageResource(BaseSearchViewController.getSearchIcon());
holder.iconView.setImageResource(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
}
/**

View File

@@ -14,7 +14,6 @@ import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
@@ -38,7 +37,6 @@ import java.util.Set;
import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.AppLanguage;
import app.revanced.extension.shared.settings.BaseSettings;
@@ -72,29 +70,14 @@ public abstract class BaseSearchViewController {
protected static final int MAX_SEARCH_RESULTS = 50; // Maximum number of search results displayed.
protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow(
ResourceType.ID, "revanced_search_view");
protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow(
ResourceType.ID, "revanced_search_view_container");
protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow(
ResourceType.ID, "action_search");
protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow(
ResourceType.ID, "revanced_settings_fragments");
private static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_search_icon");
private static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON_BOLD = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_search_icon_bold");
protected static final int MENU_REVANCED_SEARCH_MENU = getResourceIdentifierOrThrow(
ResourceType.MENU, "revanced_search_menu");
/**
* @return The search icon, either bold or not bold, depending on the ReVanced UI setting.
*/
public static int getSearchIcon() {
return Utils.appIsUsingBoldIcons()
? DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON_BOLD
: DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON;
}
protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow("revanced_search_view", "id");
protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow("revanced_search_view_container", "id");
protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow("action_search", "id");
protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
public static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON =
getResourceIdentifierOrThrow("revanced_settings_search_icon", "drawable");
protected static final int MENU_REVANCED_SEARCH_MENU =
getResourceIdentifierOrThrow("revanced_search_menu", "menu");
/**
* Constructs a new BaseSearchViewController instance.
@@ -129,7 +112,7 @@ public abstract class BaseSearchViewController {
// Retrieve SearchView and container from XML.
searchView = activity.findViewById(ID_REVANCED_SEARCH_VIEW);
EditText searchEditText = searchView.findViewById(Utils.getResourceIdentifierOrThrow(
null, "android:id/search_src_text"));
"android:id/search_src_text", null));
// Disable fullscreen keyboard mode.
searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
@@ -265,10 +248,6 @@ public abstract class BaseSearchViewController {
}
return false;
});
// Set bold icon if needed.
MenuItem search = toolbar.getMenu().findItem(ID_ACTION_SEARCH);
search.setIcon(getSearchIcon());
}
/**
@@ -545,7 +524,7 @@ public abstract class BaseSearchViewController {
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
noResultsPreference.setSelectable(false);
noResultsPreference.setIcon(getSearchIcon());
noResultsPreference.setIcon(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList()));
}

View File

@@ -24,8 +24,6 @@ import java.util.Deque;
import java.util.LinkedList;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.BulletPointPreference;
import app.revanced.extension.shared.ui.CustomDialog;
@@ -39,35 +37,25 @@ public class SearchHistoryManager {
private static final int MAX_HISTORY_SIZE = 5; // Maximum history items stored.
private static final int ID_CLEAR_HISTORY_BUTTON = getResourceIdentifierOrThrow(
ResourceType.ID, "clear_history_button");
"clear_history_button", "id");
private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow(
ResourceType.ID, "history_text");
private static final int ID_HISTORY_ICON = getResourceIdentifierOrThrow(
ResourceType.ID, "history_icon");
"history_text", "id");
private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow(
ResourceType.ID, "delete_icon");
"delete_icon", "id");
private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow(
ResourceType.ID, "empty_history_title");
"empty_history_title", "id");
private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow(
ResourceType.ID, "empty_history_summary");
"empty_history_summary", "id");
private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow(
ResourceType.ID, "search_history_header");
"search_history_header", "id");
private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow(
ResourceType.ID, "revanced_settings_search_tips_summary");
"revanced_settings_search_tips_summary", "id");
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow(
ResourceType.LAYOUT, "revanced_preference_search_history_screen");
"revanced_preference_search_history_screen", "layout");
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow(
ResourceType.LAYOUT, "revanced_preference_search_history_item");
"revanced_preference_search_history_item", "layout");
private static final int ID_SEARCH_HISTORY_LIST = getResourceIdentifierOrThrow(
ResourceType.ID, "search_history_list");
private static final int ID_SEARCH_REMOVE_ICON = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_search_remove");
private static final int ID_SEARCH_REMOVE_ICON_BOLD = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_search_remove_bold");
private static final int ID_SEARCH_ARROW_TIME_ICON = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_arrow_time");
private static final int ID_SEARCH_ARROW_TIME_ICON_BOLD = getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_settings_arrow_time_bold");
"search_history_list", "id");
private final Deque<String> searchHistory;
private final Activity activity;
@@ -109,8 +97,7 @@ public class SearchHistoryManager {
// Inflate search history layout.
LayoutInflater inflater = LayoutInflater.from(activity);
View historyView = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN,
searchHistoryContainer, false);
View historyView = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN, searchHistoryContainer, false);
searchHistoryContainer.addView(historyView, new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT));
@@ -333,29 +320,17 @@ public class SearchHistoryManager {
public void notifyDataSetChanged() {
container.removeAllViews();
for (String query : history) {
View view = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM,
container, false);
View view = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM, container, false);
TextView historyText = view.findViewById(ID_HISTORY_TEXT);
ImageView deleteIcon = view.findViewById(ID_DELETE_ICON);
historyText.setText(query);
// Set click listener for main item (select query).
view.setOnClickListener(v -> onSelectHistoryItemListener.onSelectHistoryItem(query));
// Set history icon.
ImageView historyIcon = view.findViewById(ID_HISTORY_ICON);
historyIcon.setImageResource(Utils.appIsUsingBoldIcons()
? ID_SEARCH_ARROW_TIME_ICON_BOLD
: ID_SEARCH_ARROW_TIME_ICON
);
TextView historyText = view.findViewById(ID_HISTORY_TEXT);
historyText.setText(query);
// Set click listener for delete icon.
ImageView deleteIcon = view.findViewById(ID_DELETE_ICON);
deleteIcon.setImageResource(Utils.appIsUsingBoldIcons()
? ID_SEARCH_REMOVE_ICON_BOLD
: ID_SEARCH_REMOVE_ICON
);
deleteIcon.setOnClickListener(v -> createAndShowDialog(
query,
str("revanced_settings_search_remove_message"),

View File

@@ -16,6 +16,7 @@ import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -82,15 +83,22 @@ public class StreamingDataRequest {
*/
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
/**
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
Utils.createSizeRestrictedMap(50));
new LinkedHashMap<>(100) {
/**
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private static final int CACHE_LIMIT = 50;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});
/**
* Strings found in the response if the video is a livestream.

View File

@@ -1,7 +1,6 @@
package app.revanced.extension.spotify.layout.hide.createbutton;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.spotify.shared.ComponentFilters.ComponentFilter;
import app.revanced.extension.spotify.shared.ComponentFilters.ResourceIdComponentFilter;
import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFilter;
@@ -17,7 +16,7 @@ public final class HideCreateButtonPatch {
* The main approach used is matching the resource id for the Create button title.
*/
private static final List<ComponentFilter> CREATE_BUTTON_COMPONENT_FILTERS = List.of(
new ResourceIdComponentFilter(ResourceType.STRING, "navigationbar_musicappitems_create_title"),
new ResourceIdComponentFilter("navigationbar_musicappitems_create_title", "string"),
// Temporary fallback and fix for APKs merged with AntiSplit-M not having resources properly encoded,
// and thus getting the resource identifier for the Create button title always return 0.
// FIXME: Remove this once the above issue is no longer relevant.
@@ -29,7 +28,7 @@ public final class HideCreateButtonPatch {
* Used in older versions of the app.
*/
private static final ResourceIdComponentFilter OLD_CREATE_BUTTON_COMPONENT_FILTER =
new ResourceIdComponentFilter(ResourceType.STRING, "bottom_navigation_bar_create_tab_title");
new ResourceIdComponentFilter("bottom_navigation_bar_create_tab_title", "string");
/**
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.

View File

@@ -3,7 +3,6 @@ package app.revanced.extension.spotify.shared;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
public final class ComponentFilters {
@@ -20,26 +19,21 @@ public final class ComponentFilters {
public static final class ResourceIdComponentFilter implements ComponentFilter {
public final String resourceName;
public final ResourceType resourceType;
public final String resourceType;
// Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded.
// 0 is returned when a resource has not been found.
private int resourceId = -1;
@Nullable
private String stringfiedResourceId;
@Deprecated
public ResourceIdComponentFilter(String resourceName, String resourceType) {
this(ResourceType.valueOf(resourceType), resourceName);
}
public ResourceIdComponentFilter(ResourceType resourceType, String resourceName) {
this.resourceName = resourceName;
this.resourceType = resourceType;
}
public int getResourceId() {
if (resourceId == -1) {
resourceId = Utils.getResourceIdentifier(resourceType, resourceName);
resourceId = Utils.getResourceIdentifier(resourceName, resourceType);
}
return resourceId;
}

View File

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

View File

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

View File

@@ -0,0 +1,216 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
import com.strava.mediamodels.data.MediaType;
import com.strava.photos.data.Media;
import okhttp3.*;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import app.revanced.extension.shared.Utils;
@SuppressLint("NewApi")
public final class AddMediaDownloadPatch {
public static final int ACTION_DOWNLOAD = -1;
public static final int ACTION_OPEN_LINK = -2;
public static final int ACTION_COPY_LINK = -3;
private static final OkHttpClient client = new OkHttpClient();
public static boolean handleAction(int actionId, Media media) {
String url = getUrl(media);
switch (actionId) {
case ACTION_DOWNLOAD:
String name = media.getId();
if (media.getType() == MediaType.VIDEO) {
downloadVideo(url, name);
} else {
downloadPhoto(url, name);
}
return true;
case ACTION_OPEN_LINK:
Utils.openLink(url);
return true;
case ACTION_COPY_LINK:
copyLink(url);
return true;
default:
return false;
}
}
public static void copyLink(CharSequence url) {
Utils.setClipboard(url);
showInfoToast("link_copied_to_clipboard", "🔗");
}
public static void downloadPhoto(String url, String name) {
showInfoToast("loading", "");
Utils.runOnBackgroundThread(() -> {
try (Response response = fetch(url)) {
ResponseBody body = response.body();
String mimeType = body.contentType().toString();
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
ContentResolver resolver = Utils.getContext().getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, name + '.' + extension);
values.put(MediaStore.Images.Media.IS_PENDING, 1);
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/Strava");
Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
: MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri row = resolver.insert(collection, values);
try (OutputStream outputStream = resolver.openOutputStream(row)) {
transferTo(body.byteStream(), outputStream);
} finally {
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(row, values, null);
}
showInfoToast("yis_2024_local_save_image_success", "✔️");
} catch (IOException e) {
showErrorToast("download_failure", "", e);
}
});
}
/**
* Downloads a video in the M3U8 / HLS (HTTP Live Streaming) format.
*/
public static void downloadVideo(String url, String name) {
// The first request yields multiple URLs with different stream options.
// In case of Strava, the first one is always of highest quality.
// Each stream can consist of multiple chunks.
// The second request yields the URLs of all of these chunks.
// Fetch all of them concurrently and pipe their streams into the file in order.
showInfoToast("loading", "");
Utils.runOnBackgroundThread(() -> {
try {
String highestQualityStreamUrl;
try (Response response = fetch(url)) {
highestQualityStreamUrl = replaceFileName(url, lines(response).findFirst().get());
}
List<Future<Response>> futures;
try (Response response = fetch(highestQualityStreamUrl)) {
futures = lines(response)
.map(line -> replaceFileName(highestQualityStreamUrl, line))
.map(chunkUrl -> Utils.submitOnBackgroundThread(() -> fetch(chunkUrl)))
.collect(Collectors.toList());
}
ContentResolver resolver = Utils.getContext().getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DISPLAY_NAME, name + '.' + "mp4");
values.put(MediaStore.Video.Media.IS_PENDING, 1);
values.put(MediaStore.Video.Media.MIME_TYPE, MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp4"));
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/Strava");
Uri collection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
: MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
Uri row = resolver.insert(collection, values);
try (OutputStream outputStream = resolver.openOutputStream(row)) {
Throwable error = null;
for (Future<Response> future : futures) {
if (error != null) {
if (future.cancel(true)) {
continue;
}
}
try (Response response = future.get()) {
if (error == null) {
transferTo(response.body().byteStream(), outputStream);
}
} catch (InterruptedException | IOException e) {
error = e;
} catch (ExecutionException e) {
error = e.getCause();
}
}
if (error != null) {
throw new IOException(error);
}
} finally {
values.clear();
values.put(MediaStore.Video.Media.IS_PENDING, 0);
resolver.update(row, values, null);
}
showInfoToast("yis_2024_local_save_video_success", "✔️");
} catch (IOException e) {
showErrorToast("download_failure", "", e);
}
});
}
private static String getUrl(Media media) {
return media.getType() == MediaType.VIDEO
? ((Media.Video) media).getVideoUrl()
: media.getLargestUrl();
}
private static String getString(String name, String fallback) {
int id = Utils.getResourceIdentifier(name, "string");
return id != 0
? Utils.getResourceString(id)
: fallback;
}
private static void showInfoToast(String resourceName, String fallback) {
String text = getString(resourceName, fallback);
Utils.showToastShort(text);
}
private static void showErrorToast(String resourceName, String fallback, IOException exception) {
String text = getString(resourceName, fallback);
Utils.showToastLong(text + ' ' + exception.getLocalizedMessage());
}
private static Response fetch(String url) throws IOException {
Request request = new Request.Builder().url(url).build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new IOException("Got HTTP status code " + response.code());
}
return response;
}
/**
* {@code inputStream.transferTo(outputStream)} is "too new".
*/
private static void transferTo(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[1024 * 8];
int length;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
}
/**
* Gets all file names.
*/
private static Stream<String> lines(Response response) {
BufferedReader reader = new BufferedReader(response.body().charStream());
return reader.lines().filter(line -> !line.startsWith("#"));
}
private static String replaceFileName(String uri, String newName) {
return uri.substring(0, uri.lastIndexOf('/') + 1) + newName;
}
}

View File

@@ -0,0 +1,227 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import com.strava.modularframework.data.Destination;
import com.strava.modularframework.data.GenericLayoutModule;
import com.strava.modularframework.data.GenericModuleField;
import com.strava.modularframework.data.ListField;
import com.strava.modularframework.data.ListProperties;
import com.strava.modularframework.data.ModularComponent;
import com.strava.modularframework.data.ModularEntry;
import com.strava.modularframework.data.ModularEntryContainer;
import com.strava.modularframework.data.ModularMenuItem;
import com.strava.modularframework.data.Module;
import com.strava.modularframework.data.MultiStateFieldDescriptor;
import com.strava.modularframeworknetwork.ModularEntryNetworkContainer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@SuppressLint("NewApi")
public class HideDistractionsPatch {
public static boolean upselling;
public static boolean promo;
public static boolean followSuggestions;
public static boolean challengeSuggestions;
public static boolean joinChallenge;
public static boolean joinClub;
public static boolean activityLookback;
public static List<ModularEntry> filterChildrenEntries(ModularEntry modularEntry) {
if (hideModularEntry(modularEntry)) {
return Collections.emptyList();
}
return modularEntry.getChildrenEntries$original().stream()
.filter(childrenEntry -> !hideModularEntry(childrenEntry))
.collect(Collectors.toList());
}
public static List<ModularEntry> filterEntries(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return Collections.emptyList();
}
return modularEntryContainer.getEntries$original().stream()
.filter(entry -> !hideModularEntry(entry))
.collect(Collectors.toList());
}
public static List<ModularEntry> filterEntries(ModularEntryNetworkContainer modularEntryNetworkContainer) {
if (hideModularEntryNetworkContainer(modularEntryNetworkContainer)) {
return Collections.emptyList();
}
return modularEntryNetworkContainer.getEntries$original().stream()
.filter(entry -> !hideModularEntry(entry))
.collect(Collectors.toList());
}
public static List<ModularMenuItem> filterMenuItems(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return Collections.emptyList();
}
return modularEntryContainer.getMenuItems$original().stream()
.filter(menuItem -> !hideModularMenuItem(menuItem))
.collect(Collectors.toList());
}
public static ListProperties filterProperties(ModularEntryContainer modularEntryContainer) {
if (hideModularEntryContainer(modularEntryContainer)) {
return null;
}
return modularEntryContainer.getProperties$original();
}
public static ListProperties filterProperties(ModularEntryNetworkContainer modularEntryNetworkContainer) {
if (hideModularEntryNetworkContainer(modularEntryNetworkContainer)) {
return null;
}
return modularEntryNetworkContainer.getProperties$original();
}
public static ListField filterField(ListProperties listProperties, String key) {
ListField listField = listProperties.getField$original(key);
if (hideListField(listField)) {
return null;
}
return listField;
}
public static List<ListField> filterFields(ListField listField) {
if (hideListField(listField)) {
return null;
}
return listField.getFields$original().stream()
.filter(field -> !hideListField(field))
.collect(Collectors.toList());
}
public static List<Module> filterModules(ModularEntry modularEntry) {
if (hideModularEntry(modularEntry)) {
return Collections.emptyList();
}
return modularEntry.getModules$original().stream()
.filter(module -> !hideModule(module))
.collect(Collectors.toList());
}
public static GenericModuleField filterField(GenericLayoutModule genericLayoutModule, String key) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return null;
}
GenericModuleField field = genericLayoutModule.getField$original(key);
if (hideGenericModuleField(field)) {
return null;
}
return field;
}
public static GenericModuleField[] filterFields(GenericLayoutModule genericLayoutModule) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return new GenericModuleField[0];
}
return Arrays.stream(genericLayoutModule.getFields$original())
.filter(field -> !hideGenericModuleField(field))
.toArray(GenericModuleField[]::new);
}
public static GenericLayoutModule[] filterSubmodules(GenericLayoutModule genericLayoutModule) {
if (hideGenericLayoutModule(genericLayoutModule)) {
return new GenericLayoutModule[0];
}
return Arrays.stream(genericLayoutModule.getSubmodules$original())
.filter(submodule -> !hideGenericLayoutModule(submodule))
.toArray(GenericLayoutModule[]::new);
}
public static List<Module> filterSubmodules(ModularComponent modularComponent) {
if (hideByName(modularComponent.getPage()) || hideByName(modularComponent.getElement())) {
return Collections.emptyList();
}
return modularComponent.getSubmodules$original().stream()
.filter(submodule -> !hideModule(submodule))
.collect(Collectors.toList());
}
public static Map<String, GenericModuleField> filterStateMap(MultiStateFieldDescriptor multiStateFieldDescriptor) {
return multiStateFieldDescriptor.getStateMap$original().entrySet().stream()
.filter(entry -> !hideGenericModuleField(entry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static boolean hideModule(Module module) {
return module == null ||
hideByName(module.getPage()) ||
hideByName(module.getElement());
}
private static boolean hideModularEntry(ModularEntry modularEntry) {
return modularEntry == null ||
hideByName(modularEntry.getPage()) ||
hideByName(modularEntry.getElement()) ||
hideByDestination(modularEntry.getDestination());
}
private static boolean hideGenericLayoutModule(GenericLayoutModule genericLayoutModule) {
try {
return genericLayoutModule == null ||
hideByName(genericLayoutModule.getPage()) ||
hideByName(genericLayoutModule.getElement()) ||
hideByDestination(genericLayoutModule.getDestination());
} catch (RuntimeException getParentEntryOrThrowException) {
return false;
}
}
private static boolean hideListField(ListField listField) {
return listField == null ||
hideByName(listField.getElement()) ||
hideByDestination(listField.getDestination());
}
private static boolean hideGenericModuleField(GenericModuleField genericModuleField) {
return genericModuleField == null ||
hideByName(genericModuleField.getElement()) ||
hideByDestination(genericModuleField.getDestination());
}
private static boolean hideModularEntryContainer(ModularEntryContainer modularEntryContainer) {
return modularEntryContainer == null ||
hideByName(modularEntryContainer.getPage());
}
private static boolean hideModularEntryNetworkContainer(ModularEntryNetworkContainer modularEntryNetworkContainer) {
return modularEntryNetworkContainer == null ||
hideByName(modularEntryNetworkContainer.getPage());
}
private static boolean hideModularMenuItem(ModularMenuItem modularMenuItem) {
return modularMenuItem == null ||
hideByName(modularMenuItem.getElementName()) ||
hideByDestination(modularMenuItem.getDestination());
}
private static boolean hideByName(String name) {
return name != null && (
upselling && name.contains("_upsell") ||
promo && (name.equals("promo") || name.equals("top_of_tab_promo")) ||
followSuggestions && name.equals("suggested_follows") ||
challengeSuggestions && name.equals("suggested_challenges") ||
joinChallenge && name.equals("challenge") ||
joinClub && name.equals("club") ||
activityLookback && name.equals("highlighted_activity_lookback")
);
}
private static boolean hideByDestination(Destination destination) {
if (destination == null) {
return false;
}
String url = destination.getUrl();
return url != null && (
upselling && url.startsWith("strava://subscription/checkout")
);
}
}

View File

@@ -0,0 +1,12 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 21
}
}

View File

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

View File

@@ -0,0 +1,15 @@
package com.strava.mediamodels.data;
import java.io.Serializable;
public interface MediaContent extends Serializable {
String getCaption();
String getId();
String getReferenceId();
MediaType getType();
void setCaption(String caption);
}

View File

@@ -0,0 +1,44 @@
package com.strava.mediamodels.data;
import java.io.Serializable;
public final class MediaDimension implements Comparable<MediaDimension>, Serializable {
private final int height;
private final int width;
public MediaDimension(int width, int height) {
this.width = width;
this.height = height;
}
public int getHeight() {
return height;
}
public float getHeightScale() {
if (width <= 0 || height <= 0) {
return 1f;
}
return height / width;
}
public int getWidth() {
return width;
}
public float getWidthScale() {
if (width <= 0 || height <= 0) {
return 1f;
}
return width / height;
}
public boolean isLandscape() {
return width > 0 && width >= height;
}
@Override
public int compareTo(MediaDimension other) {
return 0;
}
}

View File

@@ -0,0 +1,16 @@
package com.strava.mediamodels.data;
public enum MediaType {
PHOTO(1),
VIDEO(2);
private final int remoteValue;
private MediaType(int remoteValue) {
this.remoteValue = remoteValue;
}
public int getRemoteValue() {
return remoteValue;
}
}

View File

@@ -0,0 +1,17 @@
package com.strava.mediamodels.data;
import java.util.SortedMap;
public interface RemoteMediaContent extends MediaContent {
MediaDimension getLargestSize();
String getLargestUrl();
SortedMap<Integer, MediaDimension> getSizes();
String getSmallestUrl();
RemoteMediaStatus getStatus();
SortedMap<Integer, String> getUrls();
}

View File

@@ -0,0 +1,11 @@
package com.strava.mediamodels.data;
public enum RemoteMediaStatus {
NEW,
PENDING,
PROCESSED,
REPORTED,
REINSTATED,
DELETED,
FAILED
}

View File

@@ -0,0 +1,7 @@
package com.strava.modularframework.data;
import java.io.Serializable;
public abstract class Destination implements Serializable {
public abstract String getUrl();
}

View File

@@ -0,0 +1,28 @@
package com.strava.modularframework.data;
import java.io.Serializable;
public abstract class GenericLayoutModule implements Serializable, Module {
public abstract Destination getDestination();
@Override
public abstract String getElement();
public abstract GenericModuleField getField(String key);
// Added by patch.
public abstract GenericModuleField getField$original(String key);
public abstract GenericModuleField[] getFields();
// Added by patch.
public abstract GenericModuleField[] getFields$original();
@Override
public abstract String getPage();
public abstract GenericLayoutModule[] getSubmodules();
// Added by patch.
public abstract GenericLayoutModule[] getSubmodules$original();
}

View File

@@ -0,0 +1,9 @@
package com.strava.modularframework.data;
import java.io.Serializable;
public abstract class GenericModuleField implements Serializable {
public abstract Destination getDestination();
public abstract String getElement();
}

View File

@@ -0,0 +1,14 @@
package com.strava.modularframework.data;
import java.util.List;
public abstract class ListField {
public abstract Destination getDestination();
public abstract String getElement();
public abstract List<ListField> getFields();
// Added by patch.
public abstract List<ListField> getFields$original();
}

View File

@@ -0,0 +1,8 @@
package com.strava.modularframework.data;
public abstract class ListProperties {
public abstract ListField getField(String key);
// Added by patch.
public abstract ListField getField$original(String key);
}

View File

@@ -0,0 +1,16 @@
package com.strava.modularframework.data;
import java.util.List;
public abstract class ModularComponent implements Module {
@Override
public abstract String getElement();
@Override
public abstract String getPage();
public abstract List<Module> getSubmodules();
// Added by patch.
public abstract List<Module> getSubmodules$original();
}

View File

@@ -0,0 +1,21 @@
package com.strava.modularframework.data;
import java.util.List;
public interface ModularEntry {
List<ModularEntry> getChildrenEntries();
// Added by patch.
List<ModularEntry> getChildrenEntries$original();
Destination getDestination();
String getElement();
List<Module> getModules();
// Added by patch.
List<Module> getModules$original();
String getPage();
}

View File

@@ -0,0 +1,22 @@
package com.strava.modularframework.data;
import java.util.List;
public abstract class ModularEntryContainer {
public abstract List<ModularEntry> getEntries();
// Added by patch.
public abstract List<ModularEntry> getEntries$original();
public abstract List<ModularMenuItem> getMenuItems();
// Added by patch.
public abstract List<ModularMenuItem> getMenuItems$original();
public abstract String getPage();
public abstract ListProperties getProperties();
// Added by patch.
public abstract ListProperties getProperties$original();
}

View File

@@ -0,0 +1,7 @@
package com.strava.modularframework.data;
public abstract class ModularMenuItem {
public abstract Destination getDestination();
public abstract String getElementName();
}

View File

@@ -0,0 +1,7 @@
package com.strava.modularframework.data;
public interface Module {
String getElement();
String getPage();
}

View File

@@ -0,0 +1,10 @@
package com.strava.modularframework.data;
import java.util.Map;
public abstract class MultiStateFieldDescriptor {
public abstract Map<String, GenericModuleField> getStateMap();
// Added by patch.
public abstract Map<String, GenericModuleField> getStateMap$original();
}

View File

@@ -0,0 +1,19 @@
package com.strava.modularframeworknetwork;
import com.strava.modularframework.data.ListProperties;
import com.strava.modularframework.data.ModularEntry;
import java.util.List;
public abstract class ModularEntryNetworkContainer {
public abstract List<ModularEntry> getEntries();
// Added by patch.
public abstract List<ModularEntry> getEntries$original();
public abstract String getPage();
public abstract ListProperties getProperties();
// Added by patch.
public abstract ListProperties getProperties$original();
}

View File

@@ -0,0 +1,287 @@
package com.strava.photos.data;
import com.strava.mediamodels.data.MediaDimension;
import com.strava.mediamodels.data.MediaType;
import com.strava.mediamodels.data.RemoteMediaContent;
import com.strava.mediamodels.data.RemoteMediaStatus;
import java.util.SortedMap;
public abstract class Media implements RemoteMediaContent {
public static final class Photo extends Media {
private final Long activityId;
private final String activityName;
private final long athleteId;
private String caption;
private final String createdAt;
private final String createdAtLocal;
private final String cursor;
private final String id;
private final SortedMap<Integer, MediaDimension> sizes;
private final RemoteMediaStatus status;
private final String tag;
private final MediaType type;
private final SortedMap<Integer, String> urls;
@Override
public Long getActivityId() {
return activityId;
}
@Override
public String getActivityName() {
return activityName;
}
@Override
public long getAthleteId() {
return athleteId;
}
@Override
public String getCaption() {
return caption;
}
@Override
public String getCreatedAt() {
return createdAt;
}
@Override
public String getCreatedAtLocal() {
return createdAtLocal;
}
@Override
public String getCursor() {
return cursor;
}
@Override
public String getId() {
return id;
}
@Override
public SortedMap<Integer, MediaDimension> getSizes() {
return sizes;
}
@Override
public RemoteMediaStatus getStatus() {
return status;
}
@Override
public String getTag() {
return tag;
}
@Override
public MediaType getType() {
return type;
}
@Override
public SortedMap<Integer, String> getUrls() {
return urls;
}
@Override
public void setCaption(String caption) {
this.caption = caption;
}
public Photo(String id,
String caption,
SortedMap<Integer, String> urls,
SortedMap<Integer, MediaDimension> sizes,
long athleteId,
String createdAt,
String createdAtLocal,
Long activityId,
String activityName,
RemoteMediaStatus status,
String tag,
String cursor) {
this.id = id;
this.caption = caption;
this.urls = urls;
this.sizes = sizes;
this.athleteId = athleteId;
this.createdAt = createdAt;
this.createdAtLocal = createdAtLocal;
this.activityId = activityId;
this.activityName = activityName;
this.status = status;
this.tag = tag;
this.cursor = cursor;
this.type = MediaType.PHOTO;
}
}
public static final class Video extends Media {
private final Long activityId;
private final String activityName;
private final long athleteId;
private String caption;
private final String createdAt;
private final String createdAtLocal;
private final String cursor;
private final Float durationSeconds;
private final String id;
private final SortedMap<Integer, MediaDimension> sizes;
private final RemoteMediaStatus status;
private final String tag;
private final MediaType type;
private final SortedMap<Integer, String> urls;
private final String videoUrl;
@Override
public Long getActivityId() {
return activityId;
}
@Override
public String getActivityName() {
return activityName;
}
@Override
public long getAthleteId() {
return athleteId;
}
@Override
public String getCaption() {
return caption;
}
@Override
public String getCreatedAt() {
return createdAt;
}
@Override
public String getCreatedAtLocal() {
return createdAtLocal;
}
@Override
public String getCursor() {
return cursor;
}
public final Float getDurationSeconds() {
return durationSeconds;
}
@Override
public String getId() {
return id;
}
@Override
public SortedMap<Integer, MediaDimension> getSizes() {
return sizes;
}
@Override
public RemoteMediaStatus getStatus() {
return status;
}
@Override
public String getTag() {
return tag;
}
@Override
public MediaType getType() {
return type;
}
@Override
public SortedMap<Integer, String> getUrls() {
return urls;
}
public final String getVideoUrl() {
return videoUrl;
}
@Override
public void setCaption(String caption) {
this.caption = caption;
}
public Video(String id,
String caption,
SortedMap<Integer, String> urls,
SortedMap<Integer, MediaDimension> sizes,
long athleteId,
String createdAt,
String createdAtLocal,
Long activityId,
String activityName,
RemoteMediaStatus status,
String videoUrl,
Float durationSeconds,
String tag,
String cursor) {
this.id = id;
this.caption = caption;
this.urls = urls;
this.sizes = sizes;
this.athleteId = athleteId;
this.createdAt = createdAt;
this.createdAtLocal = createdAtLocal;
this.activityId = activityId;
this.activityName = activityName;
this.status = status;
this.videoUrl = videoUrl;
this.durationSeconds = durationSeconds;
this.tag = tag;
this.cursor = cursor;
this.type = MediaType.VIDEO;
}
}
public abstract Long getActivityId();
public abstract String getActivityName();
public abstract long getAthleteId();
public abstract String getCreatedAt();
public abstract String getCreatedAtLocal();
public abstract String getCursor();
@Override
public MediaDimension getLargestSize() {
return null;
}
@Override
public String getLargestUrl() {
return null;
}
@Override
public String getReferenceId() {
return null;
}
@Override
public String getSmallestUrl() {
return null;
}
public abstract String getTag();
private Media() {
}
}

View File

@@ -1,18 +1,14 @@
package app.revanced.extension.twitch;
import app.revanced.extension.shared.ResourceType;
public class Utils {
/* Called from SettingsPatch smali */
public static int getStringId(String name) {
return app.revanced.extension.shared.Utils.getResourceIdentifier(
ResourceType.STRING, name);
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "string");
}
/* Called from SettingsPatch smali */
public static int getDrawableId(String name) {
return app.revanced.extension.shared.Utils.getResourceIdentifier(
ResourceType.DRAWABLE, name);
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "drawable");
}
}

View File

@@ -4,21 +4,19 @@ import static app.revanced.extension.twitch.Utils.getStringId;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.twitch.settings.preference.TwitchPreferenceFragment;
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
import tv.twitch.android.settings.SettingsActivity;
import java.util.ArrayList;
import java.util.List;
/**
* Hooks AppCompatActivity to inject a custom {@link TwitchPreferenceFragment}.
*/
@@ -110,7 +108,7 @@ public class TwitchActivityHook {
base.getFragmentManager()
.beginTransaction()
.replace(Utils.getResourceIdentifier(ResourceType.ID, "fragment_container"), fragment)
.replace(Utils.getResourceIdentifier("fragment_container", "id"), fragment)
.commit();
return true;
}

View File

@@ -528,8 +528,14 @@ public final class AlternativeThumbnailsPatch {
* Cache used to verify if an alternative thumbnails exists for a given video id.
*/
@GuardedBy("itself")
private static final Map<String, VerifiedQualities> altVideoIdLookup =
Utils.createSizeRestrictedMap(1000);
private static final Map<String, VerifiedQualities> altVideoIdLookup = new LinkedHashMap<>(100) {
private static final int CACHE_LIMIT = 1000;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
};
private static VerifiedQualities getVerifiedQualities(@NonNull String videoId, boolean returnNullIfDoesNotExist) {
synchronized (altVideoIdLookup) {

View File

@@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
@@ -51,7 +50,7 @@ public class ChangeHeaderPatch {
return null;
}
final int identifier = Utils.getResourceIdentifier(ResourceType.ATTR, attributeName);
final int identifier = Utils.getResourceIdentifier(attributeName, "attr");
if (identifier == 0) {
// Should never happen.
Logger.printException(() -> "Could not find attribute: " + drawableName);
@@ -72,7 +71,7 @@ public class ChangeHeaderPatch {
? "_dark"
: "_light");
final int identifier = Utils.getResourceIdentifier(ResourceType.DRAWABLE, drawableFullName);
final int identifier = Utils.getResourceIdentifier(drawableFullName, "drawable");
if (identifier != 0) {
return Utils.getContext().getDrawable(identifier);
}

View File

@@ -21,7 +21,7 @@ public final class DownloadsPatch {
/**
* Injection point.
*/
public static void setMainActivity(Activity mainActivity) {
public static void activityCreated(Activity mainActivity) {
activityRef = new WeakReference<>(mainActivity);
}

View File

@@ -1,24 +0,0 @@
package app.revanced.extension.youtube.patches;
import java.util.Map;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public class FixContentProviderPatch {
/**
* Injection point.
*/
public static void removeNullMapEntries(Map<?, ?> map) {
map.entrySet().removeIf(entry -> {
Object value = entry.getValue();
if (value == null) {
Logger.printDebug(() -> "Removing content provider key with null value: " + entry.getKey());
return true;
}
return false;
});
}
}

View File

@@ -7,7 +7,6 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
@@ -30,15 +29,6 @@ public final class HidePlayerOverlayButtonsPatch {
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
}
/**
* Injection point.
*/
public static boolean getCastButtonOverrideV2(boolean original) {
if (Settings.HIDE_CAST_BUTTON.get()) return false;
return original;
}
/**
* Injection point.
*/
@@ -50,10 +40,10 @@ public final class HidePlayerOverlayButtonsPatch {
= Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS.get();
private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID = getResourceIdentifierOrThrow(
ResourceType.ID, "player_control_previous_button_touch_area");
"player_control_previous_button_touch_area", "id");
private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = getResourceIdentifierOrThrow(
ResourceType.ID, "player_control_next_button_touch_area");
"player_control_next_button_touch_area", "id");
/**
* Injection point.

View File

@@ -4,17 +4,7 @@ import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class HideSeekbarPatch {
/**
* Injection point.
*/
public static boolean hideSeekbar() {
return Settings.HIDE_SEEKBAR.get();
}
/**
* Injection point.
*/
public static boolean useFullscreenLargeSeekbar(boolean original) {
return Settings.FULLSCREEN_LARGE_SEEKBAR.get();
}
}

View File

@@ -15,7 +15,6 @@ import androidx.annotation.Nullable;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings;
@@ -116,7 +115,7 @@ public final class MiniplayerPatch {
* Resource is not present in older targets, and this field will be zero.
*/
private static final int MODERN_OVERLAY_SUBTITLE_TEXT
= Utils.getResourceIdentifier(ResourceType.ID, "modern_miniplayer_subtitle_text");
= Utils.getResourceIdentifier("modern_miniplayer_subtitle_text", "id");
private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
@@ -379,19 +378,6 @@ public final class MiniplayerPatch {
return original;
}
/**
* Injection point.
*/
public static boolean allowBoldIcons(boolean original) {
if (CURRENT_TYPE == MINIMAL) {
// Minimal player does not have the correct pause/play icon (it's too large).
// Use the non bold icons instead.
return false;
}
return original;
}
/**
* Injection point.
*/

View File

@@ -5,11 +5,12 @@ import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButt
import android.os.Build;
import android.view.View;
import android.widget.TextView;
import java.util.EnumMap;
import java.util.Map;
import android.widget.TextView;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
@@ -29,13 +30,13 @@ public final class NavigationButtonsPatch {
private static final boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON
= Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get();
private static final boolean DISABLE_TRANSLUCENT_STATUS_BAR
private static final Boolean DISABLE_TRANSLUCENT_STATUS_BAR
= Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get();
private static final boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT
private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT
= Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.get();
private static final boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
= Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get();
/**
@@ -61,13 +62,6 @@ public final class NavigationButtonsPatch {
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BUTTON_LABELS, navigationLabelsView);
}
/**
* Injection point.
*/
public static boolean useAnimatedNavigationButtons(boolean original) {
return Settings.NAVIGATION_BAR_ANIMATIONS.get();
}
/**
* Injection point.
*/

View File

@@ -20,6 +20,15 @@ public class OpenShortsInRegularPlayerPatch {
REGULAR_PLAYER_FULLSCREEN
}
static {
if (!VersionCheckPatch.IS_19_46_OR_GREATER
&& Settings.SHORTS_PLAYER_TYPE.get() == ShortsPlayerType.REGULAR_PLAYER_FULLSCREEN) {
// User imported newer settings to an older app target.
Logger.printInfo(() -> "Resetting " + Settings.SHORTS_PLAYER_TYPE);
Settings.SHORTS_PLAYER_TYPE.resetToDefault();
}
}
private static WeakReference<Activity> mainActivityRef = new WeakReference<>(null);
private static volatile boolean overrideBackPressToExit;

View File

@@ -24,20 +24,18 @@ public class OpenVideosFullscreenHookPatch {
/**
* Injection point.
*
* Returns negated value.
*/
public static boolean doNotOpenVideoFullscreenPortrait(boolean original) {
public static boolean openVideoFullscreenPortrait(boolean original) {
Boolean openFullscreen = openNextVideoFullscreen;
if (openFullscreen != null) {
openNextVideoFullscreen = null;
return !openFullscreen;
return openFullscreen;
}
if (!isFullScreenPatchIncluded()) {
return original;
}
return !Settings.OPEN_VIDEOS_FULLSCREEN_PORTRAIT.get();
return Settings.OPEN_VIDEOS_FULLSCREEN_PORTRAIT.get();
}
}

View File

@@ -0,0 +1,30 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class PauseOnAudioInterruptPatch {
private static final int AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = -3;
private static final int AUDIOFOCUS_LOSS_TRANSIENT = -2;
/**
* Injection point for AudioFocusRequest builder.
* Returns true if audio ducking should be disabled (willPauseWhenDucked = true).
*/
public static boolean shouldPauseOnAudioInterrupt() {
return Settings.PAUSE_ON_AUDIO_INTERRUPT.get();
}
/**
* Injection point for onAudioFocusChange callback.
* Converts AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK to AUDIOFOCUS_LOSS_TRANSIENT
* when the setting is enabled, causing YouTube to pause instead of ducking.
*/
public static int overrideAudioFocusChange(int focusChange) {
if (Settings.PAUSE_ON_AUDIO_INTERRUPT.get() && focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
return AUDIOFOCUS_LOSS_TRANSIENT;
}
return focusChange;
}
}

View File

@@ -42,7 +42,7 @@ public class PlayerControlsPatch {
Logger.printDebug(() -> "fullscreen button visibility: "
+ (visibility == View.VISIBLE ? "VISIBLE" :
visibility == View.GONE ? "GONE" : "INVISIBLE"));
visibility == View.GONE ? "GONE" : "INVISIBLE"));
fullscreenButtonVisibilityChanged(visibility == View.VISIBLE);
}

View File

@@ -1,29 +1,19 @@
package app.revanced.extension.youtube.patches;
import android.app.AlertDialog;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
/** @noinspection unused*/
public class RemoveViewerDiscretionDialogPatch {
/**
* Injection point.
*/
public static void confirmDialog(AlertDialog dialog) {
if (Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) {
Logger.printDebug(() -> "Clicking alert dialog dismiss button");
final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setSoundEffectsEnabled(false);
button.performClick();
if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) {
// Since the patch replaces the AlertDialog#show() method, we need to call the original method here.
dialog.show();
return;
}
// Since the patch replaces the AlertDialog#show() method, we need to call the original method here.
Logger.printDebug(() -> "Showing alert dialog");
dialog.show();
final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
button.setSoundEffectsEnabled(false);
button.performClick();
}
}

View File

@@ -13,11 +13,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import java.util.Set;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilter;
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.youtube.settings.Settings;
@@ -133,10 +131,6 @@ public class ReturnYouTubeDislikePatch {
String conversionContextString = conversionContext.toString();
if (Settings.RYD_ENABLED.get()) { // FIXME: Remove this.
Logger.printDebug(() -> "RYD conversion context: " + conversionContext);
}
if (isRollingNumber && !conversionContextString.contains("video_action_bar.e")) {
return original;
}

View File

@@ -2,6 +2,8 @@ package app.revanced.extension.youtube.patches;
import android.app.Activity;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.Objects;
@@ -76,7 +78,7 @@ public class ShortsAutoplayPatch {
/**
* Injection point.
*/
public static Enum<?> changeShortsRepeatBehavior(Enum<?> original) {
public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) {
try {
final boolean autoplay;
@@ -93,19 +95,19 @@ public class ShortsAutoplayPatch {
autoplay = Settings.SHORTS_AUTOPLAY.get();
}
Enum<?> overrideBehavior = (autoplay
final ShortsLoopBehavior behavior = autoplay
? ShortsLoopBehavior.SINGLE_PLAY
: ShortsLoopBehavior.REPEAT).ytEnumValue;
: ShortsLoopBehavior.REPEAT;
if (overrideBehavior != null) {
if (behavior.ytEnumValue != null) {
Logger.printDebug(() -> {
String name = (original == null ? "unknown (null)" : original.name());
return overrideBehavior == original
return behavior == original
? "Behavior setting is same as original. Using original: " + name
: "Changing Shorts repeat behavior from: " + name + " to: " + overrideBehavior.name();
: "Changing Shorts repeat behavior from: " + name + " to: " + behavior.name();
});
return overrideBehavior;
return behavior.ytEnumValue;
}
if (original == null) {
@@ -116,12 +118,13 @@ public class ShortsAutoplayPatch {
return unknown;
}
} catch (Exception ex) {
Logger.printException(() -> "changeShortsRepeatState failure", ex);
Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
}
return original;
}
/**
* Injection point.
*/

View File

@@ -19,12 +19,5 @@ public class VersionCheckPatch {
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
@Deprecated
public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
public static final boolean IS_20_21_OR_GREATER = isVersionOrGreater("20.21.00");
public static final boolean IS_20_22_OR_GREATER = isVersionOrGreater("20.22.00");
public static final boolean IS_20_31_OR_GREATER = isVersionOrGreater("20.31.00");
public static final boolean IS_20_37_OR_GREATER = isVersionOrGreater("20.37.00");
public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00");
}

View File

@@ -11,6 +11,9 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
@@ -153,8 +156,8 @@ public final class AdsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == playerShoppingShelf) {
return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered();
}

View File

@@ -1,5 +1,7 @@
package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
import app.revanced.extension.youtube.settings.Settings;
@@ -19,7 +21,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
isVideoQualityMenuVisible = true;

View File

@@ -1,10 +1,13 @@
package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
final class ButtonsFilter extends Filter {
public final class ButtonsFilter extends Filter {
private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.e";
private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.e";
private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.e";
@@ -39,6 +42,7 @@ final class ButtonsFilter extends Filter {
addPathCallbacks(
likeSubscribeGlow,
bufferFilterPathGroup,
new StringFilterGroup(
Settings.HIDE_LIKE_DISLIKE_BUTTON,
"|segmented_like_dislike_button"
@@ -57,12 +61,6 @@ final class ButtonsFilter extends Filter {
)
);
// FIXME: 20.22+ filtering of the action buttons doesn't work because
// the buffer is the same for all buttons.
if (!VersionCheckPatch.IS_20_22_OR_GREATER) {
addPathCallbacks(bufferFilterPathGroup);
}
bufferButtonsGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_REPORT_BUTTON,
@@ -114,19 +112,17 @@ final class ButtonsFilter extends Filter {
}
private boolean isEveryFilterGroupEnabled() {
for (var group : pathCallbacks) {
for (var group : pathCallbacks)
if (!group.isEnabled()) return false;
}
for (var group : bufferButtonsGroupList) {
for (var group : bufferButtonsGroupList)
if (!group.isEnabled()) return false;
}
return true;
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == likeSubscribeGlow) {
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))

View File

@@ -1,10 +1,12 @@
package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
final class CommentsFilter extends Filter {
public final class CommentsFilter extends Filter {
private static final String COMMENT_COMPOSER_PATH = "comment_composer.e";
@@ -88,8 +90,8 @@ final class CommentsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == chipBar) {
// Playlist sort button uses same components and must only filter if the player is opened.
return PlayerType.getCurrent().isMaximizedOrFullscreen()

View File

@@ -1,11 +1,14 @@
package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
final class DescriptionComponentsFilter extends Filter {
public final class DescriptionComponentsFilter extends Filter {
private static final String INFOCARDS_SECTION_PATH = "infocards_section.e";
@@ -128,8 +131,8 @@ final class DescriptionComponentsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == aiGeneratedVideoSummarySection || matchedGroup == hypePoints) {
// Only hide if player is open, in case this component is used somewhere else.

View File

@@ -1,214 +0,0 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.ByteTrieSearch;
abstract class FilterGroup<T> {
final static class FilterGroupResult {
private BooleanSetting setting;
private int matchedIndex;
private int matchedLength;
// In the future it might be useful to include which pattern matched,
// but for now that is not needed.
FilterGroupResult() {
this(null, -1, 0);
}
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
setValues(setting, matchedIndex, matchedLength);
}
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
this.setting = setting;
this.matchedIndex = matchedIndex;
this.matchedLength = matchedLength;
}
/**
* A null value if the group has no setting,
* or if no match is returned from {@link FilterGroupList#check(Object)}.
*/
public BooleanSetting getSetting() {
return setting;
}
public boolean isFiltered() {
return matchedIndex >= 0;
}
/**
* Matched index of first pattern that matched, or -1 if nothing matched.
*/
public int getMatchedIndex() {
return matchedIndex;
}
/**
* Length of the matched filter pattern.
*/
public int getMatchedLength() {
return matchedLength;
}
}
protected final BooleanSetting setting;
protected final T[] filters;
/**
* Initialize a new filter group.
*
* @param setting The associated setting.
* @param filters The filters.
*/
@SafeVarargs
public FilterGroup(final BooleanSetting setting, final T... filters) {
this.setting = setting;
this.filters = filters;
if (filters.length == 0) {
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
}
}
public boolean isEnabled() {
return setting == null || setting.get();
}
/**
* @return If {@link FilterGroupList} should include this group when searching.
* By default, all filters are included except non enabled settings that require reboot.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean includeInSearch() {
return isEnabled() || !setting.rebootApp;
}
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
}
public abstract FilterGroupResult check(final T stack);
}
class StringFilterGroup extends FilterGroup<String> {
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
super(setting, filters);
}
@Override
public FilterGroupResult check(final String string) {
int matchedIndex = -1;
int matchedLength = 0;
if (isEnabled()) {
for (String pattern : filters) {
if (!string.isEmpty()) {
final int indexOf = string.indexOf(pattern);
if (indexOf >= 0) {
matchedIndex = indexOf;
matchedLength = pattern.length();
break;
}
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}
/**
* If you have more than 1 filter patterns, then all instances of
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
* which uses a prefix tree to give better performance.
*/
class ByteArrayFilterGroup extends FilterGroup<byte[]> {
private volatile int[][] failurePatterns;
// Modified implementation from https://stackoverflow.com/a/1507813
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
// Finds the first occurrence of the pattern in the byte array using
// KMP matching algorithm.
int patternLength = pattern.length;
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
while (j > 0 && pattern[j] != data[i]) {
j = failure[j - 1];
}
if (pattern[j] == data[i]) {
j++;
}
if (j == patternLength) {
return i - patternLength + 1;
}
}
return -1;
}
private static int[] createFailurePattern(byte[] pattern) {
// Computes the failure function using a boot-strapping process,
// where the pattern is matched against itself.
final int patternLength = pattern.length;
final int[] failure = new int[patternLength];
for (int i = 1, j = 0; i < patternLength; i++) {
while (j > 0 && pattern[j] != pattern[i]) {
j = failure[j - 1];
}
if (pattern[j] == pattern[i]) {
j++;
}
failure[i] = j;
}
return failure;
}
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
super(setting, filters);
}
/**
* Converts the Strings into byte arrays. Used to search for text in binary data.
*/
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
}
private synchronized void buildFailurePatterns() {
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
Logger.printDebug(() -> "Building failure array for: " + this);
int[][] failurePatterns = new int[filters.length][];
int i = 0;
for (byte[] pattern : filters) {
failurePatterns[i++] = createFailurePattern(pattern);
}
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
}
@Override
public FilterGroupResult check(final byte[] bytes) {
int matchedLength = 0;
int matchedIndex = -1;
if (isEnabled()) {
int[][] failures = failurePatterns;
if (failures == null) {
buildFailurePatterns(); // Lazy load.
failures = failurePatterns;
}
for (int i = 0, length = filters.length; i < length; i++) {
byte[] filter = filters[i];
matchedIndex = indexOf(bytes, filter, failures[i]);
if (matchedIndex >= 0) {
matchedLength = filter.length;
break;
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}

View File

@@ -1,6 +1,8 @@
package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
@SuppressWarnings("unused")
public final class HideInfoCardsFilter extends Filter {

View File

@@ -17,6 +17,8 @@ import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.TrieSearch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -41,7 +43,7 @@ import app.revanced.extension.youtube.shared.PlayerType;
* - When using whole word syntax, some keywords may need additional pluralized variations.
*/
@SuppressWarnings("unused")
final class KeywordContentFilter extends Filter {
public final class KeywordContentFilter extends Filter {
/**
* Strings found in the buffer for every videos. Full strings should be specified.
@@ -554,8 +556,8 @@ final class KeywordContentFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
return false;
}

View File

@@ -1,6 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import static app.revanced.extension.youtube.patches.VersionCheckPatch.IS_20_21_OR_GREATER;
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
import android.graphics.drawable.Drawable;
@@ -15,6 +14,9 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.StringTrieSearch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
import app.revanced.extension.youtube.patches.ChangeHeaderPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
@@ -343,7 +345,7 @@ public final class LayoutComponentsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// This identifier is used not only in players but also in search results:
// https://github.com/ReVanced/revanced-patches/issues/3245
@@ -400,22 +402,20 @@ public final class LayoutComponentsFilter extends Filter {
* Injection point.
* Called from a different place then the other filters.
*/
public static boolean filterMixPlaylists(Object conversionContext, @Nullable byte[] buffer) {
// Edit: This hook may no longer be needed, and mix playlist filtering
// might be possible using the existing litho filters.
public static boolean filterMixPlaylists(Object conversionContext, @Nullable final byte[] bytes) {
try {
if (!Settings.HIDE_MIX_PLAYLISTS.get()) {
return false;
}
if (buffer == null) {
Logger.printDebug(() -> "buffer is null");
if (bytes == null) {
Logger.printDebug(() -> "bytes is null");
return false;
}
if (mixPlaylists.check(buffer).isFiltered()
if (mixPlaylists.check(bytes).isFiltered()
// Prevent hiding the description of some videos accidentally.
&& !mixPlaylistsBufferExceptions.check(buffer).isFiltered()
&& !mixPlaylistsBufferExceptions.check(bytes).isFiltered()
// Prevent playlist items being hidden, if a mix playlist is present in it.
// Check last since it requires creating a context string.
//
@@ -478,23 +478,11 @@ public final class LayoutComponentsFilter extends Filter {
: height;
}
private static final boolean HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS_ENABLED
= Settings.HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS.get();
/**
* Injection point.
*/
public static void hideInRelatedVideos(View chipView) {
// Cannot use 0dp hide with later targets, otherwise the suggested videos
// can be shown in full screen mode.
// This behavior may also be present in earlier app targets.
if (IS_20_21_OR_GREATER) {
// FIXME: The filter bar is still briefly shown when dragging the suggested videos
// below the video player.
Utils.hideViewUnderCondition(HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS_ENABLED, chipView);
} else {
Utils.hideViewBy0dpUnderCondition(HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS_ENABLED, chipView);
}
Utils.hideViewBy0dpUnderCondition(Settings.HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS, chipView);
}
private static final boolean HIDE_DOODLES_ENABLED = Settings.HIDE_DOODLES.get();
@@ -519,9 +507,7 @@ public final class LayoutComponentsFilter extends Filter {
&& NavigationBar.isSearchBarActive()
// Search bar can be active but behind the player.
&& !PlayerType.getCurrent().isMaximizedOrFullscreen()) {
// FIXME: "Show more" button is visible hidden,
// but an empty space remains that can be clicked.
Utils.hideViewBy0dp(view);
Utils.hideViewByLayoutParams(view);
}
}

View File

@@ -1,5 +1,7 @@
package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings;
@@ -36,7 +38,7 @@ public final class PlaybackSpeedMenuFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == oldPlaybackMenuGroup) {
isOldPlaybackSpeedMenuVisible = true;

View File

@@ -3,13 +3,16 @@ package app.revanced.extension.youtube.patches.components;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import java.util.List;
@SuppressWarnings("unused")
public class PlayerFlyoutMenuItemsFilter extends Filter {
public final class PlayerFlyoutMenuItemsFilter extends Filter {
public static final class HideAudioFlyoutMenuAvailability implements Setting.Availability {
@Override
@@ -94,7 +97,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == videoQualityMenuFooter) {
return true;

View File

@@ -4,15 +4,18 @@ import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.TrieSearch;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.TrieSearch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
/**
* Searches for video id's in the proto buffer of Shorts dislike.
@@ -33,7 +36,18 @@ public final class ReturnYouTubeDislikeFilter extends Filter {
* Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry().
*/
@GuardedBy("itself")
private static final Map<String, Boolean> lastVideoIds = Utils.createSizeRestrictedMap(5);
private static final Map<String, Boolean> lastVideoIds = new LinkedHashMap<>() {
/**
* Number of video id's to keep track of for searching thru the buffer.
* A minimum value of 3 should be sufficient, but check a few more just in case.
*/
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
}
};
/**
* Injection point.
@@ -73,13 +87,13 @@ public final class ReturnYouTubeDislikeFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
return false;
}
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(buffer);
FilterGroupResult result = videoIdFilterGroup.check(buffer);
if (result.isFiltered()) {
String matchedVideoId = findVideoId(buffer);
// Matched video will be null if in incognito mode.

View File

@@ -11,7 +11,9 @@ import java.util.Arrays;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.shared.patches.litho.Filter;
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
import app.revanced.extension.shared.patches.litho.FilterGroupList.*;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -224,11 +226,7 @@ public final class ShortsFilter extends Filter {
videoActionButton = new StringFilterGroup(
null,
// Can be any of:
// button.eml
// shorts_video_action_button.eml
// reel_action_button.eml
// reel_pivot_button.eml
// Can be simply 'button.e', 'shorts_video_action_button.e' or 'reel_action_button.e'
"button.e"
);
@@ -239,37 +237,31 @@ public final class ShortsFilter extends Filter {
addPathCallbacks(
shortsCompactFeedVideo, joinButton, subscribeButton, paidPromotionLabel, autoDubbedLabel,
suggestedAction, pausedOverlayButtons, channelBar, previewComment,
shortsActionBar, suggestedAction, pausedOverlayButtons, channelBar, previewComment,
fullVideoLinkLabel, videoTitle, useSoundButton, reelSoundMetadata, soundButton, infoPanel,
stickers, likeFountain, likeButton, dislikeButton, livePreview
);
// FIXME: The Shorts buffer is very different with 20.22+ and if any of these filters
// are enabled then all Shorts player vertical buttons are hidden.
if (!VersionCheckPatch.IS_20_22_OR_GREATER) {
addPathCallbacks(shortsActionBar);
//
// All other action buttons.
//
videoActionButtonBuffer.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
"reel_comment_button",
"youtube_shorts_comment_outline"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHARE_BUTTON,
"reel_share_button",
"youtube_shorts_share_outline"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_REMIX_BUTTON,
"reel_remix_button",
"youtube_shorts_remix_outline"
)
);
}
//
// All other action buttons.
//
videoActionButtonBuffer.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
"reel_comment_button",
"youtube_shorts_comment_outline"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHARE_BUTTON,
"reel_share_button",
"youtube_shorts_share_outline"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_REMIX_BUTTON,
"reel_remix_button",
"youtube_shorts_remix_outline"
)
);
//
// Suggested actions.
@@ -350,7 +342,7 @@ public final class ShortsFilter extends Filter {
}
@Override
boolean isFiltered(String identifier, String path, byte[] buffer,
public boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentType == FilterContentType.PATH) {
if (matchedGroup == subscribeButton || matchedGroup == joinButton

View File

@@ -21,6 +21,7 @@ public class RememberVideoQualityPatch {
private static final IntegerSetting shortsQualityWifi = Settings.SHORTS_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting shortsQualityMobile = Settings.SHORTS_QUALITY_DEFAULT_MOBILE;
public static boolean shouldRememberVideoQuality() {
BooleanSetting preference = ShortsPlayerState.isOpen()
? Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED

View File

@@ -1,85 +0,0 @@
package app.revanced.extension.youtube.patches.theme;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
/**
* Dynamic drawable that is either the regular or bolded ReVanced preference icon.
*
* This is needed because the YouTube ReVanced preference intent is an AndroidX preference,
* and AndroidX classes are not built into Android which makes programmatically changing
* the preference thru patching overly complex. This solves the problem by using a drawable
* wrapper to dynamically pick which icon drawable to use at runtime.
*/
@SuppressWarnings("unused")
public class ReVancedSettingsIconDynamicDrawable extends Drawable {
private final Drawable icon;
public ReVancedSettingsIconDynamicDrawable() {
final int resId = Utils.getResourceIdentifier(ResourceType.DRAWABLE,
Utils.appIsUsingBoldIcons()
? "revanced_settings_icon_bold"
: "revanced_settings_icon"
);
icon = Utils.getContext().getDrawable(resId);
}
@Override
public void draw(@NonNull Canvas canvas) {
icon.draw(canvas);
}
@Override
public void setAlpha(int alpha) {
icon.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
icon.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return icon.getOpacity();
}
@Override
public int getIntrinsicWidth() {
return icon.getIntrinsicWidth();
}
@Override
public int getIntrinsicHeight() {
return icon.getIntrinsicHeight();
}
@Override
public void setBounds(int left, int top, int right, int bottom) {
super.setBounds(left, top, right, bottom);
icon.setBounds(left, top, right, bottom);
}
@Override
public void setBounds(@NonNull Rect bounds) {
super.setBounds(bounds);
icon.setBounds(bounds);
}
@Override
public void onBoundsChange(@NonNull Rect bounds) {
super.onBoundsChange(bounds);
icon.setBounds(bounds);
}
}

View File

@@ -16,7 +16,6 @@ import java.util.Arrays;
import java.util.Scanner;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.settings.Settings;
@@ -102,6 +101,16 @@ public final class SeekbarColorPatch {
return customSeekbarColor;
}
/**
* injection point.
*/
public static boolean useLotteLaunchSplashScreen(boolean original) {
// This method is only used for development purposes to force the old style launch screen.
// Forcing this off on some devices can cause unexplained startup crashes,
// where the lottie animation is still used even though this condition appears to bypass it.
return original; // false = drawable style, true = lottie style.
}
/**
* Injection point.
* Modern Lottie style animation.

View File

@@ -260,8 +260,7 @@ public class ReturnYouTubeDislike {
// middle separator
String middleSeparatorString = compactLayout
? " " + MIDDLE_SEPARATOR_CHARACTER + " "
: " \u2009\u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009\u2009 "; // u2009 = 'narrow space'
: " \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character
final int shapeInsertionIndex = middleSeparatorString.length() / 2;
Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString);
ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
@@ -556,8 +555,7 @@ public class ReturnYouTubeDislike {
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null
&& spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
Logger.printDebug(() -> "Replacing span: " + original + " with " +
"previously created dislike span of data: " + videoId);
Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
return replacementLikeDislikeSpan;
}

View File

@@ -32,6 +32,7 @@ import android.graphics.Color;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
@@ -45,11 +46,11 @@ import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.DeArrow
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption;
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.patches.MiniplayerPatch;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
public class Settings extends BaseSettings {
public class Settings extends YouTubeAndMusicSettings {
// Video
public static final BooleanSetting ADVANCED_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_advanced_video_quality_menu", TRUE);
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE);
@@ -187,7 +188,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, new MiniplayerAnyModernAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_OVERLAY_BUTTONS = new BooleanSetting("revanced_miniplayer_hide_overlay_buttons", FALSE, true, new MiniplayerHideOverlayButtonsAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, new MiniplayerHideSubtextsAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, new MiniplayerHideRewindOrOverlayOpacityAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, new MiniplayerPatch.MiniplayerHideRewindOrOverlayOpacityAvailability());
public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, new MiniplayerAnyModernAvailability());
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, new MiniplayerHideRewindOrOverlayOpacityAvailability());
@@ -274,11 +275,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting CHANGE_START_PAGE_ALWAYS = new BooleanSetting("revanced_change_start_page_always", FALSE, true,
new ChangeStartPageTypeAvailability());
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "19.01.34", true, parent(SPOOF_APP_VERSION));
// Custom filter
public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE);
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
// Navigation buttons
public static final BooleanSetting HIDE_HOME_BUTTON = new BooleanSetting("revanced_hide_home_button", FALSE, true);
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", TRUE, true);
@@ -288,7 +284,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_hide_notifications_button", FALSE, true);
public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true,
"revanced_switch_create_with_notifications_button_user_dialog_message");
public static final BooleanSetting NAVIGATION_BAR_ANIMATIONS = new BooleanSetting("revanced_navigation_bar_animations", FALSE);
public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", FALSE, true,
"revanced_disable_translucent_status_bar_user_dialog_message");
public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT = new BooleanSetting("revanced_disable_translucent_navigation_bar_light", FALSE, true);
@@ -342,7 +337,6 @@ public class Settings extends BaseSettings {
public static final BooleanSetting DISABLE_PRECISE_SEEKING_GESTURE = new BooleanSetting("revanced_disable_precise_seeking_gesture", FALSE);
public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true);
public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE, true);
public static final BooleanSetting FULLSCREEN_LARGE_SEEKBAR = new BooleanSetting("revanced_fullscreen_large_seekbar", FALSE);
public static final BooleanSetting HIDE_TIMESTAMP = new BooleanSetting("revanced_hide_timestamp", FALSE);
public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE);
public static final BooleanSetting SEEKBAR_TAPPING = new BooleanSetting("revanced_seekbar_tapping", FALSE);
@@ -358,6 +352,7 @@ public class Settings extends BaseSettings {
public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1, false, false);
public static final BooleanSetting LOOP_VIDEO = new BooleanSetting("revanced_loop_video", FALSE);
public static final BooleanSetting LOOP_VIDEO_BUTTON = new BooleanSetting("revanced_loop_video_button", FALSE);
public static final BooleanSetting PAUSE_ON_AUDIO_INTERRUPT = new BooleanSetting("revanced_pause_on_audio_interrupt", FALSE, true);
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE);
@@ -369,8 +364,6 @@ public class Settings extends BaseSettings {
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting SPOOF_VIDEO_STREAMS_AV1 = new BooleanSetting("revanced_spoof_video_streams_av1", FALSE, true,
"revanced_spoof_video_streams_av1_user_dialog_message", new SpoofClientAv1Availability());
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
// Swipe controls
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
@@ -383,7 +376,7 @@ public class Settings extends BaseSettings {
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_threshold", 30, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final IntegerSetting SWIPE_VOLUME_SENSITIVITY = new IntegerSetting("revanced_swipe_volume_sensitivity", 1, true, parent(SWIPE_VOLUME));
public static final EnumSetting<SwipeOverlayStyle> SWIPE_OVERLAY_STYLE = new EnumSetting<>("revanced_swipe_overlay_style", SwipeOverlayStyle.HORIZONTAL,true,
public static final EnumSetting<SwipeOverlayStyle> SWIPE_OVERLAY_STYLE = new EnumSetting<>("revanced_swipe_overlay_style", SwipeOverlayStyle.HORIZONTAL, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_text_overlay_size", 14, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
@@ -412,7 +405,9 @@ public class Settings extends BaseSettings {
// SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
/**
* Do not use id setting directly. Instead use {@link SponsorBlockSettings}.
*/
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "", parent(SB_ENABLED));
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
@@ -461,7 +456,7 @@ public class Settings extends BaseSettings {
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFFFF", false, false);
// Deprecated migrations
private static final StringSetting DEPRECATED_SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_value", "#FF0033");
private static final StringSetting DEPRECATED_SEEKBAR_CUSTOM_COLOR_PRIMARY = new StringSetting("revanced_seekbar_custom_color_value", "#FF0033");
private static final FloatSetting DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f, false, false);
private static final FloatSetting DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f, false, false);
@@ -477,13 +472,6 @@ public class Settings extends BaseSettings {
static {
// region Migration
// 20.37+ YT removed parts of the code for the legacy tablet miniplayer.
// This check must remain until the Tablet type is eventually removed.
if (VersionCheckPatch.IS_20_37_OR_GREATER && MINIPLAYER_TYPE.get() == MiniplayerType.TABLET) {
Logger.printInfo(() -> "Resetting miniplayer tablet type");
MINIPLAYER_TYPE.resetToDefault();
}
// Migrate renamed change header enums.
if (HEADER_LOGO.get() == HeaderLogo.REVANCED) {
HEADER_LOGO.save(HeaderLogo.ROUNDED);
@@ -520,20 +508,12 @@ public class Settings extends BaseSettings {
// or is spoofing to a version the same or newer than this app.
if (!SPOOF_APP_VERSION_TARGET.isSetToDefault() &&
(SPOOF_APP_VERSION_TARGET.get().compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0
|| (Utils.getAppVersionName().compareTo(SPOOF_APP_VERSION_TARGET.get()) <= 0))) {
|| (Utils.getAppVersionName().compareTo(SPOOF_APP_VERSION_TARGET.get()) <= 0))) {
Logger.printInfo(() -> "Resetting spoof app version");
SPOOF_APP_VERSION_TARGET.resetToDefault();
SPOOF_APP_VERSION.resetToDefault();
}
if (!BaseSettings.SETTINGS_DISABLE_BOLD_ICONS.get() && SPOOF_APP_VERSION.get()
&& SPOOF_APP_VERSION_TARGET.get().compareTo("19.35.00") <= 0) {
Logger.printInfo(() -> "Temporarily disabling bold icons that don't work with old spoof targets");
// Don't save and only temporarily overwrite the value so
// if spoofing is turned off the old setting value is used.
BooleanSetting.privateSetValue(BaseSettings.SETTINGS_DISABLE_BOLD_ICONS, false);
}
// VR 1.61 is not selectable in the settings, and it's selected by spoof stream patch if needed.
if (SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.ANDROID_VR_1_61_48) {
SPOOF_VIDEO_STREAMS_CLIENT_TYPE.resetToDefault();

View File

@@ -7,7 +7,6 @@ import android.preference.PreferenceFragment;
import android.view.View;
import android.widget.Toolbar;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseActivityHook;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
@@ -16,28 +15,11 @@ import app.revanced.extension.youtube.settings.preference.YouTubePreferenceFragm
import app.revanced.extension.youtube.settings.search.YouTubeSearchViewController;
/**
* Hooks LicenseActivity to inject a custom {@link YouTubePreferenceFragment}
* with a toolbar and search functionality.
* Hooks LicenseActivity to inject a custom {@link YouTubePreferenceFragment} with a toolbar and search functionality.
*/
@SuppressWarnings("deprecation")
public class YouTubeActivityHook extends BaseActivityHook {
/**
* How much time has passed since the first launch of the app. Simple check to prevent
* forcing bold icons on first launch where the settings menu is partially broken
* due to missing icon resources the client has not yet received.
*/
private static final long MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS = 30 * 1000; // 30 seconds.
private static final boolean USE_BOLD_ICONS = VersionCheckPatch.IS_20_31_OR_GREATER
&& !Settings.SETTINGS_DISABLE_BOLD_ICONS.get()
&& (System.currentTimeMillis() - Settings.FIRST_TIME_APP_LAUNCHED.get())
> MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS;
static {
Utils.setAppIsUsingBoldIcons(USE_BOLD_ICONS);
}
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
/**
@@ -62,7 +44,15 @@ public class YouTubeActivityHook extends BaseActivityHook {
final var theme = Utils.isDarkModeEnabled()
? "Theme.YouTube.Settings.Dark"
: "Theme.YouTube.Settings";
activity.setTheme(Utils.getResourceIdentifierOrThrow(ResourceType.STYLE, theme));
activity.setTheme(Utils.getResourceIdentifierOrThrow(theme, "style"));
}
/**
* Returns the resource ID for the YouTube settings layout.
*/
@Override
protected int getContentViewResourceId() {
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
}
/**
@@ -165,12 +155,4 @@ public class YouTubeActivityHook extends BaseActivityHook {
public static boolean handleBackPress() {
return YouTubeSearchViewController.handleFinish(searchViewController);
}
/**
* Injection point.
*/
@SuppressWarnings("unused")
public static boolean useBoldIcons(boolean original) {
return USE_BOLD_ICONS;
}
}

View File

@@ -19,10 +19,8 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.patches.VersionCheckPatch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
@@ -74,7 +72,7 @@ public final class NavigationBar {
*/
public static boolean isSearchBarActive() {
View searchbarResults = searchBarResultsRef.get();
return searchbarResults != null && searchbarResults.isShown();
return searchbarResults != null && searchbarResults.getParent() != null;
}
public static boolean isBackButtonVisible() {
@@ -279,14 +277,12 @@ public final class NavigationBar {
}
/**
* Custom cairo notification filled icon to fix unpatched app missing resource.
* Use the bundled non cairo filled icon instead of a custom icon.
* Use the old non cairo filled icon, which is almost identical to
* the what would be the filled cairo icon.
*/
private static final int fillBellCairoBlack = Utils.getResourceIdentifier(ResourceType.DRAWABLE,
// The bold cairo notification filled icon is present,
// but YT still has not fixed the icon not associated to the enum.
VersionCheckPatch.IS_20_31_OR_GREATER && !Settings.SETTINGS_DISABLE_BOLD_ICONS.get()
? "yt_fill_experimental_bell_vd_theme_24"
: "revanced_fill_bell_cairo_black_24");
private static final int fillBellCairoBlack = Utils.getResourceIdentifier(
"yt_fill_bell_black_24", "drawable");
/**
* Injection point.
@@ -294,12 +290,13 @@ public final class NavigationBar {
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static void setCairoNotificationFilledIcon(EnumMap enumMap, Enum tabActivityCairo) {
// Show a popup informing this fix is no longer needed to those who might care.
if (BaseSettings.DEBUG.get() && enumMap.containsKey(tabActivityCairo)) {
Logger.printException(() -> "YouTube fixed the notification icons");
if (fillBellCairoBlack != 0) {
// Show a popup informing this fix is no longer needed to those who might care.
if (BaseSettings.DEBUG.get() && enumMap.containsKey(tabActivityCairo)) {
Logger.printException(() -> "YouTube fixed the cairo notification icons");
}
enumMap.putIfAbsent(tabActivityCairo, fillBellCairoBlack);
}
enumMap.putIfAbsent(tabActivityCairo, fillBellCairoBlack);
}
public enum NavigationButton {

View File

@@ -3,7 +3,6 @@ package app.revanced.extension.youtube.shared
import android.app.Activity
import android.view.View
import android.view.ViewGroup
import app.revanced.extension.shared.ResourceType
import app.revanced.extension.shared.Utils
import java.lang.ref.WeakReference
@@ -20,13 +19,13 @@ class PlayerControlsVisibilityObserverImpl(
* id of the direct parent of controls_layout, R.id.youtube_controls_overlay
*/
private val controlsLayoutParentId =
Utils.getResourceIdentifier(activity, ResourceType.ID, "youtube_controls_overlay")
Utils.getResourceIdentifier(activity, "youtube_controls_overlay", "id")
/**
* id of R.id.controls_layout
*/
private val controlsLayoutId =
Utils.getResourceIdentifier(activity, ResourceType.ID, "controls_layout")
Utils.getResourceIdentifier(activity, "controls_layout", "id")
/**
* reference to the controls layout view

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