chore: Merge branch dev to main (#6591)
This commit is contained in:
commit
938c6fae60
1057 changed files with 112182 additions and 97443 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
[*.{kt,kts}]
|
|
||||||
ktlint_code_style = intellij_idea
|
|
||||||
ktlint_standard_no-wildcard-imports = disabled
|
|
||||||
7
.github/workflows/build_pull_request.yml
vendored
7
.github/workflows/build_pull_request.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.pr && format('refs/pull/{0}/merge', inputs.pr) || github.ref }}
|
ref: ${{ inputs.pr && format('refs/pull/{0}/merge', inputs.pr) || github.ref }}
|
||||||
|
|
||||||
|
|
@ -34,12 +34,13 @@ jobs:
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
|
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew :patches:buildAndroid --no-daemon
|
run: ./gradlew :patches:buildAndroid --no-daemon
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: revanced-patches
|
name: revanced-patches
|
||||||
path: patches/build/libs
|
path: patches/build/libs
|
||||||
|
archive: false
|
||||||
|
|
|
||||||
2
.github/workflows/open_pull_request.yml
vendored
2
.github/workflows/open_pull_request.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
|
|
|
||||||
4
.github/workflows/pull_strings.yml
vendored
4
.github/workflows/pull_strings.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
@ -32,7 +32,7 @@ jobs:
|
||||||
|
|
||||||
- name: Process strings
|
- name: Process strings
|
||||||
run: |
|
run: |
|
||||||
gradlew processStringsFromCrowdin
|
./gradlew processStringsFromCrowdin
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
|
||||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
2
.github/workflows/push_strings.yml
vendored
2
.github/workflows/push_strings.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Process strings
|
- name: Process strings
|
||||||
env:
|
env:
|
||||||
|
|
|
||||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
|
|
@ -15,10 +15,11 @@ jobs:
|
||||||
packages: write
|
packages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
attestations: write
|
attestations: write
|
||||||
|
artifact-metadata: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v5
|
uses: actions/setup-java@v5
|
||||||
|
|
@ -61,7 +62,7 @@ jobs:
|
||||||
|
|
||||||
- name: Attest
|
- name: Attest
|
||||||
if: steps.release.outputs.new_release_published == 'true'
|
if: steps.release.outputs.new_release_published == 'true'
|
||||||
uses: actions/attest-build-provenance@v3
|
uses: actions/attest@v4
|
||||||
with:
|
with:
|
||||||
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
||||||
subject-path: patches/build/libs/patches-*.rvp
|
subject-path: patches/build/libs/patches-*.rvp
|
||||||
|
|
|
||||||
4
.github/workflows/update-gradle-wrapper.yml
vendored
4
.github/workflows/update-gradle-wrapper.yml
vendored
|
|
@ -10,9 +10,9 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Update Gradle Wrapper
|
- name: Update Gradle Wrapper
|
||||||
uses: gradle-update/update-gradle-wrapper-action@v1
|
uses: gradle-update/update-gradle-wrapper-action@v2
|
||||||
with:
|
with:
|
||||||
target-branch: dev
|
target-branch: dev
|
||||||
|
|
|
||||||
314
CHANGELOG.md
314
CHANGELOG.md
|
|
@ -1,3 +1,317 @@
|
||||||
|
# [6.0.0-dev.26](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.25...v6.0.0-dev.26) (2026-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Add minSdk to all extension projects ([#6778](https://github.com/ReVanced/revanced-patches/issues/6778)) ([7517f57](https://github.com/ReVanced/revanced-patches/commit/7517f57ac7a54e1c914e8dd8cc3e1aa908e28e54))
|
||||||
|
|
||||||
|
# [6.0.0-dev.25](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.24...v6.0.0-dev.25) (2026-03-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Instagram:** Add `Disable Reels auto-scroll` patch ([#6736](https://github.com/ReVanced/revanced-patches/issues/6736)) ([806d6c7](https://github.com/ReVanced/revanced-patches/commit/806d6c799fb67c0fb630ae954ef615ff01597b1f))
|
||||||
|
|
||||||
|
# [6.0.0-dev.24](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.23...v6.0.0-dev.24) (2026-03-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Photoshop Mix:** Add `Bypass login` patch ([#6745](https://github.com/ReVanced/revanced-patches/issues/6745)) ([24caae9](https://github.com/ReVanced/revanced-patches/commit/24caae98b7b4d61b388f644cc1512438e408e6b1))
|
||||||
|
|
||||||
|
# [6.0.0-dev.23](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.22...v6.0.0-dev.23) (2026-03-09)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **ProtonVPN - Remove delay:** Make it work on latest version by patching the correct class ([#6757](https://github.com/ReVanced/revanced-patches/issues/6757)) ([e0dc009](https://github.com/ReVanced/revanced-patches/commit/e0dc009780afea9c2f393c4f348cda5ca9c3cbbf))
|
||||||
|
|
||||||
|
# [6.0.0-dev.22](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.21...v6.0.0-dev.22) (2026-03-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Settings:** Icon not drawn correctly on some systems ([#6683](https://github.com/ReVanced/revanced-patches/issues/6683)) ([ddb6396](https://github.com/ReVanced/revanced-patches/commit/ddb6396b3f3f7a2c29b9fa171e189f9931ba0e02))
|
||||||
|
|
||||||
|
# [6.0.0-dev.21](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.20...v6.0.0-dev.21) (2026-03-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Instagram:** Update fingerprints for version `417.0.0.54.77` ([#6734](https://github.com/ReVanced/revanced-patches/issues/6734)) ([55f510d](https://github.com/ReVanced/revanced-patches/commit/55f510dbedd28678411b4f11d9bbdd303fa68a0d))
|
||||||
|
* **Spotify - Sanitize sharing links:** Update patch to latest app versions ([#6685](https://github.com/ReVanced/revanced-patches/issues/6685)) ([bb7448b](https://github.com/ReVanced/revanced-patches/commit/bb7448bc9d789843371d16bfccc9815662913333))
|
||||||
|
|
||||||
|
# [6.0.0-dev.20](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.19...v6.0.0-dev.20) (2026-03-08)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Enable Debugging Patch:** Use correct Protocolbuffer setting name ([#6711](https://github.com/ReVanced/revanced-patches/issues/6711)) ([f934022](https://github.com/ReVanced/revanced-patches/commit/f934022f37ba178ac23abfa9bcd59a0c12abe43f))
|
||||||
|
|
||||||
|
# [6.0.0-dev.19](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.18...v6.0.0-dev.19) (2026-03-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Hex:** Add back name, which was accidentally removed from the patch ([6a547a9](https://github.com/ReVanced/revanced-patches/commit/6a547a97e52b7914bb6602f3ecc2c6cecd50e946))
|
||||||
|
|
||||||
|
# [6.0.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.17...v6.0.0-dev.18) (2026-03-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide Shorts components:** Find resource id only for 21.05+ ([63161e9](https://github.com/ReVanced/revanced-patches/commit/63161e9fb357387685294e4a80de94cb351c6713))
|
||||||
|
|
||||||
|
# [6.0.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.16...v6.0.0-dev.17) (2026-03-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Spoof video streams:** Make it work on 21.x ([#6705](https://github.com/ReVanced/revanced-patches/issues/6705)) ([fdfed3c](https://github.com/ReVanced/revanced-patches/commit/fdfed3c9dd46f477c1cc1b9db0f08054ffa32293))
|
||||||
|
|
||||||
|
# [6.0.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.15...v6.0.0-dev.16) (2026-03-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Spoof app version:** Remove target `19.35.36` no longer supported by YouTube ([#6717](https://github.com/ReVanced/revanced-patches/issues/6717)) ([46fb366](https://github.com/ReVanced/revanced-patches/commit/46fb3669ee59534327d7c3d78e07b813d8a2badb))
|
||||||
|
* **YouTube Music:** Prevent crash on bold icons loading ([#6712](https://github.com/ReVanced/revanced-patches/issues/6712)) ([e9bfb7c](https://github.com/ReVanced/revanced-patches/commit/e9bfb7ca9bcd1499f1abe8872999aefff10cd187))
|
||||||
|
|
||||||
|
# [6.0.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.14...v6.0.0-dev.15) (2026-03-05)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Check environment:** Use another (also more suitable) API to circumvent a bug ([393700f](https://github.com/ReVanced/revanced-patches/commit/393700f74ac141bfa109988202707b40d35a64ea))
|
||||||
|
|
||||||
|
# [6.0.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.13...v6.0.0-dev.14) (2026-03-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Playback speed:** Use correct extension method name ([b8b4cfb](https://github.com/ReVanced/revanced-patches/commit/b8b4cfbd016058a158364f4549e7ef6ed4d154e0))
|
||||||
|
|
||||||
|
# [6.0.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.12...v6.0.0-dev.13) (2026-03-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Use custom comparison block for strings in `anyOf` ([56a087d](https://github.com/ReVanced/revanced-patches/commit/56a087dbacf331ccadfe753cbc1ced77e318fc27))
|
||||||
|
|
||||||
|
# [6.0.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.11...v6.0.0-dev.12) (2026-03-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fix return type check to match method successfully ([0a73452](https://github.com/ReVanced/revanced-patches/commit/0a734528dc4407571ae1dba3e80347bc9f236e3e))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Check environment patch:** Support another ReVanced Manager debug variant package name ([e4dea68](https://github.com/ReVanced/revanced-patches/commit/e4dea682c6640ce817d5e30cfddec953fe85436f))
|
||||||
|
|
||||||
|
# [6.0.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.10...v6.0.0-dev.11) (2026-03-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Use correct string key ([9d55d00](https://github.com/ReVanced/revanced-patches/commit/9d55d00ff46a2cd18111a91a98dbc8e3137dd0ed))
|
||||||
|
|
||||||
|
# [6.0.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.9...v6.0.0-dev.10) (2026-03-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Enable debugging:** Add missing preference to log protocol buffer ([26d8a9e](https://github.com/ReVanced/revanced-patches/commit/26d8a9e5f891e08fe3c23601e8238de6a301b8df))
|
||||||
|
|
||||||
|
# [6.0.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.8...v6.0.0-dev.9) (2026-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube:** Add back missing custom filter by adding the preference to the correct screen ([2a10489](https://github.com/ReVanced/revanced-patches/commit/2a10489a869cbab1ed01502bc6fe9330c4052e06))
|
||||||
|
|
||||||
|
# [6.0.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.7...v6.0.0-dev.8) (2026-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **GmsCore support:** Try replacing in strings before prefixing to handle more edge cases ([4d94a41](https://github.com/ReVanced/revanced-patches/commit/4d94a41c46f2d4e1bf33debc95b8aa84a64964bb))
|
||||||
|
|
||||||
|
# [6.0.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.6...v6.0.0-dev.7) (2026-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Rename string keys correctly ([16e00ab](https://github.com/ReVanced/revanced-patches/commit/16e00ab4c0ff10e58adea40c7de72658788fcd97))
|
||||||
|
|
||||||
|
# [6.0.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.5...v6.0.0-dev.6) (2026-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Move strings to correct patch ([4dfe3fb](https://github.com/ReVanced/revanced-patches/commit/4dfe3fb08812ed572e01e58a8604c1be9e989438))
|
||||||
|
|
||||||
|
# [6.0.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.4...v6.0.0-dev.5) (2026-02-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Reddit clients:** Fix patching broken during patcher migration by searching for strings with contains([#6681](https://github.com/ReVanced/revanced-patches/issues/6681)) ([00da402](https://github.com/ReVanced/revanced-patches/commit/00da4027707068f06ee7041b53d1316a7b218d5d))
|
||||||
|
|
||||||
|
# [6.0.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v6.0.0-dev.3...v6.0.0-dev.4) (2026-02-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Custom branding:** Fix defaults ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([3e00a99](https://github.com/ReVanced/revanced-patches/commit/3e00a99c1bb3af24f9e8420e8c7c2bbaeb003c6c))
|
||||||
|
* **Custom branding:** Resolve background playback crash with custom branded root installation ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([6aba2d1](https://github.com/ReVanced/revanced-patches/commit/6aba2d127472643c346108d481513442fa9a3fde))
|
||||||
|
* **Hex patch:** Fix bug in implementation of Boyer-Moore algorithm ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([f59323c](https://github.com/ReVanced/revanced-patches/commit/f59323c87d8da36b39e19936c8ed5c07d3903b16))
|
||||||
|
* **YouTube - Exit fullscreen mode:** Handle exiting fullscreen on first opened video ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([88724d4](https://github.com/ReVanced/revanced-patches/commit/88724d47b13d56a90384b0a2588ba82ccdd5b101))
|
||||||
|
* **YouTube - Hide ads:** Empty space left when ads are hidden on tablets ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([c1c2aa9](https://github.com/ReVanced/revanced-patches/commit/c1c2aa98b2d7ce900eb152bc736f3c1a5558d9fc))
|
||||||
|
* **YouTube - Hide ads:** Fix "Hide YouTube Premium promotions" hiding YouTube Doodles ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([d26e352](https://github.com/ReVanced/revanced-patches/commit/d26e352850c2659a65b13ff1ba50dcd18278603a))
|
||||||
|
* **YouTube - Hide ads:** Hide new type of general ad, movie ad and web search result ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([9b12dd1](https://github.com/ReVanced/revanced-patches/commit/9b12dd106546d94004c971b887ffa7627ae5a8d4))
|
||||||
|
* **YouTube - Hide ads:** Hide new type of player ad ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([c97aefc](https://github.com/ReVanced/revanced-patches/commit/c97aefc272b83b522e5ac393ec41d03630cee6fb))
|
||||||
|
* **YouTube - Hide ads:** Hide video ads does not hide Shorts ads ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([8d274a7](https://github.com/ReVanced/revanced-patches/commit/8d274a7afc3abfafc2b702b27f022316c854dae6))
|
||||||
|
* **YouTube - Hide ads:** Support Hide fullscreen ads on Android 13+ devices ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([02b405e](https://github.com/ReVanced/revanced-patches/commit/02b405e6ac5beeff81c7705379e6c6eb1561270d))
|
||||||
|
* **YouTube - Hide ads:** YouTube Doodles unclickable when Hide ads is enabled ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([5d45b6d](https://github.com/ReVanced/revanced-patches/commit/5d45b6da74165ca69a336aa36e90daafaaf87411))
|
||||||
|
* **YouTube - Hide end screen cards:** Resolve patching 20.31.4x ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([3ff303f](https://github.com/ReVanced/revanced-patches/commit/3ff303f045c4fbda0331e3f1e9fbba50f97dedab))
|
||||||
|
* **YouTube - Hide layout components:** Ensure featured places also hide watch history shelf ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([d639faf](https://github.com/ReVanced/revanced-patches/commit/d639faf71f476bcd7fffa08bfbb0e77c02450c9f))
|
||||||
|
* **YouTube - Hide layout components:** Fix certain description components not working ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([1bf64eb](https://github.com/ReVanced/revanced-patches/commit/1bf64eb8b06435dea9cd292376c5feda6683e0a6))
|
||||||
|
* **YouTube - Hide layout components:** Fix empty space issues (subscribed channels bar, show more button, landscape mode) ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([22ef700](https://github.com/ReVanced/revanced-patches/commit/22ef7002e07df919c30e9274a2479925a4be69c0))
|
||||||
|
* **YouTube - Hide layout components:** Fix side effect of Disable translucent status bar ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([5760c58](https://github.com/ReVanced/revanced-patches/commit/5760c5860ac2dc6a41821cc66f849a58e44bf3e7))
|
||||||
|
* **YouTube - Hide layout components:** Resolve "Hide community posts" not working in search results ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([3153222](https://github.com/ReVanced/revanced-patches/commit/315322220d6a09814406394414bcfcff61ead786))
|
||||||
|
* **YouTube - Hide layout components:** Resolve community posts sometimes showing in player suggestions ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([828df77](https://github.com/ReVanced/revanced-patches/commit/828df77810b551c70e03d888dc0fe1555c488f51))
|
||||||
|
* **YouTube - Hide Shorts components:** Action buttons not hidden in 20.22+ ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([a90a0b1](https://github.com/ReVanced/revanced-patches/commit/a90a0b1199e66cace3eb1b8c827314ceaf514ecf))
|
||||||
|
* **YouTube - Hide Shorts components:** Do not hide channel page headers when hiding shorts ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([1246e43](https://github.com/ReVanced/revanced-patches/commit/1246e430f2104bc4a33881fa4dbb188201c02202))
|
||||||
|
* **YouTube - Hide Shorts components:** Fix sound metadata label hiding other components ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([49d1f65](https://github.com/ReVanced/revanced-patches/commit/49d1f65fcae5b6732b768f6184969a6c796bc5e3))
|
||||||
|
* **YouTube - Hide Shorts components:** Hide new type of sound metadata label ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([a6b8d2f](https://github.com/ReVanced/revanced-patches/commit/a6b8d2f1039b7896b21826a46f3f13b32d16b51d))
|
||||||
|
* **YouTube - Hide Shorts components:** Resolve hiding Shorts not working ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([ae69bdc](https://github.com/ReVanced/revanced-patches/commit/ae69bdc1d376a05b6854401586408cb6a9bda7eb))
|
||||||
|
* **YouTube - Loop video:** Enable loop video not working in playlist ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([06dbf7e](https://github.com/ReVanced/revanced-patches/commit/06dbf7ee80c836404e3698c9db6176da9a2ab8e1))
|
||||||
|
* **YouTube - Loop video:** Fix looping button state ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([14d0135](https://github.com/ReVanced/revanced-patches/commit/14d0135b3c41bb0c06fb8cd6569a489c41e51105))
|
||||||
|
* **YouTube - Loop video:** Wrong icon applied ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([b34adf6](https://github.com/ReVanced/revanced-patches/commit/b34adf6437294b0b28500c207b5f29ddd2ed294d))
|
||||||
|
* **YouTube - Open Shorts in regular player:** Fix back behavior with 20.51 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([46ec3d3](https://github.com/ReVanced/revanced-patches/commit/46ec3d3bdd7d0368e1503a1b1be815eaf9b56525))
|
||||||
|
* **YouTube - Open Shorts in regular player:** Resolve back button closing app instead of exiting fullscreen ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([b787c46](https://github.com/ReVanced/revanced-patches/commit/b787c469fd856dff74870fcb61bb3fc3dc5514b7))
|
||||||
|
* **YouTube - Remove background playback restrictions:** Fix background playback not working with certain offline videos ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([2d098f2](https://github.com/ReVanced/revanced-patches/commit/2d098f2352b7dc1f0dc185ac65074443289ef2de))
|
||||||
|
* **YouTube - Remove viewer discretion dialog:** Not working on 20.14.43+ ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([64c397e](https://github.com/ReVanced/revanced-patches/commit/64c397eb1c46bdd77f2b05d03c22a841971bea81))
|
||||||
|
* **YouTube - Return YouTube Dislike:** Fix incorrect dislike counts after cancel ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([ad10d76](https://github.com/ReVanced/revanced-patches/commit/ad10d760354dba1e8f470972955a706da9b85c02))
|
||||||
|
* **YouTube - ReturnYouTubeDislike:** Fix dislikes not showing with 20.31+ ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([2033883](https://github.com/ReVanced/revanced-patches/commit/203388329484616cc83aef2c3bda38a3069839ca))
|
||||||
|
* **YouTube - SponsorBlock:** Do not show context toast when auto skipping in feed ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([88157ac](https://github.com/ReVanced/revanced-patches/commit/88157ac5b791d4d56e8347203a02f5c78014235b))
|
||||||
|
* **YouTube - SponsorBlock:** Resolve segments not fetching on experimental app targets ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([2067799](https://github.com/ReVanced/revanced-patches/commit/206779942d9b4e8131c4df1acb1e7eab63ec75a0))
|
||||||
|
* **YouTube - SponsorBlock:** Show correct nested skip segment when seeking ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([f5ef68b](https://github.com/ReVanced/revanced-patches/commit/f5ef68b61a5880a574f6d0f06e4b96c00daf11bb))
|
||||||
|
* **YouTube Music - Navigation bar:** Hide library tab with 8.24+ ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([cfcae43](https://github.com/ReVanced/revanced-patches/commit/cfcae434652b747345cb31b66748f0cc3220eb4a))
|
||||||
|
* **YouTube:** Change recommended version to 20.37.48 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([3dd305c](https://github.com/ReVanced/revanced-patches/commit/3dd305ca5d092144a924e150a668443b8f7ec3d8))
|
||||||
|
* **YouTube:** Changes the default values for some settings ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([dce204b](https://github.com/ReVanced/revanced-patches/commit/dce204b41beb13b675d04afea3129df73a182172))
|
||||||
|
* **YouTube:** Do not show bold icons if old settings menus is enabled ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([30bd852](https://github.com/ReVanced/revanced-patches/commit/30bd852ba5236ca25a7cc49fc23f987def27d23a))
|
||||||
|
* **YouTube:** Fix patching unsupported 20.13.41 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([ed45375](https://github.com/ReVanced/revanced-patches/commit/ed453751057310a053600c4d50c87532a3f94989))
|
||||||
|
* **YouTube:** Ignore cairo flag in debug flag manager ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([093497c](https://github.com/ReVanced/revanced-patches/commit/093497c34f7d6c431ce7958d6b0f85b9dd0373cd))
|
||||||
|
* **YouTube:** Remove 19.43.41 that YouTube no longer supports ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([a8526dc](https://github.com/ReVanced/revanced-patches/commit/a8526dc8ae325b3b3d386ad1d23670b05a48da51))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add overlay buttons animation ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([f6fc6aa](https://github.com/ReVanced/revanced-patches/commit/f6fc6aa5ac6364dc2806e62618c300a8542b3cb0))
|
||||||
|
* **Custom branding:** Default to user-provided icon and name when provided ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([f19c35e](https://github.com/ReVanced/revanced-patches/commit/f19c35e21cc77e8f6f746f7f910d520f86981dd5))
|
||||||
|
* **Enable debugging:** Allow overriding String/long/double flags in debug flag manager ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([1f91bc8](https://github.com/ReVanced/revanced-patches/commit/1f91bc8a20134c5519b8e031badfa741f7cac7a7))
|
||||||
|
* Handle multiple branch conditionals jumping to the same instruction index ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([2f7b57d](https://github.com/ReVanced/revanced-patches/commit/2f7b57d071d316985a1fec215045b6b78ede6212))
|
||||||
|
* Perform full search of free registers ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([01ef43a](https://github.com/ReVanced/revanced-patches/commit/01ef43ababdf015f1ad3edaf45445da0e72199f2))
|
||||||
|
* Update YouTube & YouTube Music patches ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([88d33b8](https://github.com/ReVanced/revanced-patches/commit/88d33b847de4d2ad834a4940ee257e06e3c3ad31))
|
||||||
|
* Use more informative patch error if the same APK is patched twice ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([26e5ce1](https://github.com/ReVanced/revanced-patches/commit/26e5ce1a325c2a6e78a5486d661f7750ecc792a3))
|
||||||
|
* **YouTube - Disable haptic feedback:** Add Disable tap and hold haptics setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([f135122](https://github.com/ReVanced/revanced-patches/commit/f135122df1a5e6a8b822652abb2451ea4e4a3d08))
|
||||||
|
* **YouTube - Hide ads:** Add Hide player popup ads setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([487a95d](https://github.com/ReVanced/revanced-patches/commit/487a95d3efa878d9b41f1b719924c5504e0a1d0a))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide channel tab filter" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([0adcd8c](https://github.com/ReVanced/revanced-patches/commit/0adcd8c62e12619d5adaac5ee9886613deb53ca4))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide collapse button" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([1554fd9](https://github.com/ReVanced/revanced-patches/commit/1554fd916d1bcc9c67319d55b21072423926fc32))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide comments section in Home feed" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([5278434](https://github.com/ReVanced/revanced-patches/commit/5278434534653ea741e67cc1e5258abb7ca0e21e))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide course progress" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([1927564](https://github.com/ReVanced/revanced-patches/commit/192756443a1b2ede413e2d4ae55eed2bd9d57aac))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide explore this course" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([3e24762](https://github.com/ReVanced/revanced-patches/commit/3e24762c1847dfc467a5d6bf65cc1c3c0931ca0f))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide featured links", "Hide featured videos", "Hide join button", and "Hide subscribe button" options ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([f9e843d](https://github.com/ReVanced/revanced-patches/commit/f9e843d75641d4a87dfbe05fa8fd407ccc0345d6))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide feed flyout menu filter" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([a93de46](https://github.com/ReVanced/revanced-patches/commit/a93de46572a7bd1ff30a1fb653e3f7afb1c67571))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide fullscreen button" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([b07b160](https://github.com/ReVanced/revanced-patches/commit/b07b1609e4bd9341611d6aa0194c9764616719b4))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide latest videos button" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([ebfdd8d](https://github.com/ReVanced/revanced-patches/commit/ebfdd8df2c5323290f6e655ebf0dd1db683f33dd))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide live chat replay button" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([a6bd311](https://github.com/ReVanced/revanced-patches/commit/a6bd3116f97e539482c752e8e4e1b1e8e90ed464))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide quizzes" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([70b9e10](https://github.com/ReVanced/revanced-patches/commit/70b9e103aea817bed1d0972444c7b0726214c69c))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide search box trending results" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([21bf455](https://github.com/ReVanced/revanced-patches/commit/21bf455c3f61e5fd19f97a1580ecb26ac40dcdce))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide subscribed channels bar" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([e41a40f](https://github.com/ReVanced/revanced-patches/commit/e41a40f0d754397f9cea09f387cc901f0397787e))
|
||||||
|
* **YouTube - Hide layout components:** Add "Hide video title" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([2cfbe08](https://github.com/ReVanced/revanced-patches/commit/2cfbe08b2137b2520dd37927202a4586af8326ff))
|
||||||
|
* **YouTube - Hide layout components:** Apply hide search suggestions only to more recent app targets ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([a43c0e1](https://github.com/ReVanced/revanced-patches/commit/a43c0e111bfe290f7dec3c9b75b882ea9dc5630f))
|
||||||
|
* **YouTube - Hide layout components:** Replace "Hide search suggestions" with "Hide You may like section" ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([80f6b01](https://github.com/ReVanced/revanced-patches/commit/80f6b01c64971881bb9144cada0e91bb78b9f38d))
|
||||||
|
* **YouTube - Hide Shorts components:** Add "Hide AI button" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([99aace4](https://github.com/ReVanced/revanced-patches/commit/99aace4178ccc9aeaaeb0b19cd6f520c10ef7df2))
|
||||||
|
* **YouTube - Hide Shorts components:** Add "Hide in video description" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([e0a8b7b](https://github.com/ReVanced/revanced-patches/commit/e0a8b7bc59113ce57e5b8b358bad9171a4ea1f99))
|
||||||
|
* **YouTube - Navigation bar:** Add settings to hide toolbar buttons ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([d72e39f](https://github.com/ReVanced/revanced-patches/commit/d72e39f2a8fc0894667546826ef07cb3edf78e50))
|
||||||
|
* **YouTube - Navigation buttons:** Add setting to use narrow navigation bar buttons ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([e48a5d7](https://github.com/ReVanced/revanced-patches/commit/e48a5d76f7651b0edcdb5a9b27e596df41e9c6af))
|
||||||
|
* **YouTube - SponsorBlock:** Show skip button if player overlay controls are active ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([35ec655](https://github.com/ReVanced/revanced-patches/commit/35ec655f83ffe7ab661dca07107a74f2f9617037))
|
||||||
|
* **YouTube - Theme:** Add "Hide splash screen" setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([ca6e184](https://github.com/ReVanced/revanced-patches/commit/ca6e184172e67cca48ea4c70cfe6371e806dd793))
|
||||||
|
* **YouTube - Video quality:** Add Hide Premium video quality setting ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([50a2b67](https://github.com/ReVanced/revanced-patches/commit/50a2b67ef6e6382894636acdc1c2fcf7236ab4ee))
|
||||||
|
* **YouTube Music:** Add experimental support for 9.02.50 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([50a102d](https://github.com/ReVanced/revanced-patches/commit/50a102d8afc573936f790991381b0a8d2f8dd54d))
|
||||||
|
* **YouTube Music:** Add experimental support for 9.03.52 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([d5b9c0c](https://github.com/ReVanced/revanced-patches/commit/d5b9c0c03d334ff31c9601a48a3beb1a4db98310))
|
||||||
|
* **YouTube Music:** Change recommended version to 8.37.56 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([d1e7900](https://github.com/ReVanced/revanced-patches/commit/d1e7900793ceef7b53b140ba9efe25025a8aac01))
|
||||||
|
* **YouTube Music:** Support version 8.40.54 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([62f130c](https://github.com/ReVanced/revanced-patches/commit/62f130cc883d69d40c364cac45158012dd01272f))
|
||||||
|
* **YouTube Music:** Unofficial support of 8.50.51 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([c1d7cae](https://github.com/ReVanced/revanced-patches/commit/c1d7caeee2cfa425769571b0ebff2da86e709ef9))
|
||||||
|
* **YouTube:** Add experimental support for 21.02.32 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([7904b60](https://github.com/ReVanced/revanced-patches/commit/7904b60dbea526af45b4a69dc349c6250453b385))
|
||||||
|
* **YouTube:** Add experimental support for 21.03.34 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([1ae36a1](https://github.com/ReVanced/revanced-patches/commit/1ae36a1cc72f0fb29d592206f74fcd40e37acaba))
|
||||||
|
* **YouTube:** Add experimental support for 21.04.221 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([66e113a](https://github.com/ReVanced/revanced-patches/commit/66e113a96639d0c99126749125adf234a9b10cab))
|
||||||
|
* **YouTube:** Add experimental support for 21.05.264 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([f646c82](https://github.com/ReVanced/revanced-patches/commit/f646c820d7d6027cf013e0968189a1e2cfd9e641))
|
||||||
|
* **YouTube:** Add experimental support for 21.06.251 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([44b17d4](https://github.com/ReVanced/revanced-patches/commit/44b17d47588251b9fab5c801a49ace2ce371fa99))
|
||||||
|
* **YouTube:** Add experimental support for 21.06.257 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([abb703d](https://github.com/ReVanced/revanced-patches/commit/abb703dcb2ac96f30e699a33d3a896b775bb0851))
|
||||||
|
* **YouTube:** Add experimental support for 21.07.240 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([79b0c1f](https://github.com/ReVanced/revanced-patches/commit/79b0c1f72ff5b52b162f3f861d5e10c657efa097))
|
||||||
|
* **YouTube:** Add Hide autoplay preview patch ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([79e3955](https://github.com/ReVanced/revanced-patches/commit/79e3955fde7068eac90ae404b3869c27f17bd5f7))
|
||||||
|
* **YouTube:** Add more double tap to seek length options ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([fb04071](https://github.com/ReVanced/revanced-patches/commit/fb04071528683d38913c57f628cbab64bf0ef6a4))
|
||||||
|
* **YouTube:** Remove obsolete seekbar thumbnail patch ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([9909fc1](https://github.com/ReVanced/revanced-patches/commit/9909fc1e5d490e9edb59894d66c6a929fbaebb3b))
|
||||||
|
* **YouTube:** Support version 20.40.45 ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([96c85d0](https://github.com/ReVanced/revanced-patches/commit/96c85d03712e79217dc8f97bcda5f38c0e47f064))
|
||||||
|
|
||||||
|
# [6.0.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.51.0-dev.2...v6.0.0-dev.1) (2026-02-27)
|
||||||
|
|
||||||
|
|
||||||
|
* build(Needs bump)!: Update to ReVanced Patcher v22 ([#6542](https://github.com/ReVanced/revanced-patches/issues/6542)) ([ab2ac36](https://github.com/ReVanced/revanced-patches/commit/ab2ac36e3041cda87b659924ea2b75089f0bdb6e))
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* Deprecated APIs have been removed, and various APIs now use the updated ReVanced Patcher v22 APIs.
|
||||||
|
|
||||||
|
# [5.51.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.51.0-dev.1...v5.51.0-dev.2) (2026-02-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **GMX Mail:** Add `Hide ads` and `Hide Premium upgrade button` patches ([#6583](https://github.com/ReVanced/revanced-patches/issues/6583)) ([2976ea3](https://github.com/ReVanced/revanced-patches/commit/2976ea3ddd09d26eeedf646f0a1020fa582d0ec0))
|
||||||
|
|
||||||
|
# [5.51.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.50.3-dev.4...v5.51.0-dev.1) (2026-02-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **GMX Mail:** Add `Force enable Freephone` patch ([#6650](https://github.com/ReVanced/revanced-patches/issues/6650)) ([997b5d6](https://github.com/ReVanced/revanced-patches/commit/997b5d63d1fc1684bea9e5b265f3aca53ad5fd88))
|
||||||
|
|
||||||
|
## [5.50.3-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.50.3-dev.3...v5.50.3-dev.4) (2026-02-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **GmsCore support:** Insert check after another missing necessary context hook ([3c0c5a8](https://github.com/ReVanced/revanced-patches/commit/3c0c5a86d8e24b47b1c30bc5a7fe994240014e2d))
|
||||||
|
|
||||||
|
## [5.50.3-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.50.3-dev.2...v5.50.3-dev.3) (2026-02-20)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **GmsCore support:** Insert check after necessary context hook ([03e8e3d](https://github.com/ReVanced/revanced-patches/commit/03e8e3d75cb3b03987299885cea5eb615a5cef23))
|
||||||
|
|
||||||
|
## [5.50.3-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.50.3-dev.1...v5.50.3-dev.2) (2026-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **GmsCore support:** Handle GmsCore flavors when checking for updates ([2aa19f5](https://github.com/ReVanced/revanced-patches/commit/2aa19f5995fd050c40b15331a77d58144a5a1f69))
|
||||||
|
* Use positional substitutes in strings where multiple are present ([aa8c87f](https://github.com/ReVanced/revanced-patches/commit/aa8c87f8650bd5def5f726f02be5d62d72a3007b))
|
||||||
|
|
||||||
|
## [5.50.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.50.2...v5.50.3-dev.1) (2026-02-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **GmsCore support:** Rename MicroG GmsCore specific strings as well and rename app specific strings correctly ([c2ac1f0](https://github.com/ReVanced/revanced-patches/commit/c2ac1f04a0ac180555a9d19e7ff41525487fbc6d))
|
||||||
|
|
||||||
## [5.50.2](https://github.com/ReVanced/revanced-patches/compare/v5.50.1...v5.50.2) (2026-02-15)
|
## [5.50.2](https://github.com/ReVanced/revanced-patches/compare/v5.50.1...v5.50.2) (2026-02-15)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.extension"
|
defaultConfig {
|
||||||
|
minSdk = 23
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,8 @@
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
aidl = true
|
aidl = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
android {
|
android {
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,9 @@ dependencies {
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
compileOnly(libs.okhttp)
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 22
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,9 @@ dependencies {
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
compileOnly(libs.okhttp)
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:cricbuzz:stub"))
|
compileOnly(project(":extensions:cricbuzz:stub"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@ public final class SanitizeSharingLinksPatch {
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String sanitizeSharingLink(String url) {
|
public static String sanitizeSharingLink(String url) {
|
||||||
return sanitizer.sanitizeUrlString(url);
|
return sanitizer.sanitizeURLString(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,6 @@ public final class SanitizeSharingLinksPatch {
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String sanitizeSharingLink(String url) {
|
public static String sanitizeSharingLink(String url) {
|
||||||
return sanitizer.sanitizeUrlString(url);
|
return sanitizer.sanitizeURLString(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package app.revanced.extension.music;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
public class VersionCheckUtils {
|
||||||
|
private static boolean isVersionOrGreater(String version) {
|
||||||
|
return Utils.getAppVersionName().compareTo(version) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final boolean IS_8_40_OR_GREATER = isVersionOrGreater("8.40.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -5,14 +5,15 @@ import static app.revanced.extension.shared.Utils.hideViewUnderCondition;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class NavigationBarPatch {
|
public class NavigationBarPatch {
|
||||||
@NonNull
|
|
||||||
private static String lastYTNavigationEnumName = "";
|
private static String lastYTNavigationEnumName = "";
|
||||||
|
|
||||||
public static void setLastAppNavigationEnum(@Nullable Enum<?> ytNavigationEnumName) {
|
public static void setLastAppNavigationEnum(@Nullable Enum<?> ytNavigationEnumName) {
|
||||||
|
|
@ -25,7 +26,7 @@ public class NavigationBarPatch {
|
||||||
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview);
|
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void hideNavigationButton(@NonNull View view) {
|
public static void hideNavigationButton(View view) {
|
||||||
// Hide entire navigation bar.
|
// Hide entire navigation bar.
|
||||||
if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) {
|
if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) {
|
||||||
hideViewUnderCondition(true, (View) view.getParent());
|
hideViewUnderCondition(true, (View) view.getParent());
|
||||||
|
|
@ -34,7 +35,7 @@ public class NavigationBarPatch {
|
||||||
|
|
||||||
// Hide navigation buttons based on their type.
|
// Hide navigation buttons based on their type.
|
||||||
for (NavigationButton button : NavigationButton.values()) {
|
for (NavigationButton button : NavigationButton.values()) {
|
||||||
if (button.ytEnumNames.equals(lastYTNavigationEnumName)) {
|
if (button.ytEnumNames.contains(lastYTNavigationEnumName)) {
|
||||||
hideViewUnderCondition(button.hidden, view);
|
hideViewUnderCondition(button.hidden, view);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -43,30 +44,41 @@ public class NavigationBarPatch {
|
||||||
|
|
||||||
private enum NavigationButton {
|
private enum NavigationButton {
|
||||||
HOME(
|
HOME(
|
||||||
"TAB_HOME",
|
Arrays.asList(
|
||||||
|
"TAB_HOME"
|
||||||
|
),
|
||||||
Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get()
|
Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get()
|
||||||
),
|
),
|
||||||
SAMPLES(
|
SAMPLES(
|
||||||
"TAB_SAMPLES",
|
Arrays.asList(
|
||||||
|
"TAB_SAMPLES"
|
||||||
|
),
|
||||||
Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get()
|
Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get()
|
||||||
),
|
),
|
||||||
EXPLORE(
|
EXPLORE(
|
||||||
"TAB_EXPLORE",
|
Arrays.asList(
|
||||||
|
"TAB_EXPLORE"
|
||||||
|
),
|
||||||
Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get()
|
Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get()
|
||||||
),
|
),
|
||||||
LIBRARY(
|
LIBRARY(
|
||||||
"LIBRARY_MUSIC",
|
Arrays.asList(
|
||||||
|
"LIBRARY_MUSIC",
|
||||||
|
"TAB_BOOKMARK" // YouTube Music 8.24+
|
||||||
|
),
|
||||||
Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get()
|
Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get()
|
||||||
),
|
),
|
||||||
UPGRADE(
|
UPGRADE(
|
||||||
"TAB_MUSIC_PREMIUM",
|
Arrays.asList(
|
||||||
|
"TAB_MUSIC_PREMIUM"
|
||||||
|
),
|
||||||
Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get()
|
Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get()
|
||||||
);
|
);
|
||||||
|
|
||||||
private final String ytEnumNames;
|
private final List<String> ytEnumNames;
|
||||||
private final boolean hidden;
|
private final boolean hidden;
|
||||||
|
|
||||||
NavigationButton(@NonNull String ytEnumNames, boolean hidden) {
|
NavigationButton(List<String> ytEnumNames, boolean hidden) {
|
||||||
this.ytEnumNames = ytEnumNames;
|
this.ytEnumNames = ytEnumNames;
|
||||||
this.hidden = hidden;
|
this.hidden = hidden;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ import android.preference.PreferenceFragment;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.VersionCheckUtils;
|
||||||
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
||||||
import app.revanced.extension.music.settings.search.MusicSearchViewController;
|
import app.revanced.extension.music.settings.search.MusicSearchViewController;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
|
|
||||||
|
|
@ -22,6 +24,24 @@ public class MusicActivityHook extends BaseActivityHook {
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
public static MusicSearchViewController searchViewController;
|
public static MusicSearchViewController searchViewController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @see app.revanced.extension.youtube.settings.YouTubeActivityHook#MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS
|
||||||
|
*/
|
||||||
|
private static final long MINIMUM_TIME_AFTER_FIRST_LAUNCH_BEFORE_ALLOWING_BOLD_ICONS = 30 * 1000; // 30 seconds.
|
||||||
|
|
||||||
|
static {
|
||||||
|
final boolean useBoldIcons = VersionCheckUtils.IS_8_40_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;
|
||||||
|
|
||||||
|
Utils.setAppIsUsingBoldIcons(useBoldIcons);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
|
|
@ -46,15 +66,7 @@ public class MusicActivityHook extends BaseActivityHook {
|
||||||
// Override the default YouTube Music theme to increase start padding of list items.
|
// Override the default YouTube Music theme to increase start padding of list items.
|
||||||
// Custom style located in resources/music/values/style.xml
|
// Custom style located in resources/music/values/style.xml
|
||||||
activity.setTheme(Utils.getResourceIdentifierOrThrow(
|
activity.setTheme(Utils.getResourceIdentifierOrThrow(
|
||||||
"Theme.ReVanced.YouTubeMusic.Settings", "style"));
|
ResourceType.STYLE, "Theme.ReVanced.YouTubeMusic.Settings"));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the resource ID for the YouTube Music settings layout.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected int getContentViewResourceId() {
|
|
||||||
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -123,4 +135,14 @@ public class MusicActivityHook extends BaseActivityHook {
|
||||||
public static boolean handleFinish() {
|
public static boolean handleFinish() {
|
||||||
return MusicSearchViewController.handleFinish(searchViewController);
|
return MusicSearchViewController.handleFinish(searchViewController);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* Decides whether to use bold icons.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static boolean useBoldIcons(boolean original) {
|
||||||
|
return Utils.appIsUsingBoldIcons();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,6 @@ dependencies {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 23
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,3 +2,9 @@ dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:nunl:stub"))
|
compileOnly(project(":extensions:nunl:stub"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,9 @@ dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:primevideo:stub"))
|
compileOnly(project(":extensions:primevideo:stub"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:reddit:stub"))
|
compileOnly(project(":extensions:reddit:stub"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,9 @@ dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:samsung:radio:stub"))
|
compileOnly(project(":extensions:samsung:radio:stub"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,9 @@ dependencies {
|
||||||
implementation(project(":extensions:shared:library"))
|
implementation(project(":extensions:shared:library"))
|
||||||
compileOnly(libs.okhttp)
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 23
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,14 @@ import android.os.PowerManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
|
@ -219,11 +222,17 @@ public class GmsCoreSupport {
|
||||||
Utils.runOnBackgroundThread(() -> {
|
Utils.runOnBackgroundThread(() -> {
|
||||||
try {
|
try {
|
||||||
PackageManager manager = context.getPackageManager();
|
PackageManager manager = context.getPackageManager();
|
||||||
String installedVersion = manager.getPackageInfo(packageName, 0).versionName;
|
var installedVersion = manager.getPackageInfo(packageName, 0).versionName;
|
||||||
|
|
||||||
Logger.printDebug(() -> "Installed GmsCore version: " + installedVersion);
|
// GmsCore adds suffixes for flavor builds. Remove the suffix for version comparison.
|
||||||
|
int suffixIndex = installedVersion.indexOf('-');
|
||||||
|
if (suffixIndex != -1)
|
||||||
|
installedVersion = installedVersion.substring(0, suffixIndex);
|
||||||
|
String finalInstalledVersion = installedVersion;
|
||||||
|
|
||||||
String latestVersion = getLatestVersion.get();
|
Logger.printDebug(() -> "Installed GmsCore version: " + finalInstalledVersion);
|
||||||
|
|
||||||
|
var latestVersion = getLatestVersion.get();
|
||||||
|
|
||||||
if (latestVersion == null || latestVersion.isEmpty()) {
|
if (latestVersion == null || latestVersion.isEmpty()) {
|
||||||
Logger.printDebug(() -> "Could not get latest GmsCore version");
|
Logger.printDebug(() -> "Could not get latest GmsCore version");
|
||||||
|
|
@ -235,7 +244,7 @@ public class GmsCoreSupport {
|
||||||
|
|
||||||
// Compare versions
|
// Compare versions
|
||||||
if (!installedVersion.equals(latestVersion)) {
|
if (!installedVersion.equals(latestVersion)) {
|
||||||
Logger.printInfo(() -> "GmsCore update available. Installed: " + installedVersion
|
Logger.printInfo(() -> "GmsCore update available. Installed: " + finalInstalledVersion
|
||||||
+ ", Latest: " + latestVersion);
|
+ ", Latest: " + latestVersion);
|
||||||
|
|
||||||
showUpdateDialog(context, installedVersion, latestVersion);
|
showUpdateDialog(context, installedVersion, latestVersion);
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
* ReVanced specific logger. Logging is done to standard device log (accessible through ADB),
|
||||||
* and additionally accessible thru {@link LogBufferManager}.
|
* and additionally accessible through {@link LogBufferManager}.
|
||||||
*
|
*
|
||||||
* All methods are thread safe, and are safe to call even
|
* All methods are thread safe, and are safe to call even
|
||||||
* if {@link Utils#getContext()} is not available.
|
* if {@link Utils#getContext()} is not available.
|
||||||
|
|
@ -202,7 +202,7 @@ public class Logger {
|
||||||
/**
|
/**
|
||||||
* Logs exceptions under the outer class name of the code calling this method.
|
* Logs exceptions under the outer class name of the code calling this method.
|
||||||
* <p>
|
* <p>
|
||||||
* If the calling code is showing it's own error toast,
|
* If the calling code is showing its own error toast,
|
||||||
* instead use {@link #printInfo(LogMessage, Exception)}
|
* instead use {@link #printInfo(LogMessage, Exception)}
|
||||||
*
|
*
|
||||||
* @param message log message
|
* @param message log message
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -70,7 +70,7 @@ public class StringRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a StringRef object that'll not change it's value
|
* Creates a StringRef object that'll not change its value
|
||||||
*
|
*
|
||||||
* @param value value which toString() method returns when invoked on returned object
|
* @param value value which toString() method returns when invoked on returned object
|
||||||
* @return Unique StringRef instance, its value will never change
|
* @return Unique StringRef instance, its value will never change
|
||||||
|
|
@ -102,7 +102,7 @@ public class StringRef {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
if (resources == null || packageName == null) {
|
if (resources == null || packageName == null) {
|
||||||
Context context = Utils.getContext();
|
var context = Utils.getContext();
|
||||||
resources = context.getResources();
|
resources = context.getResources();
|
||||||
packageName = context.getPackageName();
|
packageName = context.getPackageName();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,18 @@ public abstract class TrieSearch<T> {
|
||||||
* Elements not contained can collide with elements the array does contain,
|
* Elements not contained can collide with elements the array does contain,
|
||||||
* so must compare the nodes character value.
|
* so must compare the nodes character value.
|
||||||
*
|
*
|
||||||
* Alternatively this array could be a sorted and densely packed array,
|
/*
|
||||||
* and lookup is done using binary search.
|
* Alternatively, this could be implemented as a sorted, densely packed array
|
||||||
* That would save a small amount of memory because there's no null children entries,
|
* with lookups performed via binary search.
|
||||||
* but would give a worst case search of O(nlog(m)) where n is the number of
|
* This approach would save a small amount of memory by eliminating null
|
||||||
* characters in the searched text and m is the maximum size of the sorted character arrays.
|
* child entries. However, it would result in a worst-case lookup time of
|
||||||
* Using a hash table array always gives O(n) search time.
|
* O(n log m), where:
|
||||||
* The memory usage here is very small (all Litho filters use ~10KB of memory),
|
* - n is the number of characters in the input text, and
|
||||||
* so the more performant hash implementation is chosen.
|
* - m is the maximum size of the sorted character arrays.
|
||||||
|
* In contrast, using a hash-based array guarantees O(n) lookup time.
|
||||||
|
* Given that the total memory usage is already very small (all Litho filters
|
||||||
|
* together use approximately 10KB), the hash-based implementation is preferred
|
||||||
|
* for its superior performance.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private TrieNode<T>[] children;
|
private TrieNode<T>[] children;
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,11 @@ import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
@ -43,8 +47,10 @@ import java.text.Collator;
|
||||||
import java.text.Normalizer;
|
import java.text.Normalizer;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
@ -59,7 +65,6 @@ import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
@SuppressWarnings("NewApi")
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
|
@ -76,6 +81,8 @@ public class Utils {
|
||||||
@Nullable
|
@Nullable
|
||||||
private static Boolean isDarkModeEnabled;
|
private static Boolean isDarkModeEnabled;
|
||||||
|
|
||||||
|
private static boolean appIsUsingBoldIcons;
|
||||||
|
|
||||||
// Cached Collator instance with its locale.
|
// Cached Collator instance with its locale.
|
||||||
@Nullable
|
@Nullable
|
||||||
private static Locale cachedCollatorLocale;
|
private static Locale cachedCollatorLocale;
|
||||||
|
|
@ -148,12 +155,12 @@ public class Utils {
|
||||||
/**
|
/**
|
||||||
* Hide a view by setting its layout height and width to 1dp.
|
* Hide a view by setting its layout height and width to 1dp.
|
||||||
*
|
*
|
||||||
* @param condition The setting to check for hiding the view.
|
* @param setting The setting to check for hiding the view.
|
||||||
* @param view The view to hide.
|
* @param view The view to hide.
|
||||||
*/
|
*/
|
||||||
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
|
public static void hideViewBy0dpUnderCondition(BooleanSetting setting, View view) {
|
||||||
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
|
if (hideViewBy0dpUnderCondition(setting.get(), view)) {
|
||||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
Logger.printDebug(() -> "View hidden by setting: " + setting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,7 +172,7 @@ public class Utils {
|
||||||
*/
|
*/
|
||||||
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
|
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
|
||||||
if (condition) {
|
if (condition) {
|
||||||
hideViewByLayoutParams(view);
|
hideViewBy0dp(view);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,19 +180,44 @@ public class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide a view by setting its visibility to GONE.
|
* Hide a view by setting its layout params to 0x0
|
||||||
*
|
* @param view The view to hide.
|
||||||
* @param condition The setting to check for hiding the view.
|
|
||||||
* @param view The view to hide.
|
|
||||||
*/
|
*/
|
||||||
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
|
public static void hideViewBy0dp(View view) {
|
||||||
if (hideViewUnderCondition(condition.get(), view)) {
|
if (view instanceof LinearLayout) {
|
||||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
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.
|
* Hide a view by setting its visibility as GONE.
|
||||||
|
*
|
||||||
|
* @param setting 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide a view by setting its visibility as GONE.
|
||||||
*
|
*
|
||||||
* @param condition The setting to check for hiding the view.
|
* @param condition The setting to check for hiding the view.
|
||||||
* @param view The view to hide.
|
* @param view The view to hide.
|
||||||
|
|
@ -199,14 +231,14 @@ public class Utils {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) {
|
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting setting, View view) {
|
||||||
if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) {
|
if (hideViewByRemovingFromParentUnderCondition(setting.get(), view)) {
|
||||||
Logger.printDebug(() -> "View hidden by setting: " + condition);
|
Logger.printDebug(() -> "View hidden by setting: " + setting);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
|
public static boolean hideViewByRemovingFromParentUnderCondition(boolean condition, View view) {
|
||||||
if (setting) {
|
if (condition) {
|
||||||
ViewParent parent = view.getParent();
|
ViewParent parent = view.getParent();
|
||||||
if (parent instanceof ViewGroup parentGroup) {
|
if (parent instanceof ViewGroup parentGroup) {
|
||||||
parentGroup.removeView(view);
|
parentGroup.removeView(view);
|
||||||
|
|
@ -255,7 +287,7 @@ public class Utils {
|
||||||
// Could do a thread sleep, but that will trigger an exception if the thread is interrupted.
|
// Could do a thread sleep, but that will trigger an exception if the thread is interrupted.
|
||||||
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
|
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
|
||||||
}
|
}
|
||||||
// Return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
|
// Return the value, otherwise the compiler or VM might optimize and remove the meaningless time-wasting work,
|
||||||
// leaving an empty loop that hammers on the System.currentTimeMillis native call.
|
// leaving an empty loop that hammers on the System.currentTimeMillis native call.
|
||||||
return meaninglessValue;
|
return meaninglessValue;
|
||||||
}
|
}
|
||||||
|
|
@ -265,10 +297,12 @@ public class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int indexOfFirstFound(String value, String... targets) {
|
public static int indexOfFirstFound(String value, String... targets) {
|
||||||
for (String string : targets) {
|
if (isNotEmpty(value)) {
|
||||||
if (!string.isEmpty()) {
|
for (String string : targets) {
|
||||||
final int indexOf = value.indexOf(string);
|
if (!string.isEmpty()) {
|
||||||
if (indexOf >= 0) return indexOf;
|
final int indexOf = value.indexOf(string);
|
||||||
|
if (indexOf >= 0) return indexOf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -278,12 +312,13 @@ public class Utils {
|
||||||
* @return zero, if the resource is not found.
|
* @return zero, if the resource is not found.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String type) {
|
public static int getResourceIdentifier(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
|
||||||
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
return context.getResources().getIdentifier(resourceIdentifierName,
|
||||||
|
type == null ? null : type.value, context.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) {
|
public static int getResourceIdentifierOrThrow(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
|
||||||
final int resourceId = getResourceIdentifier(context, resourceIdentifierName, type);
|
final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName);
|
||||||
if (resourceId == 0) {
|
if (resourceId == 0) {
|
||||||
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
||||||
+ " type: " + type);
|
+ " type: " + type);
|
||||||
|
|
@ -293,22 +328,18 @@ public class Utils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return zero, if the resource is not found.
|
* @return zero, if the resource is not found.
|
||||||
* @see #getResourceIdentifierOrThrow(String, String)
|
* @see #getResourceIdentifierOrThrow(ResourceType, String)
|
||||||
*/
|
*/
|
||||||
public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String type) {
|
public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) {
|
||||||
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
return getResourceIdentifier(getContext(), type, resourceIdentifierName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The resource identifier, or throws an exception if not found.
|
* @return zero, if the resource is not found.
|
||||||
|
* @see #getResourceIdentifier(ResourceType, String)
|
||||||
*/
|
*/
|
||||||
public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String type) {
|
public static int getResourceIdentifierOrThrow(@Nullable ResourceType type, String resourceIdentifierName) {
|
||||||
final int resourceId = getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
return getResourceIdentifierOrThrow(getContext(), type, resourceIdentifierName);
|
||||||
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 {
|
public static String getResourceString(int id) throws Resources.NotFoundException {
|
||||||
|
|
@ -316,29 +347,29 @@ public class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
|
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(resourceIdentifierName, "anim"));
|
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(ResourceType.ANIM, resourceIdentifierName));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
return getContext().getResources().getColor(getResourceIdentifierOrThrow(resourceIdentifierName, "color"));
|
return getContext().getResources().getColor(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(resourceIdentifierName, "array"));
|
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName));
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface MatchFilter<T> {
|
public interface MatchFilter<T> {
|
||||||
|
|
@ -349,7 +380,7 @@ public class Utils {
|
||||||
* Includes sub children.
|
* Includes sub children.
|
||||||
*/
|
*/
|
||||||
public static <R extends View> R getChildViewByResourceName(View view, String str) {
|
public static <R extends View> R getChildViewByResourceName(View view, String str) {
|
||||||
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(str, "id"));
|
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str));
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
return (R) child;
|
return (R) child;
|
||||||
}
|
}
|
||||||
|
|
@ -443,6 +474,10 @@ public class Utils {
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isNotEmpty(@Nullable String str) {
|
||||||
|
return str != null && !str.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isTablet() {
|
public static boolean isTablet() {
|
||||||
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
|
||||||
}
|
}
|
||||||
|
|
@ -451,7 +486,7 @@ public class Utils {
|
||||||
private static Boolean isRightToLeftTextLayout;
|
private static Boolean isRightToLeftTextLayout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc).
|
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc.).
|
||||||
* If this should match any ReVanced language override then instead use
|
* If this should match any ReVanced language override then instead use
|
||||||
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
|
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
|
||||||
* This is the default locale of the device, which may differ if
|
* This is the default locale of the device, which may differ if
|
||||||
|
|
@ -465,7 +500,7 @@ public class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc).
|
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc.).
|
||||||
*/
|
*/
|
||||||
public static boolean isRightToLeftLocale(Locale locale) {
|
public static boolean isRightToLeftLocale(Locale locale) {
|
||||||
String displayLanguage = locale.getDisplayLanguage();
|
String displayLanguage = locale.getDisplayLanguage();
|
||||||
|
|
@ -494,7 +529,7 @@ public class Utils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the text contains at least 1 number character,
|
* @return if the text contains at least 1 number character,
|
||||||
* including any unicode numbers such as Arabic.
|
* including any Unicode numbers such as Arabic.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
public static boolean containsNumber(CharSequence text) {
|
public static boolean containsNumber(CharSequence text) {
|
||||||
|
|
@ -806,6 +841,21 @@ public class Utils {
|
||||||
window.setBackgroundDrawable(null); // Remove default dialog background
|
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.
|
* Sets the theme light color used by the app.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1111,7 +1161,7 @@ public class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
|
* Uses {@link #adjustColorBrightness(int, float)} depending on if light or dark mode is active.
|
||||||
*/
|
*/
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
|
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
|
||||||
|
|
@ -1167,4 +1217,18 @@ public class Utils {
|
||||||
public static float clamp(float value, float lower, float upper) {
|
public static float clamp(float value, float lower, float upper) {
|
||||||
return Math.max(lower, Math.min(value, 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
|
@ -19,14 +19,17 @@ import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
abstract class Check {
|
abstract class Check {
|
||||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||||
|
|
||||||
|
|
@ -75,7 +78,6 @@ abstract class Check {
|
||||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
|
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
|
||||||
final var reasons = new StringBuilder();
|
final var reasons = new StringBuilder();
|
||||||
|
|
||||||
|
|
@ -128,7 +130,7 @@ abstract class Check {
|
||||||
// Add icon to the dialog.
|
// Add icon to the dialog.
|
||||||
ImageView iconView = new ImageView(activity);
|
ImageView iconView = new ImageView(activity);
|
||||||
iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
|
iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
|
||||||
"revanced_ic_dialog_alert", "drawable"));
|
ResourceType.DRAWABLE, "revanced_ic_dialog_alert"));
|
||||||
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||||
iconView.setPadding(0, 0, 0, 0);
|
iconView.setPadding(0, 0, 0, 0);
|
||||||
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,12 @@ import android.content.pm.PackageInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
|
@ -27,6 +31,7 @@ import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
|
||||||
* <br>
|
* <br>
|
||||||
* Various indicators help to detect if the app was patched by the user.
|
* Various indicators help to detect if the app was patched by the user.
|
||||||
*/
|
*/
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class CheckEnvironmentPatch {
|
public final class CheckEnvironmentPatch {
|
||||||
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
|
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
|
||||||
|
|
@ -39,6 +44,7 @@ public final class CheckEnvironmentPatch {
|
||||||
ADB((String) null),
|
ADB((String) null),
|
||||||
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
|
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
|
||||||
MANAGER("app.revanced.manager.flutter",
|
MANAGER("app.revanced.manager.flutter",
|
||||||
|
"app.revanced.manager.flutter.debug",
|
||||||
"app.revanced.manager",
|
"app.revanced.manager",
|
||||||
"app.revanced.manager.debug");
|
"app.revanced.manager.debug");
|
||||||
|
|
||||||
|
|
@ -118,7 +124,7 @@ public final class CheckEnvironmentPatch {
|
||||||
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
|
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
|
||||||
*/
|
*/
|
||||||
private static class CheckWasPatchedOnSameDevice extends Check {
|
private static class CheckWasPatchedOnSameDevice extends Check {
|
||||||
@SuppressLint({"NewApi", "HardwareIds"})
|
@SuppressLint("HardwareIds")
|
||||||
@Override
|
@Override
|
||||||
protected Boolean check() {
|
protected Boolean check() {
|
||||||
if (PATCH_BOARD.isEmpty()) {
|
if (PATCH_BOARD.isEmpty()) {
|
||||||
|
|
@ -192,7 +198,7 @@ public final class CheckEnvironmentPatch {
|
||||||
PackageManager packageManager = context.getPackageManager();
|
PackageManager packageManager = context.getPackageManager();
|
||||||
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
|
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
|
||||||
|
|
||||||
// Duration since initial install or last update, which ever is sooner.
|
// Duration since initial install or last update, whichever is sooner.
|
||||||
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
|
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
|
||||||
Logger.printInfo(() -> "App was installed/updated: "
|
Logger.printInfo(() -> "App was installed/updated: "
|
||||||
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
|
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
|
||||||
|
|
@ -288,8 +294,8 @@ public final class CheckEnvironmentPatch {
|
||||||
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
|
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
|
||||||
Boolean timeCheckPassed = nearPatchTime.check();
|
Boolean timeCheckPassed = nearPatchTime.check();
|
||||||
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
|
||||||
// Allow installing recently patched apks,
|
// Allow installing recently patched APKs,
|
||||||
// even if the install source is not Manager or ADB.
|
// even if the installation source is not Manager or ADB.
|
||||||
Check.disableForever();
|
Check.disableForever();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.ResponseBody;
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
|
||||||
public abstract class BaseFixRedgifsApiPatch implements Interceptor {
|
public abstract class BaseFixRedgifsApiPatch implements Interceptor {
|
||||||
protected static BaseFixRedgifsApiPatch INSTANCE;
|
protected static BaseFixRedgifsApiPatch INSTANCE;
|
||||||
public abstract String getDefaultUserAgent();
|
public abstract String getDefaultUserAgent();
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,15 @@ import android.content.pm.PackageManager;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import app.revanced.extension.shared.GmsCoreSupport;
|
import app.revanced.extension.shared.GmsCoreSupport;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
|
@ -52,24 +55,35 @@ public class CustomBrandingPatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int notificationSmallIcon;
|
@Nullable
|
||||||
|
private static Integer notificationSmallIcon;
|
||||||
|
|
||||||
static {
|
private static int getNotificationSmallIcon() {
|
||||||
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
// Cannot use static initialization block otherwise cyclic references exist
|
||||||
if (branding == BrandingTheme.ORIGINAL) {
|
// between Settings initialization and this class.
|
||||||
notificationSmallIcon = 0;
|
if (notificationSmallIcon == null) {
|
||||||
} else {
|
if (GmsCoreSupport.isPackageNameOriginal()) {
|
||||||
// Original icon is quantum_ic_video_youtube_white_24
|
Logger.printDebug(() -> "App is root mounted. Not overriding small notification icon");
|
||||||
String iconName = "revanced_notification_icon";
|
return notificationSmallIcon = 0;
|
||||||
if (branding == BrandingTheme.CUSTOM) {
|
|
||||||
iconName += "_custom";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationSmallIcon = Utils.getResourceIdentifier(iconName, "drawable");
|
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
|
||||||
if (notificationSmallIcon == 0) {
|
if (branding == BrandingTheme.ORIGINAL) {
|
||||||
Logger.printException(() -> "Could not load notification small icon");
|
notificationSmallIcon = 0;
|
||||||
|
} else {
|
||||||
|
// Original icon is quantum_ic_video_youtube_white_24
|
||||||
|
String iconName = "revanced_notification_icon";
|
||||||
|
if (branding == BrandingTheme.CUSTOM) {
|
||||||
|
iconName += "_custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationSmallIcon = Utils.getResourceIdentifier(ResourceType.DRAWABLE, iconName);
|
||||||
|
if (notificationSmallIcon == 0) {
|
||||||
|
Logger.printException(() -> "Could not load notification small icon");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return notificationSmallIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,8 +102,9 @@ public class CustomBrandingPatch {
|
||||||
*/
|
*/
|
||||||
public static void setNotificationIcon(Notification.Builder builder) {
|
public static void setNotificationIcon(Notification.Builder builder) {
|
||||||
try {
|
try {
|
||||||
if (notificationSmallIcon != 0) {
|
final int smallIcon = getNotificationSmallIcon();
|
||||||
builder.setSmallIcon(notificationSmallIcon)
|
if (smallIcon != 0) {
|
||||||
|
builder.setSmallIcon(smallIcon)
|
||||||
.setColor(Color.TRANSPARENT); // Remove YT red tint.
|
.setColor(Color.TRANSPARENT); // Remove YT red tint.
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|
@ -103,8 +118,41 @@ public class CustomBrandingPatch {
|
||||||
* The total number of app name aliases, including dummy aliases.
|
* The total number of app name aliases, including dummy aliases.
|
||||||
*/
|
*/
|
||||||
private static int numberOfPresetAppNames() {
|
private static int numberOfPresetAppNames() {
|
||||||
// Modified during patching.
|
// Modified during patching, but requires a default if custom branding is excluded.
|
||||||
throw new IllegalStateException();
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* If a custom icon was provided during patching.
|
||||||
|
*/
|
||||||
|
private static boolean userProvidedCustomIcon() {
|
||||||
|
// Modified during patching, but requires a default if custom branding is excluded.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* If a custom name was provided during patching.
|
||||||
|
*/
|
||||||
|
private static boolean userProvidedCustomName() {
|
||||||
|
// Modified during patching, but requires a default if custom branding is excluded..
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDefaultAppNameIndex() {
|
||||||
|
return userProvidedCustomName()
|
||||||
|
? numberOfPresetAppNames()
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BrandingTheme getDefaultIconStyle() {
|
||||||
|
return userProvidedCustomIcon()
|
||||||
|
? BrandingTheme.CUSTOM
|
||||||
|
: BrandingTheme.ORIGINAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -41,12 +41,13 @@ public final class EnableDebuggingPatch {
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) {
|
public static boolean isBooleanFeatureFlagEnabled(boolean value, long flag) {
|
||||||
if (LOG_FEATURE_FLAGS && value) {
|
if (LOG_FEATURE_FLAGS && value) {
|
||||||
if (DISABLED_FEATURE_FLAGS.contains(flag)) {
|
Long flagObj = flag;
|
||||||
|
if (DISABLED_FEATURE_FLAGS.contains(flagObj)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (featureFlags.putIfAbsent(flag, TRUE) == null) {
|
if (featureFlags.putIfAbsent(flagObj, TRUE) == null) {
|
||||||
Logger.printDebug(() -> "boolean feature is enabled: " + flag);
|
Logger.printDebug(() -> "boolean feature is enabled: " + flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,6 +60,8 @@ public final class EnableDebuggingPatch {
|
||||||
*/
|
*/
|
||||||
public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) {
|
public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) {
|
||||||
if (LOG_FEATURE_FLAGS && defaultValue != value) {
|
if (LOG_FEATURE_FLAGS && defaultValue != value) {
|
||||||
|
if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
|
||||||
|
|
||||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
if (featureFlags.putIfAbsent(flag, true) == null) {
|
||||||
// Align the log outputs to make post processing easier.
|
// Align the log outputs to make post processing easier.
|
||||||
Logger.printDebug(() -> " double feature is enabled: " + flag
|
Logger.printDebug(() -> " double feature is enabled: " + flag
|
||||||
|
|
@ -74,6 +77,8 @@ public final class EnableDebuggingPatch {
|
||||||
*/
|
*/
|
||||||
public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) {
|
public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) {
|
||||||
if (LOG_FEATURE_FLAGS && defaultValue != value) {
|
if (LOG_FEATURE_FLAGS && defaultValue != value) {
|
||||||
|
if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
|
||||||
|
|
||||||
if (featureFlags.putIfAbsent(flag, true) == null) {
|
if (featureFlags.putIfAbsent(flag, true) == null) {
|
||||||
Logger.printDebug(() -> " long feature is enabled: " + flag
|
Logger.printDebug(() -> " long feature is enabled: " + flag
|
||||||
+ " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue));
|
+ " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue));
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ public final class SanitizeSharingLinksPatch {
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String sanitize(String url) {
|
public static String sanitize(String url) {
|
||||||
if (BaseSettings.SANITIZE_SHARED_LINKS.get()) {
|
if (BaseSettings.SANITIZE_SHARING_LINKS.get()) {
|
||||||
url = sanitizer.sanitizeUrlString(url);
|
url = sanitizer.sanitizeURLString(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
|
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package app.revanced.extension.shared.patches.components;
|
package app.revanced.extension.shared.patches.litho;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
|
@ -13,11 +13,11 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.ByteTrieSearch;
|
import app.revanced.extension.shared.ByteTrieSearch;
|
||||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
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.
|
* Allows custom filtering using a path and optionally a proto buffer string.
|
||||||
|
|
@ -25,7 +25,7 @@ import app.revanced.extension.shared.patches.litho.Filter;
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class CustomFilter extends Filter {
|
public final class CustomFilter extends Filter {
|
||||||
|
|
||||||
private static void showInvalidSyntaxToast(@NonNull String expression) {
|
private static void showInvalidSyntaxToast(String expression) {
|
||||||
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
|
Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +37,12 @@ public final class CustomFilter extends Filter {
|
||||||
public static final String SYNTAX_STARTS_WITH = "^";
|
public static final String SYNTAX_STARTS_WITH = "^";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional character that separates the path from a proto buffer string pattern.
|
* Optional character that separates the path from an accessibility string pattern.
|
||||||
|
*/
|
||||||
|
public static final String SYNTAX_ACCESSIBILITY_SYMBOL = "#";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional character that separates the path/accessibility from a proto buffer string pattern.
|
||||||
*/
|
*/
|
||||||
public static final String SYNTAX_BUFFER_SYMBOL = "$";
|
public static final String SYNTAX_BUFFER_SYMBOL = "$";
|
||||||
|
|
||||||
|
|
@ -52,15 +57,21 @@ public final class CustomFilter extends Filter {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map key is the path including optional special characters (^ and/or $)
|
// Map key is the full path including optional special characters (^, #, $),
|
||||||
|
// and any accessibility pattern, but does not contain any buffer patterns.
|
||||||
Map<String, CustomFilterGroup> result = new HashMap<>();
|
Map<String, CustomFilterGroup> result = new HashMap<>();
|
||||||
|
|
||||||
Pattern pattern = Pattern.compile(
|
Pattern pattern = Pattern.compile(
|
||||||
"(" // map key group
|
"(" // Map key group.
|
||||||
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with
|
// Optional starts with.
|
||||||
+ "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path
|
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)"
|
||||||
+ "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol
|
// Path string.
|
||||||
+ ")" // end map key group
|
+ "([^\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + SYNTAX_BUFFER_SYMBOL + "\\E]*)"
|
||||||
+ "(.*)"); // optional buffer string
|
// Optional accessibility string.
|
||||||
|
+ "(?:\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + "\\E([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*))?"
|
||||||
|
// Optional buffer string.
|
||||||
|
+ "(?:\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E(.*))?"
|
||||||
|
+ ")"); // end map key group
|
||||||
|
|
||||||
for (String expression : rawCustomFilterText.split("\n")) {
|
for (String expression : rawCustomFilterText.split("\n")) {
|
||||||
if (expression.isBlank()) continue;
|
if (expression.isBlank()) continue;
|
||||||
|
|
@ -74,10 +85,12 @@ public final class CustomFilter extends Filter {
|
||||||
final String mapKey = matcher.group(1);
|
final String mapKey = matcher.group(1);
|
||||||
final boolean pathStartsWith = !matcher.group(2).isEmpty();
|
final boolean pathStartsWith = !matcher.group(2).isEmpty();
|
||||||
final String path = matcher.group(3);
|
final String path = matcher.group(3);
|
||||||
final boolean hasBufferSymbol = !matcher.group(4).isEmpty();
|
final String accessibility = matcher.group(4); // null if not present
|
||||||
final String bufferString = matcher.group(5);
|
final String buffer = matcher.group(5); // null if not present
|
||||||
|
|
||||||
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
|
if (path.isBlank()
|
||||||
|
|| (accessibility != null && accessibility.isEmpty())
|
||||||
|
|| (buffer != null && buffer.isEmpty())) {
|
||||||
showInvalidSyntaxToast(expression);
|
showInvalidSyntaxToast(expression);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -90,8 +103,13 @@ public final class CustomFilter extends Filter {
|
||||||
group = new CustomFilterGroup(pathStartsWith, path);
|
group = new CustomFilterGroup(pathStartsWith, path);
|
||||||
result.put(mapKey, group);
|
result.put(mapKey, group);
|
||||||
}
|
}
|
||||||
if (hasBufferSymbol) {
|
|
||||||
group.addBufferString(bufferString);
|
if (accessibility != null) {
|
||||||
|
group.addAccessibilityString(accessibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buffer != null) {
|
||||||
|
group.addBufferString(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,14 +117,22 @@ public final class CustomFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean startsWith;
|
final boolean startsWith;
|
||||||
|
StringTrieSearch accessibilitySearch;
|
||||||
ByteTrieSearch bufferSearch;
|
ByteTrieSearch bufferSearch;
|
||||||
|
|
||||||
CustomFilterGroup(boolean startsWith, @NonNull String path) {
|
CustomFilterGroup(boolean startsWith, String path) {
|
||||||
super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
|
super(YouTubeAndMusicSettings.CUSTOM_FILTER, path);
|
||||||
this.startsWith = startsWith;
|
this.startsWith = startsWith;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addBufferString(@NonNull String bufferString) {
|
void addAccessibilityString(String accessibilityString) {
|
||||||
|
if (accessibilitySearch == null) {
|
||||||
|
accessibilitySearch = new StringTrieSearch();
|
||||||
|
}
|
||||||
|
accessibilitySearch.addPattern(accessibilityString);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addBufferString(String bufferString) {
|
||||||
if (bufferSearch == null) {
|
if (bufferSearch == null) {
|
||||||
bufferSearch = new ByteTrieSearch();
|
bufferSearch = new ByteTrieSearch();
|
||||||
}
|
}
|
||||||
|
|
@ -118,6 +144,11 @@ public final class CustomFilter extends Filter {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
StringBuilder builder = new StringBuilder();
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append("CustomFilterGroup{");
|
builder.append("CustomFilterGroup{");
|
||||||
|
if (accessibilitySearch != null) {
|
||||||
|
builder.append(", accessibility=");
|
||||||
|
builder.append(accessibilitySearch.getPatterns());
|
||||||
|
}
|
||||||
|
|
||||||
builder.append("path=");
|
builder.append("path=");
|
||||||
if (startsWith) builder.append(SYNTAX_STARTS_WITH);
|
if (startsWith) builder.append(SYNTAX_STARTS_WITH);
|
||||||
builder.append(filters[0]);
|
builder.append(filters[0]);
|
||||||
|
|
@ -147,18 +178,26 @@ public final class CustomFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
// All callbacks are custom filter groups.
|
// All callbacks are custom filter groups.
|
||||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||||
|
|
||||||
|
// Check path start requirement.
|
||||||
if (custom.startsWith && contentIndex != 0) {
|
if (custom.startsWith && contentIndex != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (custom.bufferSearch == null) {
|
// Check accessibility string if specified.
|
||||||
return true; // No buffer filter, only path filtering.
|
if (custom.accessibilitySearch != null && !custom.accessibilitySearch.matches(accessibility)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return custom.bufferSearch.matches(buffer);
|
// Check buffer if specified.
|
||||||
|
if (custom.bufferSearch != null && !custom.bufferSearch.matches(buffer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // All custom filter conditions passed.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
package app.revanced.extension.shared.patches.litho;
|
package app.revanced.extension.shared.patches.litho;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||||
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
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.
|
* Filters litho based components.
|
||||||
*
|
*
|
||||||
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
|
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
|
||||||
* and {@link #addPathCallbacks(StringFilterGroup...)}.
|
* and {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||||
*
|
*
|
||||||
* To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
|
* To filter {@link FilterContentType#PROTOBUFFER} or {@link FilterContentType#ACCESSIBILITY}, first add a callback to
|
||||||
* either an identifier or a path.
|
* either an identifier or a path.
|
||||||
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
* Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||||
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
|
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
|
||||||
* or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
* or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern).
|
||||||
*
|
*
|
||||||
|
|
@ -26,6 +26,7 @@ public abstract class Filter {
|
||||||
public enum FilterContentType {
|
public enum FilterContentType {
|
||||||
IDENTIFIER,
|
IDENTIFIER,
|
||||||
PATH,
|
PATH,
|
||||||
|
ACCESSIBILITY,
|
||||||
PROTOBUFFER
|
PROTOBUFFER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,15 +34,15 @@ public abstract class Filter {
|
||||||
* Identifier callbacks. Do not add to this instance,
|
* Identifier callbacks. Do not add to this instance,
|
||||||
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
|
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
|
||||||
*/
|
*/
|
||||||
protected final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
|
public final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
|
||||||
/**
|
/**
|
||||||
* Path callbacks. Do not add to this instance,
|
* Path callbacks. Do not add to this instance,
|
||||||
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
|
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
|
||||||
*/
|
*/
|
||||||
protected final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
|
public final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
* Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||||
* if any of the groups are found.
|
* if any of the groups are found.
|
||||||
*/
|
*/
|
||||||
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
|
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
|
||||||
|
|
@ -49,7 +50,7 @@ public abstract class Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
* Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
|
||||||
* if any of the groups are found.
|
* if any of the groups are found.
|
||||||
*/
|
*/
|
||||||
protected final void addPathCallbacks(StringFilterGroup... groups) {
|
protected final void addPathCallbacks(StringFilterGroup... groups) {
|
||||||
|
|
@ -63,12 +64,15 @@ public abstract class Filter {
|
||||||
* <p>
|
* <p>
|
||||||
* Method is called off the main thread.
|
* Method is called off the main thread.
|
||||||
*
|
*
|
||||||
|
* @param identifier Litho identifier.
|
||||||
|
* @param accessibility Accessibility string, or an empty string if not present for the component.
|
||||||
|
* @param buffer Protocol buffer.
|
||||||
* @param matchedGroup The actual filter that matched.
|
* @param matchedGroup The actual filter that matched.
|
||||||
* @param contentType The type of content matched.
|
* @param contentType The type of content matched.
|
||||||
* @param contentIndex Matched index of the identifier or path.
|
* @param contentIndex Matched index of the identifier or path.
|
||||||
* @return True if the litho component should be filtered out.
|
* @return True if the litho component should be filtered out.
|
||||||
*/
|
*/
|
||||||
public boolean isFiltered(String identifier, String path, byte[] buffer,
|
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ public abstract class FilterGroup<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final BooleanSetting setting;
|
protected final BooleanSetting setting;
|
||||||
protected final T[] filters;
|
public final T[] filters;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a new filter group.
|
* Initialize a new filter group.
|
||||||
|
|
@ -122,7 +122,7 @@ public abstract class FilterGroup<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If you have more than 1 filter patterns, then all instances of
|
* If you have more than 1 filter patterns, then all instances of
|
||||||
* this class should filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
|
* this class should be filtered using {@link FilterGroupList.ByteArrayFilterGroupList#check(byte[])},
|
||||||
* which uses a prefix tree to give better performance.
|
* which uses a prefix tree to give better performance.
|
||||||
*/
|
*/
|
||||||
public static class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
public static class ByteArrayFilterGroup extends FilterGroup<byte[]> {
|
||||||
|
|
@ -149,7 +149,7 @@ public abstract class FilterGroup<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int[] createFailurePattern(byte[] pattern) {
|
private static int[] createFailurePattern(byte[] pattern) {
|
||||||
// Computes the failure function using a boot-strapping process,
|
// Computes the failure function using a bootstrapping process,
|
||||||
// where the pattern is matched against itself.
|
// where the pattern is matched against itself.
|
||||||
final int patternLength = pattern.length;
|
final int patternLength = pattern.length;
|
||||||
final int[] failure = new int[patternLength];
|
final int[] failure = new int[patternLength];
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
package app.revanced.extension.shared.patches.litho;
|
package app.revanced.extension.shared.patches.litho;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.ByteTrieSearch;
|
import app.revanced.extension.shared.ByteTrieSearch;
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
import app.revanced.extension.shared.TrieSearch;
|
import app.revanced.extension.shared.TrieSearch;
|
||||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public 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 List<T> filterGroups = new ArrayList<>();
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,17 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.StringTrieSearch;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.StringTrieSearch;
|
||||||
|
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class LithoFilterPatch {
|
public final class LithoFilterPatch {
|
||||||
|
|
@ -21,11 +24,14 @@ public final class LithoFilterPatch {
|
||||||
private static final class LithoFilterParameters {
|
private static final class LithoFilterParameters {
|
||||||
final String identifier;
|
final String identifier;
|
||||||
final String path;
|
final String path;
|
||||||
|
final String accessibility;
|
||||||
final byte[] buffer;
|
final byte[] buffer;
|
||||||
|
|
||||||
LithoFilterParameters(String lithoIdentifier, String lithoPath, byte[] buffer) {
|
LithoFilterParameters(String lithoIdentifier, String lithoPath,
|
||||||
|
String accessibility, byte[] buffer) {
|
||||||
this.identifier = lithoIdentifier;
|
this.identifier = lithoIdentifier;
|
||||||
this.path = lithoPath;
|
this.path = lithoPath;
|
||||||
|
this.accessibility = accessibility;
|
||||||
this.buffer = buffer;
|
this.buffer = buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,11 +40,16 @@ public final class LithoFilterPatch {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
// Estimate the percentage of the buffer that are Strings.
|
// Estimate the percentage of the buffer that are Strings.
|
||||||
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
|
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
|
||||||
builder.append( "ID: ");
|
builder.append("ID: ");
|
||||||
builder.append(identifier);
|
builder.append(identifier);
|
||||||
|
if (!accessibility.isEmpty()) {
|
||||||
|
// AccessibilityId and AccessibilityText are pieces of BufferStrings.
|
||||||
|
builder.append(" Accessibility: ");
|
||||||
|
builder.append(accessibility);
|
||||||
|
}
|
||||||
builder.append(" Path: ");
|
builder.append(" Path: ");
|
||||||
builder.append(path);
|
builder.append(path);
|
||||||
if (YouTubeAndMusicSettings.DEBUG_PROTOBUFFER.get()) {
|
if (YouTubeAndMusicSettings.DEBUG_PROTOCOLBUFFER.get()) {
|
||||||
builder.append(" BufferStrings: ");
|
builder.append(" BufferStrings: ");
|
||||||
findAsciiStrings(builder, buffer);
|
findAsciiStrings(builder, buffer);
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +86,16 @@ 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.
|
* Litho layout fixed thread pool size override.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -92,26 +113,57 @@ public final class LithoFilterPatch {
|
||||||
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
|
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder for actual filters.
|
* For YouTube 20.22+, this is set to true by a patch,
|
||||||
|
* because it cannot use the thread buffer due to the buffer frequently not being 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.
|
||||||
|
* <p>
|
||||||
|
* <b>This is set during patching, do not change manually.</b>
|
||||||
*/
|
*/
|
||||||
private static final class DummyFilter extends Filter { }
|
private static final boolean EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||||
|
|
||||||
private static final Filter[] filters = new Filter[] {
|
/**
|
||||||
new DummyFilter() // Replaced patching, do not touch.
|
* Turns on additional logging, used for development purposes only.
|
||||||
};
|
*/
|
||||||
|
public static final boolean DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String suffix for components.
|
||||||
|
* Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b"
|
||||||
|
*/
|
||||||
|
private static final byte[] LITHO_COMPONENT_EXTENSION_BYTES = ".e".getBytes(StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as placeholder for litho id/path filters that do not use a buffer
|
||||||
|
*/
|
||||||
|
private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Because litho filtering is multithreaded 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 multithreaded 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 pathSearchTree = new StringTrieSearch();
|
||||||
private static final StringTrieSearch identifierSearchTree = 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.
|
|
||||||
*/
|
|
||||||
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
|
||||||
for (Filter filter : filters) {
|
for (Filter filter : filters) {
|
||||||
filterUsingCallbacks(identifierSearchTree, filter,
|
filterUsingCallbacks(identifierSearchTree, filter,
|
||||||
filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
|
filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
|
||||||
|
|
@ -143,16 +195,13 @@ public final class LithoFilterPatch {
|
||||||
|
|
||||||
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
||||||
final boolean isFiltered = filter.isFiltered(parameters.identifier,
|
final boolean isFiltered = filter.isFiltered(parameters.identifier,
|
||||||
parameters.path, parameters.buffer, group, type, matchedStartIndex);
|
parameters.accessibility, parameters.path, parameters.buffer,
|
||||||
|
group, type, matchedStartIndex);
|
||||||
|
|
||||||
if (isFiltered && BaseSettings.DEBUG.get()) {
|
if (isFiltered && BaseSettings.DEBUG.get()) {
|
||||||
if (type == Filter.FilterContentType.IDENTIFIER) {
|
Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER
|
||||||
Logger.printDebug(() -> "Filtered " + filterSimpleName
|
? filterSimpleName + " filtered identifier: " + parameters.identifier
|
||||||
+ " identifier: " + parameters.identifier);
|
: filterSimpleName + " filtered path: " + parameters.path);
|
||||||
} else {
|
|
||||||
Logger.printDebug(() -> "Filtered " + filterSimpleName
|
|
||||||
+ " path: " + parameters.path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return isFiltered;
|
return isFiltered;
|
||||||
|
|
@ -162,16 +211,119 @@ 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.
|
* Injection point. Called off the main thread.
|
||||||
* Targets 20.22+
|
* Targets 20.22+
|
||||||
*/
|
*/
|
||||||
public static void setProtoBuffer(byte[] buffer) {
|
public static void setProtoBuffer(byte[] buffer) {
|
||||||
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||||
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
|
StringBuilder builder = new StringBuilder();
|
||||||
// The buffer will be cleared from memory after a new buffer is set by the same thread,
|
LithoFilterParameters.findAsciiStrings(builder, buffer);
|
||||||
// or when the calling thread eventually dies.
|
Logger.printDebug(() -> "New buffer: " + builder);
|
||||||
bufferThreadLocal.set(buffer);
|
}
|
||||||
|
|
||||||
|
// The identifier always seems to start very close to the buffer start.
|
||||||
|
// Highest identifier start index ever observed is 50, with most around 30 to 40.
|
||||||
|
// The buffer can be very large with up to 200kb has been observed,
|
||||||
|
// so the search is restricted to only the start.
|
||||||
|
final int maxBufferStartIndex = 500; // 10x expected upper bound.
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength);
|
||||||
|
for (int i = 0; i < lastBufferIndexToCheckFrom; 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.
|
||||||
|
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||||
|
Logger.printDebug(() -> "Could not find eml index");
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (BaseSettings.DEBUG.get()) {
|
||||||
|
Logger.printException(() -> "Debug: 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -179,46 +331,81 @@ public final class LithoFilterPatch {
|
||||||
* Targets 20.21 and lower.
|
* Targets 20.21 and lower.
|
||||||
*/
|
*/
|
||||||
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
|
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()) {
|
if (buffer == null || !buffer.hasArray()) {
|
||||||
// It appears the buffer can be cleared out just before the call to #filter()
|
// 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.
|
// Ignore this null value and retain the last buffer that was set.
|
||||||
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
|
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
|
||||||
} else {
|
} else {
|
||||||
setProtoBuffer(buffer.array());
|
// 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean isFiltered(String lithoIdentifier, StringBuilder pathBuilder) {
|
public static boolean isFiltered(String identifier, @Nullable String accessibilityId,
|
||||||
|
@Nullable String accessibilityText, StringBuilder pathBuilder) {
|
||||||
try {
|
try {
|
||||||
if (lithoIdentifier.isEmpty() && pathBuilder.length() == 0) {
|
if (identifier.isEmpty() || pathBuilder.length() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] buffer = bufferThreadLocal.get();
|
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.
|
||||||
|
if (BaseSettings.DEBUG.get()) {
|
||||||
|
Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer = bufferThreadLocal.get();
|
||||||
|
}
|
||||||
|
|
||||||
// Potentially the buffer may have been null or never set up until now.
|
// Potentially the buffer may have been null or never set up until now.
|
||||||
// Use an empty buffer so the litho id/path filters still work correctly.
|
// Use an empty buffer so the litho id/path filters that do not use a buffer still work.
|
||||||
if (buffer == null) {
|
if (buffer == null) {
|
||||||
buffer = EMPTY_BYTE_ARRAY;
|
buffer = EMPTY_BYTE_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
LithoFilterParameters parameter = new LithoFilterParameters(
|
String path = pathBuilder.toString();
|
||||||
lithoIdentifier, pathBuilder.toString(), buffer);
|
|
||||||
|
String accessibility = "";
|
||||||
|
if (accessibilityId != null && !accessibilityId.isBlank()) {
|
||||||
|
accessibility = accessibilityId;
|
||||||
|
}
|
||||||
|
if (accessibilityText != null && !accessibilityText.isBlank()) {
|
||||||
|
accessibility = accessibilityId + '|' + accessibilityText;
|
||||||
|
}
|
||||||
|
LithoFilterParameters parameter = new LithoFilterParameters(identifier, path, accessibility, buffer);
|
||||||
Logger.printDebug(() -> "Searching " + parameter);
|
Logger.printDebug(() -> "Searching " + parameter);
|
||||||
|
|
||||||
if (identifierSearchTree.matches(parameter.identifier, parameter)) {
|
return identifierSearchTree.matches(identifier, parameter)
|
||||||
return true;
|
|| pathSearchTree.matches(path, parameter);
|
||||||
}
|
|
||||||
|
|
||||||
if (pathSearchTree.matches(parameter.path, parameter)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "isFiltered failure", ex);
|
Logger.printException(() -> "isFiltered failure", ex);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,23 +24,23 @@ public class LinkSanitizer {
|
||||||
: List.of(parametersToRemove);
|
: List.of(parametersToRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String sanitizeUrlString(String url) {
|
public String sanitizeURLString(String url) {
|
||||||
try {
|
try {
|
||||||
return sanitizeUri(Uri.parse(url)).toString();
|
return sanitizeURI(Uri.parse(url)).toString();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "sanitizeUrlString failure: " + url, ex);
|
Logger.printException(() -> "sanitizeURLString failure: " + url, ex);
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Uri sanitizeUri(Uri uri) {
|
public Uri sanitizeURI(Uri uri) {
|
||||||
try {
|
try {
|
||||||
String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
|
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
|
||||||
// Opening YouTube share sheet 'other' option passes the video title as a URI.
|
// Opening YouTube share sheet 'other' option passes the video title as a URI.
|
||||||
// Checking !uri.isHierarchical() works for all cases, except if the
|
// Checking !uri.isHierarchical() works for all cases, except if the
|
||||||
// video title starts with / and then it's hierarchical but still an invalid URI.
|
// video title starts with / and then it's hierarchical but still an invalid URI.
|
||||||
Logger.printDebug(() -> "Ignoring uri: " + uri);
|
Logger.printDebug(() -> "Ignoring URI: " + uri);
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,12 +56,12 @@ public class LinkSanitizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri sanitizedUrl = builder.build();
|
Uri sanitizedURL = builder.build();
|
||||||
Logger.printInfo(() -> "Sanitized url: " + uri + " to: " + sanitizedUrl);
|
Logger.printInfo(() -> "Sanitized URL: " + uri + " to: " + sanitizedURL);
|
||||||
|
|
||||||
return sanitizedUrl;
|
return sanitizedURL;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "sanitizeUri failure: " + uri, ex);
|
Logger.printException(() -> "sanitizeURI failure: " + uri, ex);
|
||||||
return uri;
|
return uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ public class Requester {
|
||||||
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
|
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
|
||||||
String url = apiUrl + route.getCompiledRoute();
|
String url = apiUrl + route.getCompiledRoute();
|
||||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||||
// Request data is in the URL parameters and no body is sent.
|
// This request sends data via URL query parameters. No request body is included.
|
||||||
// The calling code must set a length if using a request body.
|
// If a request body is added, the caller must set the appropriate Content-Length header.
|
||||||
connection.setFixedLengthStreamingMode(0);
|
connection.setFixedLengthStreamingMode(0);
|
||||||
connection.setRequestMethod(route.getMethod().name());
|
connection.setRequestMethod(route.getMethod().name());
|
||||||
String agentString = System.getProperty("http.agent")
|
String agentString = System.getProperty("http.agent")
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,17 @@ import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
@ -21,17 +25,18 @@ import app.revanced.extension.shared.ui.Dim;
|
||||||
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
* Base class for hooking activities to inject a custom PreferenceFragment with a toolbar.
|
||||||
* Provides common logic for initializing the activity and setting up the toolbar.
|
* Provides common logic for initializing the activity and setting up the toolbar.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
@SuppressWarnings("deprecation")
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
public abstract class BaseActivityHook extends Activity {
|
public abstract class BaseActivityHook extends Activity {
|
||||||
|
|
||||||
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
|
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
|
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_settings_fragments");
|
||||||
private static final int ID_REVANCED_TOOLBAR_PARENT =
|
private static final int ID_REVANCED_TOOLBAR_PARENT =
|
||||||
getResourceIdentifierOrThrow("revanced_toolbar_parent", "id");
|
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_toolbar_parent");
|
||||||
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
|
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_with_toolbar", "layout");
|
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_settings_with_toolbar");
|
||||||
private static final int STRING_REVANCED_SETTINGS_TITLE =
|
private static final int STRING_REVANCED_SETTINGS_TITLE =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_title", "string");
|
getResourceIdentifierOrThrow(ResourceType.STRING, "revanced_settings_title");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
||||||
|
|
@ -95,15 +100,15 @@ public abstract class BaseActivityHook extends Activity {
|
||||||
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
||||||
// Replace dummy placeholder toolbar.
|
// Replace dummy placeholder toolbar.
|
||||||
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
||||||
ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
|
ViewGroup toolbarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
|
||||||
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
|
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolbarParent, "revanced_toolbar");
|
||||||
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
||||||
toolBarParent.removeView(dummyToolbar);
|
toolbarParent.removeView(dummyToolbar);
|
||||||
|
|
||||||
// Sets appropriate system navigation bar color for the activity.
|
// Sets appropriate system navigation bar color for the activity.
|
||||||
ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
|
ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
|
||||||
|
|
||||||
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
|
Toolbar toolbar = new Toolbar(toolbarParent.getContext());
|
||||||
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
||||||
toolbar.setNavigationIcon(getNavigationIcon());
|
toolbar.setNavigationIcon(getNavigationIcon());
|
||||||
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
||||||
|
|
@ -120,7 +125,14 @@ public abstract class BaseActivityHook extends Activity {
|
||||||
|
|
||||||
onPostToolbarSetup(activity, toolbar, fragment);
|
onPostToolbarSetup(activity, toolbar, fragment);
|
||||||
|
|
||||||
toolBarParent.addView(toolbar, 0);
|
toolbarParent.addView(toolbar, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID for the content view layout.
|
||||||
|
*/
|
||||||
|
protected int getContentViewResourceId() {
|
||||||
|
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -128,11 +140,6 @@ public abstract class BaseActivityHook extends Activity {
|
||||||
*/
|
*/
|
||||||
protected abstract void customizeActivityTheme(Activity activity);
|
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.
|
* Returns the background color for the toolbar.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import static java.lang.Boolean.TRUE;
|
||||||
import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
|
import static app.revanced.extension.shared.patches.CustomBrandingPatch.BrandingTheme;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.patches.CustomBrandingPatch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings shared across multiple apps.
|
* Settings shared across multiple apps.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -24,10 +27,19 @@ 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.
|
* 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);
|
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 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", "");
|
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);
|
||||||
|
|
||||||
public static final BooleanSetting GMS_CORE_CHECK_UPDATES = new BooleanSetting("revanced_gms_core_check_updates", true, true);
|
public static final BooleanSetting GMS_CORE_CHECK_UPDATES = new BooleanSetting("revanced_gms_core_check_updates", true, true);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
@ -37,13 +49,22 @@ public class BaseSettings {
|
||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|
||||||
public static final BooleanSetting SANITIZE_SHARED_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE);
|
||||||
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE);
|
||||||
|
|
||||||
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
||||||
|
|
||||||
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", BrandingTheme.ORIGINAL, true);
|
public static final EnumSetting<BrandingTheme> CUSTOM_BRANDING_ICON = new EnumSetting<>("revanced_custom_branding_icon", CustomBrandingPatch.getDefaultIconStyle(), true);
|
||||||
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", 1, true);
|
public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("revanced_custom_branding_name", CustomBrandingPatch.getDefaultAppNameIndex(), true);
|
||||||
|
|
||||||
public static final StringSetting DISABLED_FEATURE_FLAGS = new StringSetting("revanced_disabled_feature_flags", "", true, parent(DEBUG));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ public class BooleanSetting extends Setting<Boolean> {
|
||||||
* This method is only to be used by the Settings preference code.
|
* This method is only to be used by the Settings preference code.
|
||||||
*
|
*
|
||||||
* This intentionally is a static method to deter
|
* This intentionally is a static method to deter
|
||||||
* accidental usage when {@link #save(Boolean)} was intnded.
|
* accidental usage when {@link #save(Boolean)} was intended.
|
||||||
*/
|
*/
|
||||||
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
|
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
|
||||||
setting.value = Objects.requireNonNull(newValue);
|
setting.value = Objects.requireNonNull(newValue);
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ public class EnumSetting<T extends Enum<?>> extends Setting<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param enumName Enum name. Casing does not matter.
|
* @param enumName Enum name. Casing does not matter.
|
||||||
* @return Enum of this type with the same declared name.
|
* @return Enum of this type with the same declared name.
|
||||||
* @throws IllegalArgumentException if the name is not a valid enum of this type.
|
* @throws IllegalArgumentException if the name is not a valid enum of this type.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -274,60 +274,6 @@ public abstract class Setting<T> {
|
||||||
load();
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
|
|
||||||
*/
|
|
||||||
public static <T> void migrateOldSettingToNew(Setting<T> oldSetting, Setting<T> newSetting) {
|
|
||||||
if (oldSetting == newSetting) throw new IllegalArgumentException();
|
|
||||||
|
|
||||||
if (!oldSetting.isSetToDefault()) {
|
|
||||||
Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting);
|
|
||||||
newSetting.save(oldSetting.value);
|
|
||||||
oldSetting.resetToDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Migrate an old Setting value previously stored in a different SharedPreference.
|
|
||||||
* <p>
|
|
||||||
* This method will be deleted in the future.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"rawtypes", "NewApi"})
|
|
||||||
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
|
||||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
|
||||||
return; // Nothing to do.
|
|
||||||
}
|
|
||||||
|
|
||||||
Object newValue = setting.get();
|
|
||||||
final Object migratedValue;
|
|
||||||
if (setting instanceof BooleanSetting) {
|
|
||||||
migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue);
|
|
||||||
} else if (setting instanceof IntegerSetting) {
|
|
||||||
migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue);
|
|
||||||
} else if (setting instanceof LongSetting) {
|
|
||||||
migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue);
|
|
||||||
} else if (setting instanceof FloatSetting) {
|
|
||||||
migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue);
|
|
||||||
} else if (setting instanceof StringSetting) {
|
|
||||||
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
|
||||||
} else {
|
|
||||||
Logger.printException(() -> "Unknown setting: " + setting);
|
|
||||||
// Remove otherwise it'll show a toast on every launch.
|
|
||||||
oldPrefs.preferences.edit().remove(settingKey).apply();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting.
|
|
||||||
if (migratedValue.equals(newValue)) {
|
|
||||||
Logger.printDebug(() -> "Value does not need migrating: " + settingKey);
|
|
||||||
return; // Old value is already equal to the new setting value.
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey);
|
|
||||||
//noinspection unchecked
|
|
||||||
setting.save(migratedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets, but does _not_ persistently save the value.
|
* Sets, but does _not_ persistently save the value.
|
||||||
* This method is only to be used by the Settings preference code.
|
* This method is only to be used by the Settings preference code.
|
||||||
|
|
@ -419,7 +365,7 @@ public abstract class Setting<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the currently set value is the same as {@link #defaultValue}
|
* @return if the currently set value is the same as {@link #defaultValue}.
|
||||||
*/
|
*/
|
||||||
public boolean isSetToDefault() {
|
public boolean isSetToDefault() {
|
||||||
return value.equals(defaultValue);
|
return value.equals(defaultValue);
|
||||||
|
|
@ -450,7 +396,7 @@ public abstract class Setting<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
|
* @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
|
||||||
* @return the value stored using the import/export key. Do not set any values in this method.
|
* @return the value stored using the import/export key. Do not set any values in this method.
|
||||||
*/
|
*/
|
||||||
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
|
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@ public class YouTubeAndMusicSettings extends BaseSettings {
|
||||||
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
|
public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER));
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
|
public static final BooleanSetting DEBUG_PROTOCOLBUFFER = new BooleanSetting("revanced_debug_protocolbuffer", FALSE, false,
|
||||||
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
|
"revanced_debug_protocolbuffer_user_dialog_message", parent(BaseSettings.DEBUG));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.annotation.Nullable;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
|
|
@ -103,10 +104,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||||
* so all app specific {@link Setting} instances are loaded before this method returns.
|
* so all app specific {@link Setting} instances are loaded before this method returns.
|
||||||
*/
|
*/
|
||||||
protected void initialize() {
|
protected void initialize() {
|
||||||
String preferenceResourceName = BaseSettings.SHOW_MENU_ICONS.get()
|
String preferenceResourceName;
|
||||||
? "revanced_prefs_icons"
|
if (BaseSettings.SHOW_MENU_ICONS.get()) {
|
||||||
: "revanced_prefs";
|
preferenceResourceName = Utils.appIsUsingBoldIcons()
|
||||||
final var identifier = Utils.getResourceIdentifier(preferenceResourceName, "xml");
|
? "revanced_prefs_icons_bold"
|
||||||
|
: "revanced_prefs_icons";
|
||||||
|
} else {
|
||||||
|
preferenceResourceName = "revanced_prefs";
|
||||||
|
}
|
||||||
|
|
||||||
|
final var identifier = Utils.getResourceIdentifier(ResourceType.XML, preferenceResourceName);
|
||||||
if (identifier == 0) return;
|
if (identifier == 0) return;
|
||||||
addPreferencesFromResource(identifier);
|
addPreferencesFromResource(identifier);
|
||||||
|
|
||||||
|
|
@ -201,7 +208,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||||
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
|
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
|
||||||
boolean syncSettingValue,
|
boolean syncSettingValue,
|
||||||
boolean applySettingToPreference) {
|
boolean applySettingToPreference) {
|
||||||
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
|
// Alternatively this could iterate through all Settings and check for any matching Preferences,
|
||||||
// but there are many more Settings than UI preferences so it's more efficient to only check
|
// but there are many more Settings than UI preferences so it's more efficient to only check
|
||||||
// the Preferences.
|
// the Preferences.
|
||||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import java.util.Locale;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
|
|
@ -81,13 +82,13 @@ public class ColorPickerPreference extends EditTextPreference {
|
||||||
private boolean opacitySliderEnabled = false;
|
private boolean opacitySliderEnabled = false;
|
||||||
|
|
||||||
public static final int ID_REVANCED_COLOR_PICKER_VIEW =
|
public static final int ID_REVANCED_COLOR_PICKER_VIEW =
|
||||||
getResourceIdentifierOrThrow("revanced_color_picker_view", "id");
|
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_color_picker_view");
|
||||||
public static final int ID_PREFERENCE_COLOR_DOT =
|
public static final int ID_PREFERENCE_COLOR_DOT =
|
||||||
getResourceIdentifierOrThrow("preference_color_dot", "id");
|
getResourceIdentifierOrThrow(ResourceType.ID, "preference_color_dot");
|
||||||
public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
|
public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
|
||||||
getResourceIdentifierOrThrow("revanced_color_dot_widget", "layout");
|
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_dot_widget");
|
||||||
public static final int LAYOUT_REVANCED_COLOR_PICKER =
|
public static final int LAYOUT_REVANCED_COLOR_PICKER =
|
||||||
getResourceIdentifierOrThrow("revanced_color_picker", "layout");
|
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_picker");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes non valid hex characters, converts to all uppercase,
|
* Removes non valid hex characters, converts to all uppercase,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import android.widget.TextView;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
|
|
@ -30,14 +31,18 @@ import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class CustomDialogListPreference extends ListPreference {
|
public class CustomDialogListPreference extends ListPreference {
|
||||||
|
|
||||||
public static final int ID_REVANCED_CHECK_ICON =
|
public static final int ID_REVANCED_CHECK_ICON = getResourceIdentifierOrThrow(
|
||||||
getResourceIdentifierOrThrow("revanced_check_icon", "id");
|
ResourceType.ID, "revanced_check_icon");
|
||||||
public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER =
|
public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER = getResourceIdentifierOrThrow(
|
||||||
getResourceIdentifierOrThrow("revanced_check_icon_placeholder", "id");
|
ResourceType.ID, "revanced_check_icon_placeholder");
|
||||||
public static final int ID_REVANCED_ITEM_TEXT =
|
public static final int ID_REVANCED_ITEM_TEXT = getResourceIdentifierOrThrow(
|
||||||
getResourceIdentifierOrThrow("revanced_item_text", "id");
|
ResourceType.ID, "revanced_item_text");
|
||||||
public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED =
|
public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED = getResourceIdentifierOrThrow(
|
||||||
getResourceIdentifierOrThrow("revanced_custom_list_item_checked", "layout");
|
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");
|
||||||
|
|
||||||
private String staticSummary = null;
|
private String staticSummary = null;
|
||||||
private CharSequence[] highlightedEntriesForDialog = null;
|
private CharSequence[] highlightedEntriesForDialog = null;
|
||||||
|
|
@ -125,9 +130,13 @@ public class CustomDialogListPreference extends ListPreference {
|
||||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||||
view = inflater.inflate(layoutResourceId, parent, false);
|
view = inflater.inflate(layoutResourceId, parent, false);
|
||||||
holder = new SubViewDataContainer();
|
holder = new SubViewDataContainer();
|
||||||
holder.checkIcon = view.findViewById(ID_REVANCED_CHECK_ICON);
|
|
||||||
holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
|
holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
|
||||||
holder.itemText = view.findViewById(ID_REVANCED_ITEM_TEXT);
|
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);
|
view.setTag(holder);
|
||||||
} else {
|
} else {
|
||||||
holder = (SubViewDataContainer) view.getTag();
|
holder = (SubViewDataContainer) view.getTag();
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.patches.EnableDebuggingPatch;
|
import app.revanced.extension.shared.patches.EnableDebuggingPatch;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
@ -52,25 +53,26 @@ import app.revanced.extension.shared.ui.Dim;
|
||||||
public class FeatureFlagsManagerPreference extends Preference {
|
public class FeatureFlagsManagerPreference extends Preference {
|
||||||
|
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL =
|
private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_select_all", "drawable");
|
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_select_all");
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL =
|
private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_deselect_all", "drawable");
|
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_deselect_all");
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL =
|
private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_copy_all", "drawable");
|
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_copy_all");
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE =
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_right_one", "drawable");
|
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_one");
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE =
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_right_double", "drawable");
|
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_double");
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE =
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_left_one", "drawable");
|
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_one");
|
||||||
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE =
|
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE =
|
||||||
getResourceIdentifierOrThrow("revanced_settings_arrow_left_double", "drawable");
|
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_double");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flags to hide from the UI.
|
* Flags to hide from the UI.
|
||||||
*/
|
*/
|
||||||
private static final Set<Long> FLAGS_TO_IGNORE = Set.of(
|
private static final Set<Long> FLAGS_TO_IGNORE = Set.of(
|
||||||
45386834L // 'You' tab settings icon.
|
45386834L, // 'You' tab settings icon.
|
||||||
|
45532100L // Cairo flag. Turning this off with all other flags causes the settings menu to be a mix of old/new.
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -129,9 +131,10 @@ public class FeatureFlagsManagerPreference extends Preference {
|
||||||
disabledFlags.removeAll(FLAGS_TO_IGNORE);
|
disabledFlags.removeAll(FLAGS_TO_IGNORE);
|
||||||
|
|
||||||
if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) {
|
if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) {
|
||||||
// String does not need to be localized because it's basically impossible
|
// It's impossible to reach the settings menu without reaching at least one flag.
|
||||||
// to reach the settings menu without encountering at least 1 flag.
|
// So if theres no flags, then that means the user has just enabled debugging
|
||||||
Utils.showToastShort("No feature flags logged yet");
|
// but has not restarted the app yet.
|
||||||
|
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_no_flags"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,18 +36,18 @@ public class SharedPrefCategory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
|
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
|
||||||
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
|
preferences.edit().putString(key, (value == null ? null : value.toString())).commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes any preference data type that has the specified key.
|
* Removes any preference data type that has the specified key.
|
||||||
*/
|
*/
|
||||||
public void removeKey(@NonNull String key) {
|
public void removeKey(@NonNull String key) {
|
||||||
preferences.edit().remove(Objects.requireNonNull(key)).apply();
|
preferences.edit().remove(Objects.requireNonNull(key)).commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveBoolean(@NonNull String key, boolean value) {
|
public void saveBoolean(@NonNull String key, boolean value) {
|
||||||
preferences.edit().putBoolean(key, value).apply();
|
preferences.edit().putBoolean(key, value).commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,16 @@ import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
import app.revanced.extension.shared.ui.Dim;
|
import app.revanced.extension.shared.ui.Dim;
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
@SuppressWarnings("deprecation")
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||||
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -133,8 +136,10 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
*/
|
*/
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
public static Drawable getBackButtonDrawable() {
|
public static Drawable getBackButtonDrawable() {
|
||||||
final int backButtonResource = Utils.getResourceIdentifierOrThrow(
|
final int backButtonResource = Utils.getResourceIdentifierOrThrow(ResourceType.DRAWABLE,
|
||||||
"revanced_settings_toolbar_arrow_left", "drawable");
|
Utils.appIsUsingBoldIcons()
|
||||||
|
? "revanced_settings_toolbar_arrow_left_bold"
|
||||||
|
: "revanced_settings_toolbar_arrow_left");
|
||||||
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
|
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
|
||||||
customizeBackButtonDrawable(drawable);
|
customizeBackButtonDrawable(drawable);
|
||||||
return drawable;
|
return drawable;
|
||||||
|
|
|
||||||
|
|
@ -9,36 +9,36 @@ import android.util.AttributeSet;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple preference that opens a url when clicked.
|
* Simple preference that opens a URL when clicked.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class UrlLinkPreference extends Preference {
|
public class URLLinkPreference extends Preference {
|
||||||
|
|
||||||
protected String externalUrl;
|
protected String externalURL;
|
||||||
|
|
||||||
{
|
{
|
||||||
setOnPreferenceClickListener(pref -> {
|
setOnPreferenceClickListener(pref -> {
|
||||||
if (externalUrl == null) {
|
if (externalURL == null) {
|
||||||
Logger.printException(() -> "URL not set " + getClass().getSimpleName());
|
Logger.printException(() -> "URL not set " + getClass().getSimpleName());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
i.setData(Uri.parse(externalUrl));
|
i.setData(Uri.parse(externalURL));
|
||||||
pref.getContext().startActivity(i);
|
pref.getContext().startActivity(i);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
}
|
}
|
||||||
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
public UrlLinkPreference(Context context, AttributeSet attrs) {
|
public URLLinkPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
public UrlLinkPreference(Context context) {
|
public URLLinkPreference(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,10 +16,11 @@ import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
import app.revanced.extension.shared.settings.preference.URLLinkPreference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base class for search result items, defining common fields and behavior.
|
* Abstract base class for search result items, defining common fields and behavior.
|
||||||
|
|
@ -38,18 +39,18 @@ public abstract class BaseSearchResultItem {
|
||||||
// Get the corresponding layout resource ID.
|
// Get the corresponding layout resource ID.
|
||||||
public int getLayoutResourceId() {
|
public int getLayoutResourceId() {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
|
case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
|
||||||
case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
|
case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
|
||||||
case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
|
case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
|
||||||
case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
|
case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
|
||||||
case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
|
case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
|
||||||
case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
|
case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getResourceIdentifier(String name) {
|
private static int getResourceIdentifier(String name) {
|
||||||
// Placeholder for actual resource identifier retrieval.
|
// Placeholder for actual resource identifier retrieval.
|
||||||
return Utils.getResourceIdentifierOrThrow(name, "layout");
|
return Utils.getResourceIdentifierOrThrow(ResourceType.LAYOUT, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,7 +167,7 @@ public abstract class BaseSearchResultItem {
|
||||||
if (pref instanceof SwitchPreference) return ViewType.SWITCH;
|
if (pref instanceof SwitchPreference) return ViewType.SWITCH;
|
||||||
if (pref instanceof ListPreference) return ViewType.LIST;
|
if (pref instanceof ListPreference) return ViewType.LIST;
|
||||||
if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER;
|
if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER;
|
||||||
if (pref instanceof UrlLinkPreference) return ViewType.URL_LINK;
|
if (pref instanceof URLLinkPreference) return ViewType.URL_LINK;
|
||||||
if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS;
|
if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS;
|
||||||
return ViewType.REGULAR;
|
return ViewType.REGULAR;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package app.revanced.extension.shared.settings.search;
|
package app.revanced.extension.shared.settings.search;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
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.AnimatorSet;
|
||||||
import android.animation.ArgbEvaluator;
|
import android.animation.ArgbEvaluator;
|
||||||
|
|
@ -33,10 +32,11 @@ import java.lang.reflect.Method;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
import app.revanced.extension.shared.settings.preference.URLLinkPreference;
|
||||||
import app.revanced.extension.shared.ui.ColorDot;
|
import app.revanced.extension.shared.ui.ColorDot;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -54,15 +54,15 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
|
||||||
protected static final int PAUSE_BETWEEN_BLINKS = 100;
|
protected static final int PAUSE_BETWEEN_BLINKS = 100;
|
||||||
|
|
||||||
protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
|
protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
|
||||||
"preference_title", "id");
|
ResourceType.ID, "preference_title");
|
||||||
protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
|
protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
|
||||||
"preference_summary", "id");
|
ResourceType.ID, "preference_summary");
|
||||||
protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
|
protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
|
||||||
"preference_path", "id");
|
ResourceType.ID, "preference_path");
|
||||||
protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
|
protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
|
||||||
"preference_switch", "id");
|
ResourceType.ID, "preference_switch");
|
||||||
protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
|
protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
|
||||||
"preference_color_dot", "id");
|
ResourceType.ID, "preference_color_dot");
|
||||||
|
|
||||||
protected static class RegularViewHolder {
|
protected static class RegularViewHolder {
|
||||||
TextView titleView;
|
TextView titleView;
|
||||||
|
|
@ -275,7 +275,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
|
||||||
holder.titleView.setText(item.highlightedTitle);
|
holder.titleView.setText(item.highlightedTitle);
|
||||||
holder.summaryView.setText(item.highlightedSummary);
|
holder.summaryView.setText(item.highlightedSummary);
|
||||||
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
||||||
holder.iconView.setImageResource(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
|
holder.iconView.setImageResource(BaseSearchViewController.getSearchIcon());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -436,7 +436,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes string for comparison (removes extra characters, spaces etc).
|
* Normalizes string for comparison (removes extra characters, spaces etc.).
|
||||||
*/
|
*/
|
||||||
protected String normalizeString(String input) {
|
protected String normalizeString(String input) {
|
||||||
if (TextUtils.isEmpty(input)) return "";
|
if (TextUtils.isEmpty(input)) return "";
|
||||||
|
|
@ -609,8 +609,8 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
|
||||||
boolean hasNavigationCapability(Preference preference) {
|
boolean hasNavigationCapability(Preference preference) {
|
||||||
// PreferenceScreen always allows navigation.
|
// PreferenceScreen always allows navigation.
|
||||||
if (preference instanceof PreferenceScreen) return true;
|
if (preference instanceof PreferenceScreen) return true;
|
||||||
// UrlLinkPreference does not navigate to a new screen, it opens an external URL.
|
// URLLinkPreference does not navigate to a new screen, it opens an external URL.
|
||||||
if (preference instanceof UrlLinkPreference) return false;
|
if (preference instanceof URLLinkPreference) return false;
|
||||||
// Other group types that might have their own screens.
|
// Other group types that might have their own screens.
|
||||||
if (preference instanceof PreferenceGroup) {
|
if (preference instanceof PreferenceGroup) {
|
||||||
// Check if it has its own fragment or intent.
|
// Check if it has its own fragment or intent.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
@ -37,6 +38,7 @@ import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
@ -70,14 +72,29 @@ public abstract class BaseSearchViewController {
|
||||||
|
|
||||||
protected static final int MAX_SEARCH_RESULTS = 50; // Maximum number of search results displayed.
|
protected static final int MAX_SEARCH_RESULTS = 50; // Maximum number of search results displayed.
|
||||||
|
|
||||||
protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow("revanced_search_view", "id");
|
protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow(
|
||||||
protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow("revanced_search_view_container", "id");
|
ResourceType.ID, "revanced_search_view");
|
||||||
protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow("action_search", "id");
|
protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow(
|
||||||
protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
|
ResourceType.ID, "revanced_search_view_container");
|
||||||
public static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON =
|
protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow(
|
||||||
getResourceIdentifierOrThrow("revanced_settings_search_icon", "drawable");
|
ResourceType.ID, "action_search");
|
||||||
protected static final int MENU_REVANCED_SEARCH_MENU =
|
protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow(
|
||||||
getResourceIdentifierOrThrow("revanced_search_menu", "menu");
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new BaseSearchViewController instance.
|
* Constructs a new BaseSearchViewController instance.
|
||||||
|
|
@ -112,7 +129,7 @@ public abstract class BaseSearchViewController {
|
||||||
// Retrieve SearchView and container from XML.
|
// Retrieve SearchView and container from XML.
|
||||||
searchView = activity.findViewById(ID_REVANCED_SEARCH_VIEW);
|
searchView = activity.findViewById(ID_REVANCED_SEARCH_VIEW);
|
||||||
EditText searchEditText = searchView.findViewById(Utils.getResourceIdentifierOrThrow(
|
EditText searchEditText = searchView.findViewById(Utils.getResourceIdentifierOrThrow(
|
||||||
"android:id/search_src_text", null));
|
null, "android:id/search_src_text"));
|
||||||
// Disable fullscreen keyboard mode.
|
// Disable fullscreen keyboard mode.
|
||||||
searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||||
|
|
||||||
|
|
@ -248,6 +265,10 @@ public abstract class BaseSearchViewController {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set bold icon if needed.
|
||||||
|
MenuItem search = toolbar.getMenu().findItem(ID_ACTION_SEARCH);
|
||||||
|
search.setIcon(getSearchIcon());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -524,7 +545,7 @@ public abstract class BaseSearchViewController {
|
||||||
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
|
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
|
||||||
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
|
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
|
||||||
noResultsPreference.setSelectable(false);
|
noResultsPreference.setSelectable(false);
|
||||||
noResultsPreference.setIcon(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
|
noResultsPreference.setIcon(getSearchIcon());
|
||||||
filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList()));
|
filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import java.util.Deque;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
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.settings.preference.BulletPointPreference;
|
||||||
import app.revanced.extension.shared.ui.CustomDialog;
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
|
|
@ -37,25 +39,35 @@ public class SearchHistoryManager {
|
||||||
private static final int MAX_HISTORY_SIZE = 5; // Maximum history items stored.
|
private static final int MAX_HISTORY_SIZE = 5; // Maximum history items stored.
|
||||||
|
|
||||||
private static final int ID_CLEAR_HISTORY_BUTTON = getResourceIdentifierOrThrow(
|
private static final int ID_CLEAR_HISTORY_BUTTON = getResourceIdentifierOrThrow(
|
||||||
"clear_history_button", "id");
|
ResourceType.ID, "clear_history_button");
|
||||||
private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow(
|
private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow(
|
||||||
"history_text", "id");
|
ResourceType.ID, "history_text");
|
||||||
|
private static final int ID_HISTORY_ICON = getResourceIdentifierOrThrow(
|
||||||
|
ResourceType.ID, "history_icon");
|
||||||
private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow(
|
private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow(
|
||||||
"delete_icon", "id");
|
ResourceType.ID, "delete_icon");
|
||||||
private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow(
|
private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow(
|
||||||
"empty_history_title", "id");
|
ResourceType.ID, "empty_history_title");
|
||||||
private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow(
|
private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow(
|
||||||
"empty_history_summary", "id");
|
ResourceType.ID, "empty_history_summary");
|
||||||
private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow(
|
private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow(
|
||||||
"search_history_header", "id");
|
ResourceType.ID, "search_history_header");
|
||||||
private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow(
|
private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow(
|
||||||
"revanced_settings_search_tips_summary", "id");
|
ResourceType.ID, "revanced_settings_search_tips_summary");
|
||||||
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow(
|
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow(
|
||||||
"revanced_preference_search_history_screen", "layout");
|
ResourceType.LAYOUT, "revanced_preference_search_history_screen");
|
||||||
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow(
|
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow(
|
||||||
"revanced_preference_search_history_item", "layout");
|
ResourceType.LAYOUT, "revanced_preference_search_history_item");
|
||||||
private static final int ID_SEARCH_HISTORY_LIST = getResourceIdentifierOrThrow(
|
private static final int ID_SEARCH_HISTORY_LIST = getResourceIdentifierOrThrow(
|
||||||
"search_history_list", "id");
|
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");
|
||||||
|
|
||||||
private final Deque<String> searchHistory;
|
private final Deque<String> searchHistory;
|
||||||
private final Activity activity;
|
private final Activity activity;
|
||||||
|
|
@ -97,7 +109,8 @@ public class SearchHistoryManager {
|
||||||
|
|
||||||
// Inflate search history layout.
|
// Inflate search history layout.
|
||||||
LayoutInflater inflater = LayoutInflater.from(activity);
|
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(
|
searchHistoryContainer.addView(historyView, new FrameLayout.LayoutParams(
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT));
|
FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
|
@ -320,17 +333,29 @@ public class SearchHistoryManager {
|
||||||
public void notifyDataSetChanged() {
|
public void notifyDataSetChanged() {
|
||||||
container.removeAllViews();
|
container.removeAllViews();
|
||||||
for (String query : history) {
|
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).
|
// Set click listener for main item (select query).
|
||||||
view.setOnClickListener(v -> onSelectHistoryItemListener.onSelectHistoryItem(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.
|
// 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(
|
deleteIcon.setOnClickListener(v -> createAndShowDialog(
|
||||||
query,
|
query,
|
||||||
str("revanced_settings_search_remove_message"),
|
str("revanced_settings_search_remove_message"),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -32,7 +31,7 @@ import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Video streaming data. Fetching is tied to the behavior YT uses,
|
* Video streaming data. Fetching is tied to the behavior YT uses,
|
||||||
* where this class fetches the streams only when YT fetches.
|
* where this class fetches the streams only when YT fetches.
|
||||||
* <p>
|
* <p>
|
||||||
* Effectively the cache expiration of these fetches is the same as the stock app,
|
* Effectively the cache expiration of these fetches is the same as the stock app,
|
||||||
|
|
@ -83,22 +82,15 @@ public class StreamingDataRequest {
|
||||||
*/
|
*/
|
||||||
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
|
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(
|
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
|
||||||
new LinkedHashMap<>(100) {
|
Utils.createSizeRestrictedMap(50));
|
||||||
/**
|
|
||||||
* 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.
|
* Strings found in the response if the video is a livestream.
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,11 @@
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.protobuf)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
compileOnly(project(":extensions:spotify:stub"))
|
compileOnly(project(":extensions:spotify:stub"))
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
|
|
||||||
implementation(libs.nanohttpd)
|
|
||||||
implementation(libs.protobuf.javalite)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 24
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protobuf {
|
|
||||||
protoc {
|
|
||||||
artifact = libs.protobuf.protoc.get().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
generateProtoTasks {
|
|
||||||
all().forEach { task ->
|
|
||||||
task.builtins {
|
|
||||||
create("java") {
|
|
||||||
option("lite")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package app.revanced.extension.spotify.layout.hide.createbutton;
|
package app.revanced.extension.spotify.layout.hide.createbutton;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
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.ComponentFilter;
|
||||||
import app.revanced.extension.spotify.shared.ComponentFilters.ResourceIdComponentFilter;
|
import app.revanced.extension.spotify.shared.ComponentFilters.ResourceIdComponentFilter;
|
||||||
import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFilter;
|
import app.revanced.extension.spotify.shared.ComponentFilters.StringComponentFilter;
|
||||||
|
|
@ -16,7 +17,7 @@ public final class HideCreateButtonPatch {
|
||||||
* The main approach used is matching the resource id for the Create button title.
|
* 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(
|
private static final List<ComponentFilter> CREATE_BUTTON_COMPONENT_FILTERS = List.of(
|
||||||
new ResourceIdComponentFilter("navigationbar_musicappitems_create_title", "string"),
|
new ResourceIdComponentFilter(ResourceType.STRING, "navigationbar_musicappitems_create_title"),
|
||||||
// Temporary fallback and fix for APKs merged with AntiSplit-M not having resources properly encoded,
|
// 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.
|
// 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.
|
// FIXME: Remove this once the above issue is no longer relevant.
|
||||||
|
|
@ -28,7 +29,7 @@ public final class HideCreateButtonPatch {
|
||||||
* Used in older versions of the app.
|
* Used in older versions of the app.
|
||||||
*/
|
*/
|
||||||
private static final ResourceIdComponentFilter OLD_CREATE_BUTTON_COMPONENT_FILTER =
|
private static final ResourceIdComponentFilter OLD_CREATE_BUTTON_COMPONENT_FILTER =
|
||||||
new ResourceIdComponentFilter("bottom_navigation_bar_create_tab_title", "string");
|
new ResourceIdComponentFilter(ResourceType.STRING, "bottom_navigation_bar_create_tab_title");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.
|
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
package app.revanced.extension.spotify.misc.fix;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
import static app.revanced.extension.spotify.misc.fix.Constants.*;
|
|
||||||
|
|
||||||
class ClientTokenService {
|
|
||||||
private static final String IOS_CLIENT_ID = "58bd3c95768941ea9eb4350aaa033eb3";
|
|
||||||
private static final String IOS_USER_AGENT;
|
|
||||||
|
|
||||||
static {
|
|
||||||
String clientVersion = getClientVersion();
|
|
||||||
int commitHashIndex = clientVersion.lastIndexOf(".");
|
|
||||||
String version = clientVersion.substring(
|
|
||||||
clientVersion.indexOf("-") + 1,
|
|
||||||
clientVersion.lastIndexOf(".", commitHashIndex - 1)
|
|
||||||
);
|
|
||||||
|
|
||||||
IOS_USER_AGENT = "Spotify/" + version + " iOS/" + getSystemVersion() + " (" + getHardwareMachine() + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final ConnectivitySdkData.Builder IOS_CONNECTIVITY_SDK_DATA =
|
|
||||||
ConnectivitySdkData.newBuilder()
|
|
||||||
.setPlatformSpecificData(PlatformSpecificData.newBuilder()
|
|
||||||
.setIos(NativeIOSData.newBuilder()
|
|
||||||
.setHwMachine(getHardwareMachine())
|
|
||||||
.setSystemVersion(getSystemVersion())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
private static final ClientDataRequest.Builder IOS_CLIENT_DATA_REQUEST =
|
|
||||||
ClientDataRequest.newBuilder()
|
|
||||||
.setClientVersion(getClientVersion())
|
|
||||||
.setClientId(IOS_CLIENT_ID);
|
|
||||||
|
|
||||||
private static final ClientTokenRequest.Builder IOS_CLIENT_TOKEN_REQUEST =
|
|
||||||
ClientTokenRequest.newBuilder()
|
|
||||||
.setRequestType(ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST);
|
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
static ClientTokenRequest newIOSClientTokenRequest(String deviceId) {
|
|
||||||
Logger.printInfo(() -> "Creating new iOS client token request with device ID: " + deviceId);
|
|
||||||
|
|
||||||
return IOS_CLIENT_TOKEN_REQUEST
|
|
||||||
.setClientData(IOS_CLIENT_DATA_REQUEST
|
|
||||||
.setConnectivitySdkData(IOS_CONNECTIVITY_SDK_DATA
|
|
||||||
.setDeviceId(deviceId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
static ClientTokenResponse getClientTokenResponse(@NonNull ClientTokenRequest request) {
|
|
||||||
if (request.getRequestType() == ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST) {
|
|
||||||
Logger.printInfo(() -> "Requesting iOS client token");
|
|
||||||
String deviceId = request.getClientData().getConnectivitySdkData().getDeviceId();
|
|
||||||
request = newIOSClientTokenRequest(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientTokenResponse response;
|
|
||||||
try {
|
|
||||||
response = requestClientToken(request);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Logger.printException(() -> "Failed to handle request", ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static ClientTokenResponse requestClientToken(@NonNull ClientTokenRequest request) throws IOException {
|
|
||||||
HttpURLConnection urlConnection = (HttpURLConnection) new URL(CLIENT_TOKEN_API_URL).openConnection();
|
|
||||||
urlConnection.setRequestMethod("POST");
|
|
||||||
urlConnection.setDoOutput(true);
|
|
||||||
urlConnection.setRequestProperty("Content-Type", "application/x-protobuf");
|
|
||||||
urlConnection.setRequestProperty("Accept", "application/x-protobuf");
|
|
||||||
urlConnection.setRequestProperty("User-Agent", IOS_USER_AGENT);
|
|
||||||
|
|
||||||
byte[] requestArray = request.toByteArray();
|
|
||||||
urlConnection.setFixedLengthStreamingMode(requestArray.length);
|
|
||||||
urlConnection.getOutputStream().write(requestArray);
|
|
||||||
|
|
||||||
try (InputStream inputStream = urlConnection.getInputStream()) {
|
|
||||||
return ClientTokenResponse.parseFrom(inputStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
static ClientTokenResponse serveClientTokenRequest(@NonNull InputStream inputStream) {
|
|
||||||
ClientTokenRequest request;
|
|
||||||
try {
|
|
||||||
request = ClientTokenRequest.parseFrom(inputStream);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Logger.printException(() -> "Failed to parse request from input stream", ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Logger.printInfo(() -> "Request of type: " + request.getRequestType());
|
|
||||||
|
|
||||||
ClientTokenResponse response = getClientTokenResponse(request);
|
|
||||||
if (response != null) Logger.printInfo(() -> "Response of type: " + response.getResponseType());
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package app.revanced.extension.spotify.misc.fix;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
class Constants {
|
|
||||||
static final String CLIENT_TOKEN_API_PATH = "/v1/clienttoken";
|
|
||||||
static final String CLIENT_TOKEN_API_URL = "https://clienttoken.spotify.com" + CLIENT_TOKEN_API_PATH;
|
|
||||||
|
|
||||||
// Modified by a patch. Do not touch.
|
|
||||||
@NonNull
|
|
||||||
static String getClientVersion() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modified by a patch. Do not touch.
|
|
||||||
@NonNull
|
|
||||||
static String getSystemVersion() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modified by a patch. Do not touch.
|
|
||||||
@NonNull
|
|
||||||
static String getHardwareMachine() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
package app.revanced.extension.spotify.misc.fix;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.ClientTokenResponse;
|
|
||||||
import com.google.protobuf.MessageLite;
|
|
||||||
import fi.iki.elonen.NanoHTTPD;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static app.revanced.extension.spotify.misc.fix.ClientTokenService.serveClientTokenRequest;
|
|
||||||
import static app.revanced.extension.spotify.misc.fix.Constants.CLIENT_TOKEN_API_PATH;
|
|
||||||
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
|
|
||||||
|
|
||||||
class RequestListener extends NanoHTTPD {
|
|
||||||
RequestListener(int port) {
|
|
||||||
super(port);
|
|
||||||
|
|
||||||
try {
|
|
||||||
start();
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Logger.printException(() -> "Failed to start request listener on port " + port, ex);
|
|
||||||
throw new RuntimeException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Response serve(@NonNull IHTTPSession session) {
|
|
||||||
String uri = session.getUri();
|
|
||||||
if (!uri.equals(CLIENT_TOKEN_API_PATH)) return INTERNAL_ERROR_RESPONSE;
|
|
||||||
|
|
||||||
Logger.printInfo(() -> "Serving request for URI: " + uri);
|
|
||||||
|
|
||||||
ClientTokenResponse response = serveClientTokenRequest(getInputStream(session));
|
|
||||||
if (response != null) return newResponse(Response.Status.OK, response);
|
|
||||||
|
|
||||||
Logger.printException(() -> "Failed to serve client token request");
|
|
||||||
return INTERNAL_ERROR_RESPONSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static InputStream newLimitedInputStream(InputStream inputStream, long contentLength) {
|
|
||||||
return new FilterInputStream(inputStream) {
|
|
||||||
private long remaining = contentLength;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
if (remaining <= 0) return -1;
|
|
||||||
int result = super.read();
|
|
||||||
if (result != -1) remaining--;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int off, int len) throws IOException {
|
|
||||||
if (remaining <= 0) return -1;
|
|
||||||
len = (int) Math.min(len, remaining);
|
|
||||||
int result = super.read(b, off, len);
|
|
||||||
if (result != -1) remaining -= result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static InputStream getInputStream(@NonNull IHTTPSession session) {
|
|
||||||
long requestContentLength = Long.parseLong(Objects.requireNonNull(session.getHeaders().get("content-length")));
|
|
||||||
return newLimitedInputStream(session.getInputStream(), requestContentLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Response INTERNAL_ERROR_RESPONSE = newResponse(INTERNAL_ERROR);
|
|
||||||
|
|
||||||
@SuppressWarnings("SameParameterValue")
|
|
||||||
@NonNull
|
|
||||||
private static Response newResponse(Response.Status status) {
|
|
||||||
return newResponse(status, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
|
|
||||||
if (messageLite == null) {
|
|
||||||
return newFixedLengthResponse(status, "application/x-protobuf", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] messageBytes = messageLite.toByteArray();
|
|
||||||
InputStream stream = new ByteArrayInputStream(messageBytes);
|
|
||||||
return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package app.revanced.extension.spotify.misc.fix;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class SpoofClientPatch {
|
|
||||||
private static RequestListener listener;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point. Launch requests listener server.
|
|
||||||
*/
|
|
||||||
public synchronized static void launchListener(int port) {
|
|
||||||
if (listener != null) {
|
|
||||||
Logger.printInfo(() -> "Listener already running on port " + port);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Logger.printInfo(() -> "Launching listener on port " + port);
|
|
||||||
listener = new RequestListener(port);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "launchListener failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,6 +14,6 @@ public final class SanitizeSharingLinksPatch {
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static String sanitizeSharingLink(String url) {
|
public static String sanitizeSharingLink(String url) {
|
||||||
return sanitizer.sanitizeUrlString(url);
|
return sanitizer.sanitizeURLString(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package app.revanced.extension.spotify.shared;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
public final class ComponentFilters {
|
public final class ComponentFilters {
|
||||||
|
|
@ -19,21 +20,26 @@ public final class ComponentFilters {
|
||||||
public static final class ResourceIdComponentFilter implements ComponentFilter {
|
public static final class ResourceIdComponentFilter implements ComponentFilter {
|
||||||
|
|
||||||
public final String resourceName;
|
public final String resourceName;
|
||||||
public final String resourceType;
|
public final ResourceType resourceType;
|
||||||
// Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded.
|
// 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.
|
// 0 is returned when a resource has not been found.
|
||||||
private int resourceId = -1;
|
private int resourceId = -1;
|
||||||
@Nullable
|
@Nullable
|
||||||
private String stringfiedResourceId;
|
private String stringfiedResourceId;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public ResourceIdComponentFilter(String resourceName, String resourceType) {
|
public ResourceIdComponentFilter(String resourceName, String resourceType) {
|
||||||
|
this(ResourceType.valueOf(resourceType), resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceIdComponentFilter(ResourceType resourceType, String resourceName) {
|
||||||
this.resourceName = resourceName;
|
this.resourceName = resourceName;
|
||||||
this.resourceType = resourceType;
|
this.resourceType = resourceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getResourceId() {
|
public int getResourceId() {
|
||||||
if (resourceId == -1) {
|
if (resourceId == -1) {
|
||||||
resourceId = Utils.getResourceIdentifier(resourceName, resourceType);
|
resourceId = Utils.getResourceIdentifier(resourceType, resourceName);
|
||||||
}
|
}
|
||||||
return resourceId;
|
return resourceId;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
package spotify.clienttoken.data.v0;
|
|
||||||
|
|
||||||
option optimize_for = LITE_RUNTIME;
|
|
||||||
option java_package = "app.revanced.extension.spotify.misc.fix.clienttoken.data.v0";
|
|
||||||
|
|
||||||
message ClientTokenRequest {
|
|
||||||
ClientTokenRequestType request_type = 1;
|
|
||||||
|
|
||||||
oneof request {
|
|
||||||
ClientDataRequest client_data = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ClientTokenRequestType {
|
|
||||||
REQUEST_UNKNOWN = 0;
|
|
||||||
REQUEST_CLIENT_DATA_REQUEST = 1;
|
|
||||||
REQUEST_CHALLENGE_ANSWERS_REQUEST = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ClientDataRequest {
|
|
||||||
string client_version = 1;
|
|
||||||
string client_id = 2;
|
|
||||||
|
|
||||||
oneof data {
|
|
||||||
ConnectivitySdkData connectivity_sdk_data = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message ConnectivitySdkData {
|
|
||||||
PlatformSpecificData platform_specific_data = 1;
|
|
||||||
string device_id = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message PlatformSpecificData {
|
|
||||||
oneof data {
|
|
||||||
NativeIOSData ios = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message NativeIOSData {
|
|
||||||
int32 user_interface_idiom = 1;
|
|
||||||
bool target_iphone_simulator = 2;
|
|
||||||
string hw_machine = 3;
|
|
||||||
string system_version = 4;
|
|
||||||
string simulator_model_identifier = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message ClientTokenResponse {
|
|
||||||
ClientTokenResponseType response_type = 1;
|
|
||||||
|
|
||||||
oneof response {
|
|
||||||
GrantedTokenResponse granted_token = 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ClientTokenResponseType {
|
|
||||||
RESPONSE_UNKNOWN = 0;
|
|
||||||
RESPONSE_GRANTED_TOKEN_RESPONSE = 1;
|
|
||||||
RESPONSE_CHALLENGES_RESPONSE = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message GrantedTokenResponse {
|
|
||||||
string token = 1;
|
|
||||||
int32 expires_after_seconds = 2;
|
|
||||||
int32 refresh_after_seconds = 3;
|
|
||||||
repeated TokenDomain domains = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TokenDomain {
|
|
||||||
string domain = 1;
|
|
||||||
}
|
|
||||||
|
|
@ -7,11 +7,11 @@ android {
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,9 @@ dependencies {
|
||||||
compileOnly(project(":extensions:strava:stub"))
|
compileOnly(project(":extensions:strava:stub"))
|
||||||
compileOnly(libs.okhttp)
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
package app.revanced.extension.strava;
|
package app.revanced.extension.strava;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import com.strava.mediamodels.data.MediaType;
|
import com.strava.mediamodels.data.MediaType;
|
||||||
import com.strava.photos.data.Media;
|
import com.strava.photos.data.Media;
|
||||||
|
|
||||||
|
|
@ -27,7 +26,6 @@ import java.util.stream.Stream;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
public final class AddMediaDownloadPatch {
|
public final class AddMediaDownloadPatch {
|
||||||
public static final int ACTION_DOWNLOAD = -1;
|
public static final int ACTION_DOWNLOAD = -1;
|
||||||
public static final int ACTION_OPEN_LINK = -2;
|
public static final int ACTION_OPEN_LINK = -2;
|
||||||
|
|
@ -84,7 +82,7 @@ public final class AddMediaDownloadPatch {
|
||||||
} finally {
|
} finally {
|
||||||
values.clear();
|
values.clear();
|
||||||
values.put(MediaStore.Images.Media.IS_PENDING, 0);
|
values.put(MediaStore.Images.Media.IS_PENDING, 0);
|
||||||
resolver.update(row, values, null);
|
resolver.update(row, values, null, null);
|
||||||
}
|
}
|
||||||
showInfoToast("yis_2024_local_save_image_success", "✔️");
|
showInfoToast("yis_2024_local_save_image_success", "✔️");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -150,7 +148,7 @@ public final class AddMediaDownloadPatch {
|
||||||
} finally {
|
} finally {
|
||||||
values.clear();
|
values.clear();
|
||||||
values.put(MediaStore.Video.Media.IS_PENDING, 0);
|
values.put(MediaStore.Video.Media.IS_PENDING, 0);
|
||||||
resolver.update(row, values, null);
|
resolver.update(row, values, null, null);
|
||||||
}
|
}
|
||||||
showInfoToast("yis_2024_local_save_video_success", "✔️");
|
showInfoToast("yis_2024_local_save_video_success", "✔️");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
@ -166,7 +164,7 @@ public final class AddMediaDownloadPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getString(String name, String fallback) {
|
private static String getString(String name, String fallback) {
|
||||||
int id = Utils.getResourceIdentifier(name, "string");
|
int id = Utils.getResourceIdentifier(ResourceType.STRING, name);
|
||||||
return id != 0
|
return id != 0
|
||||||
? Utils.getResourceString(id)
|
? Utils.getResourceString(id)
|
||||||
: fallback;
|
: fallback;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
package app.revanced.extension.strava;
|
package app.revanced.extension.strava;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
|
|
||||||
import com.strava.modularframework.data.Destination;
|
import com.strava.modularframework.data.Destination;
|
||||||
import com.strava.modularframework.data.GenericLayoutModule;
|
import com.strava.modularframework.data.GenericLayoutModule;
|
||||||
import com.strava.modularframework.data.GenericModuleField;
|
import com.strava.modularframework.data.GenericModuleField;
|
||||||
|
|
@ -21,7 +19,6 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
public class HideDistractionsPatch {
|
public class HideDistractionsPatch {
|
||||||
public static boolean upselling;
|
public static boolean upselling;
|
||||||
public static boolean promo;
|
public static boolean promo;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ android {
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 26
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,9 @@ dependencies {
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
compileOnly(libs.okhttp)
|
compileOnly(libs.okhttp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 23
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,4 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 22
|
minSdk = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import java.lang.reflect.InvocationTargetException;
|
||||||
/**
|
/**
|
||||||
* Hooks AdPersonalizationActivity to inject a custom {@link TikTokPreferenceFragment}.
|
* Hooks AdPersonalizationActivity to inject a custom {@link TikTokPreferenceFragment}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"deprecation", "NewApi", "unused"})
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
public class TikTokActivityHook {
|
public class TikTokActivityHook {
|
||||||
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
|
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory {
|
||||||
addPreference(new TogglePreference(context,
|
addPreference(new TogglePreference(context,
|
||||||
"Sanitize sharing links",
|
"Sanitize sharing links",
|
||||||
"Remove tracking parameters from shared links.",
|
"Remove tracking parameters from shared links.",
|
||||||
BaseSettings.SANITIZE_SHARED_LINKS
|
BaseSettings.SANITIZE_SHARING_LINKS
|
||||||
));
|
));
|
||||||
|
|
||||||
addPreference(new TogglePreference(context,
|
addPreference(new TogglePreference(context,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package app.revanced.extension.tiktok.share;
|
package app.revanced.extension.tiktok.share;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
import app.revanced.extension.shared.privacy.LinkSanitizer;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
|
@ -13,7 +12,7 @@ public final class ShareUrlSanitizer {
|
||||||
* Injection point for setting check.
|
* Injection point for setting check.
|
||||||
*/
|
*/
|
||||||
public static boolean shouldSanitize() {
|
public static boolean shouldSanitize() {
|
||||||
return BaseSettings.SANITIZE_SHARED_LINKS.get();
|
return BaseSettings.SANITIZE_SHARING_LINKS.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -24,6 +23,6 @@ public final class ShareUrlSanitizer {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sanitizer.sanitizeUrlString(url);
|
return sanitizer.sanitizeURLString(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.appcompat)
|
compileOnly(libs.appcompat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 22
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,4 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
package app.revanced.extension.twitch;
|
package app.revanced.extension.twitch;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
/* Called from SettingsPatch smali */
|
/* Called from SettingsPatch smali */
|
||||||
public static int getStringId(String name) {
|
public static int getStringId(String name) {
|
||||||
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "string");
|
return app.revanced.extension.shared.Utils.getResourceIdentifier(
|
||||||
|
ResourceType.STRING, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Called from SettingsPatch smali */
|
/* Called from SettingsPatch smali */
|
||||||
public static int getDrawableId(String name) {
|
public static int getDrawableId(String name) {
|
||||||
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "drawable");
|
return app.revanced.extension.shared.Utils.getResourceIdentifier(
|
||||||
|
ResourceType.DRAWABLE, name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,25 @@ import static app.revanced.extension.twitch.Utils.getStringId;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.twitch.settings.preference.TwitchPreferenceFragment;
|
import app.revanced.extension.twitch.settings.preference.TwitchPreferenceFragment;
|
||||||
|
|
||||||
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
|
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
|
||||||
import tv.twitch.android.settings.SettingsActivity;
|
import tv.twitch.android.settings.SettingsActivity;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks AppCompatActivity to inject a custom {@link TwitchPreferenceFragment}.
|
* Hooks AppCompatActivity to inject a custom {@link TwitchPreferenceFragment}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"deprecation", "NewApi", "unused"})
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
public class TwitchActivityHook {
|
public class TwitchActivityHook {
|
||||||
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
|
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
|
||||||
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
|
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
|
||||||
|
|
@ -108,7 +110,7 @@ public class TwitchActivityHook {
|
||||||
|
|
||||||
base.getFragmentManager()
|
base.getFragmentManager()
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(Utils.getResourceIdentifier("fragment_container", "id"), fragment)
|
.replace(Utils.getResourceIdentifier(ResourceType.ID, "fragment_container"), fragment)
|
||||||
.commit();
|
.commit();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(project(":extensions:shared:library"))
|
compileOnly(project(":extensions:shared:library"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
<manifest/>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
* Can show YouTube provided screen captures of beginning/middle/end of the video.
|
* Can show YouTube provided screen captures of beginning/middle/end of the video.
|
||||||
* (ie: sd1.jpg, sd2.jpg, sd3.jpg).
|
* (ie: sd1.jpg, sd2.jpg, sd3.jpg).
|
||||||
* <p>
|
* <p>
|
||||||
* Or can show crowd-sourced thumbnails provided by DeArrow (<a href="http://dearrow.ajay.app">...</a>).
|
* Or can show crowdsourced thumbnails provided by DeArrow (<a href="http://dearrow.ajay.app">...</a>).
|
||||||
* <p>
|
* <p>
|
||||||
* Or can use DeArrow and fall back to screen captures if DeArrow is not available.
|
* Or can use DeArrow and fall back to screen captures if DeArrow is not available.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
@ -135,12 +135,12 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Uri dearrowApiUri;
|
private static final Uri dearrowAPIURI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scheme and host of {@link #dearrowApiUri}.
|
* The scheme and host of {@link #dearrowAPIURI}.
|
||||||
*/
|
*/
|
||||||
private static final String deArrowApiUrlPrefix;
|
private static final String deArrowAPIURLPrefix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long to temporarily turn off DeArrow if it fails for any reason.
|
* How long to temporarily turn off DeArrow if it fails for any reason.
|
||||||
|
|
@ -148,31 +148,31 @@ public final class AlternativeThumbnailsPatch {
|
||||||
private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
|
private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If non zero, then the system time of when DeArrow API calls can resume.
|
* If non-zero, then the system time of when DeArrow API calls can resume.
|
||||||
*/
|
*/
|
||||||
private static volatile long timeToResumeDeArrowAPICalls;
|
private static volatile long timeToResumeDeArrowAPICalls;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
dearrowApiUri = validateSettings();
|
dearrowAPIURI = validateSettings();
|
||||||
final int port = dearrowApiUri.getPort();
|
final int port = dearrowAPIURI.getPort();
|
||||||
String portString = port == -1 ? "" : (":" + port);
|
String portString = port == -1 ? "" : (":" + port);
|
||||||
deArrowApiUrlPrefix = dearrowApiUri.getScheme() + "://" + dearrowApiUri.getHost() + portString + "/";
|
deArrowAPIURLPrefix = dearrowAPIURI.getScheme() + "://" + dearrowAPIURI.getHost() + portString + "/";
|
||||||
Logger.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix);
|
Logger.printDebug(() -> "Using DeArrow API address: " + deArrowAPIURLPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fix any bad imported data.
|
* Fix any bad imported data.
|
||||||
*/
|
*/
|
||||||
private static Uri validateSettings() {
|
private static Uri validateSettings() {
|
||||||
Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get());
|
Uri apiURI = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get());
|
||||||
// Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made.
|
// Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made.
|
||||||
String scheme = apiUri.getScheme();
|
String scheme = apiURI.getScheme();
|
||||||
if (scheme == null || scheme.equals("http") || apiUri.getHost() == null) {
|
if (scheme == null || scheme.equals("http") || apiURI.getHost() == null) {
|
||||||
Utils.showToastLong("Invalid DeArrow API URL. Using default");
|
Utils.showToastLong("Invalid DeArrow API URL. Using default");
|
||||||
Settings.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault();
|
Settings.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault();
|
||||||
return validateSettings();
|
return validateSettings();
|
||||||
}
|
}
|
||||||
return apiUri;
|
return apiURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ThumbnailOption optionSettingForCurrentNavigation() {
|
private static ThumbnailOption optionSettingForCurrentNavigation() {
|
||||||
|
|
@ -209,16 +209,16 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the alternative thumbnail url using YouTube provided still video captures.
|
* Build the alternative thumbnail URL using YouTube provided still video captures.
|
||||||
*
|
*
|
||||||
* @param decodedUrl Decoded original thumbnail request url.
|
* @param decodedURL Decoded original thumbnail request url.
|
||||||
* @return The alternative thumbnail url, or if not available NULL.
|
* @return The alternative thumbnail URL, or if not available NULL.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String buildYouTubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
|
private static String buildYouTubeVideoStillURL(@NonNull DecodedThumbnailURL decodedURL,
|
||||||
@NonNull ThumbnailQuality qualityToUse) {
|
@NonNull ThumbnailQuality qualityToUse) {
|
||||||
String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false);
|
String sanitizedReplacement = decodedURL.createStillsURL(qualityToUse, false);
|
||||||
if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
|
if (VerifiedQualities.verifyAltThumbnailExist(decodedURL.videoId, qualityToUse, sanitizedReplacement)) {
|
||||||
return sanitizedReplacement;
|
return sanitizedReplacement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,26 +226,26 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the alternative thumbnail url using DeArrow thumbnail cache.
|
* Build the alternative thumbnail URL using DeArrow thumbnail cache.
|
||||||
*
|
*
|
||||||
* @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short).
|
* @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short).
|
||||||
* @param fallbackUrl URL to fall back to in case.
|
* @param fallbackURL URL to fall back to in case.
|
||||||
* @return The alternative thumbnail url, without tracking parameters.
|
* @return The alternative thumbnail URL, without tracking parameters.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private static String buildDeArrowThumbnailURL(String videoId, String fallbackUrl) {
|
private static String buildDeArrowThumbnailURL(String videoId, String fallbackURL) {
|
||||||
// Build thumbnail request url.
|
// Build thumbnail request URL.
|
||||||
// See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29.
|
// See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29.
|
||||||
return dearrowApiUri
|
return dearrowAPIURI
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.appendQueryParameter("videoID", videoId)
|
.appendQueryParameter("videoId", videoId)
|
||||||
.appendQueryParameter("redirectUrl", fallbackUrl)
|
.appendQueryParameter("redirectURL", fallbackURL)
|
||||||
.build()
|
.build()
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean urlIsDeArrow(@NonNull String imageUrl) {
|
private static boolean urlIsDeArrow(@NonNull String imageURL) {
|
||||||
return imageUrl.startsWith(deArrowApiUrlPrefix);
|
return imageURL.startsWith(deArrowAPIURLPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -264,7 +264,7 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void handleDeArrowError(@NonNull String url, int statusCode) {
|
private static void handleDeArrowError(@NonNull String url, int statusCode) {
|
||||||
Logger.printDebug(() -> "Encountered DeArrow error. Url: " + url);
|
Logger.printDebug(() -> "Encountered DeArrow error. URL: " + url);
|
||||||
final long now = System.currentTimeMillis();
|
final long now = System.currentTimeMillis();
|
||||||
if (timeToResumeDeArrowAPICalls < now) {
|
if (timeToResumeDeArrowAPICalls < now) {
|
||||||
timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS;
|
timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS;
|
||||||
|
|
@ -278,63 +278,63 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||||
*
|
*
|
||||||
* @param originalUrl Image url for all url images loaded, including video thumbnails.
|
* @param originalURL Image URL for all URL images loaded, including video thumbnails.
|
||||||
*/
|
*/
|
||||||
public static String overrideImageURL(String originalUrl) {
|
public static String overrideImageURL(String originalURL) {
|
||||||
try {
|
try {
|
||||||
ThumbnailOption option = optionSettingForCurrentNavigation();
|
ThumbnailOption option = optionSettingForCurrentNavigation();
|
||||||
|
|
||||||
if (option == ThumbnailOption.ORIGINAL) {
|
if (option == ThumbnailOption.ORIGINAL) {
|
||||||
return originalUrl;
|
return originalURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl);
|
final var decodedURL = DecodedThumbnailURL.decodeImageURL(originalURL);
|
||||||
if (decodedUrl == null) {
|
if (decodedURL == null) {
|
||||||
return originalUrl; // Not a thumbnail.
|
return originalURL; // Not a thumbnail.
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl);
|
Logger.printDebug(() -> "Original URL: " + decodedURL.sanitizedURL);
|
||||||
|
|
||||||
ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality);
|
ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedURL.imageQuality);
|
||||||
if (qualityToUse == null) {
|
if (qualityToUse == null) {
|
||||||
// Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these).
|
// Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these).
|
||||||
return originalUrl;
|
return originalURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
String sanitizedReplacementUrl;
|
String sanitizedReplacementURL;
|
||||||
final boolean includeTracking;
|
final boolean includeTracking;
|
||||||
if (option.useDeArrow && canUseDeArrowAPI()) {
|
if (option.useDeArrow && canUseDeArrowAPI()) {
|
||||||
includeTracking = false; // Do not include view tracking parameters with API call.
|
includeTracking = false; // Do not include view tracking parameters with API call.
|
||||||
String fallbackUrl = null;
|
String fallbackURL = null;
|
||||||
if (option.useStillImages) {
|
if (option.useStillImages) {
|
||||||
fallbackUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
|
fallbackURL = buildYouTubeVideoStillURL(decodedURL, qualityToUse);
|
||||||
}
|
}
|
||||||
if (fallbackUrl == null) {
|
if (fallbackURL == null) {
|
||||||
fallbackUrl = decodedUrl.sanitizedUrl;
|
fallbackURL = decodedURL.sanitizedURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
|
sanitizedReplacementURL = buildDeArrowThumbnailURL(decodedURL.videoId, fallbackURL);
|
||||||
} else if (option.useStillImages) {
|
} else if (option.useStillImages) {
|
||||||
includeTracking = true; // Include view tracking parameters if present.
|
includeTracking = true; // Include view tracking parameters if present.
|
||||||
sanitizedReplacementUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
|
sanitizedReplacementURL = buildYouTubeVideoStillURL(decodedURL, qualityToUse);
|
||||||
if (sanitizedReplacementUrl == null) {
|
if (sanitizedReplacementURL == null) {
|
||||||
return originalUrl; // Still capture is not available. Return the untouched original url.
|
return originalURL; // Still capture is not available. Return the untouched original url.
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled.
|
return originalURL; // Recently experienced DeArrow failure and video stills are not enabled.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not log any tracking parameters.
|
// Do not log any tracking parameters.
|
||||||
Logger.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl);
|
Logger.printDebug(() -> "Replacement URL: " + sanitizedReplacementURL);
|
||||||
|
|
||||||
return includeTracking
|
return includeTracking
|
||||||
? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters
|
? sanitizedReplacementURL + decodedURL.viewTrackingParameters
|
||||||
: sanitizedReplacementUrl;
|
: sanitizedReplacementURL;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "overrideImageURL failure", ex);
|
Logger.printException(() -> "overrideImageURL failure", ex);
|
||||||
return originalUrl;
|
return originalURL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,21 +370,21 @@ public final class AlternativeThumbnailsPatch {
|
||||||
// - very old
|
// - very old
|
||||||
// - very low view count
|
// - very low view count
|
||||||
// Take note of this, so if the image reloads the original thumbnail will be used.
|
// Take note of this, so if the image reloads the original thumbnail will be used.
|
||||||
DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(url);
|
DecodedThumbnailURL decodedURL = DecodedThumbnailURL.decodeImageURL(url);
|
||||||
if (decodedUrl == null) {
|
if (decodedURL == null) {
|
||||||
return; // Not a thumbnail.
|
return; // Not a thumbnail.
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedUrl.sanitizedUrl);
|
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedURL.sanitizedURL);
|
||||||
|
|
||||||
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
|
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedURL.imageQuality);
|
||||||
if (quality == null) {
|
if (quality == null) {
|
||||||
// Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen.
|
// Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen.
|
||||||
Logger.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl);
|
Logger.printDebug(() -> "Failed to recognize image quality of URL: " + decodedURL.sanitizedURL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality);
|
VerifiedQualities.setAltThumbnailDoesNotExist(decodedURL.videoId, quality);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Callback success error", ex);
|
Logger.printException(() -> "Callback success error", ex);
|
||||||
|
|
@ -482,7 +482,7 @@ public final class AlternativeThumbnailsPatch {
|
||||||
// (even though search and subscriptions use the exact same layout as the home feed).
|
// (even though search and subscriptions use the exact same layout as the home feed).
|
||||||
// Of note, this image quality issue only appears with the alt thumbnail images,
|
// Of note, this image quality issue only appears with the alt thumbnail images,
|
||||||
// and the regular thumbnails have identical color/contrast quality for all sizes.
|
// and the regular thumbnails have identical color/contrast quality for all sizes.
|
||||||
// Fix this by falling thru and upgrading SD to 720.
|
// Fix this by falling through and upgrading SD to 720.
|
||||||
case SDDEFAULT, HQ720 -> { // SD is max resolution for fast alt images.
|
case SDDEFAULT, HQ720 -> { // SD is max resolution for fast alt images.
|
||||||
if (useFastQuality) {
|
if (useFastQuality) {
|
||||||
yield SDDEFAULT;
|
yield SDDEFAULT;
|
||||||
|
|
@ -525,17 +525,11 @@ public final class AlternativeThumbnailsPatch {
|
||||||
private static final long NOT_AVAILABLE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; // 10 minutes.
|
private static final long NOT_AVAILABLE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; // 10 minutes.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache used to verify if an alternative thumbnails exists for a given video id.
|
* Cache used to verify if an alternative thumbnails exists for a given video ID.
|
||||||
*/
|
*/
|
||||||
@GuardedBy("itself")
|
@GuardedBy("itself")
|
||||||
private static final Map<String, VerifiedQualities> altVideoIdLookup = new LinkedHashMap<>(100) {
|
private static final Map<String, VerifiedQualities> altVideoIdLookup =
|
||||||
private static final int CACHE_LIMIT = 1000;
|
Utils.createSizeRestrictedMap(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) {
|
private static VerifiedQualities getVerifiedQualities(@NonNull String videoId, boolean returnNullIfDoesNotExist) {
|
||||||
synchronized (altVideoIdLookup) {
|
synchronized (altVideoIdLookup) {
|
||||||
|
|
@ -552,10 +546,10 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
||||||
@NonNull String imageUrl) {
|
@NonNull String imageURL) {
|
||||||
VerifiedQualities verified = getVerifiedQualities(videoId, Settings.ALT_THUMBNAIL_STILLS_FAST.get());
|
VerifiedQualities verified = getVerifiedQualities(videoId, Settings.ALT_THUMBNAIL_STILLS_FAST.get());
|
||||||
if (verified == null) return true; // Fast alt thumbnails is enabled.
|
if (verified == null) return true; // Fast alt thumbnails is enabled.
|
||||||
return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl);
|
return verified.verifyYouTubeThumbnailExists(videoId, quality, imageURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) {
|
static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) {
|
||||||
|
|
@ -596,10 +590,10 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify if a video alt thumbnail exists. Does so by making a minimal HEAD http request.
|
* Verify if a video alt thumbnail exists. Does so by making a minimal HEAD HTTP request.
|
||||||
*/
|
*/
|
||||||
synchronized boolean verifyYouTubeThumbnailExists(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
synchronized boolean verifyYouTubeThumbnailExists(@NonNull String videoId, @NonNull ThumbnailQuality quality,
|
||||||
@NonNull String imageUrl) {
|
@NonNull String imageURL) {
|
||||||
if (highestQualityVerified != null && highestQualityVerified.ordinal() >= quality.ordinal()) {
|
if (highestQualityVerified != null && highestQualityVerified.ordinal() >= quality.ordinal()) {
|
||||||
return true; // Previously verified as existing.
|
return true; // Previously verified as existing.
|
||||||
}
|
}
|
||||||
|
|
@ -615,7 +609,7 @@ public final class AlternativeThumbnailsPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fastQuality) {
|
if (fastQuality) {
|
||||||
return true; // Unknown if it exists or not. Use the URL anyways and update afterwards if loading fails.
|
return true; // Unknown if it exists or not. Use the URL anyway and update afterward if loading fails.
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean imageFileFound;
|
boolean imageFileFound;
|
||||||
|
|
@ -625,7 +619,7 @@ public final class AlternativeThumbnailsPatch {
|
||||||
final long start = System.currentTimeMillis();
|
final long start = System.currentTimeMillis();
|
||||||
imageFileFound = Utils.submitOnBackgroundThread(() -> {
|
imageFileFound = Utils.submitOnBackgroundThread(() -> {
|
||||||
final int connectionTimeoutMillis = 10000; // 10 seconds.
|
final int connectionTimeoutMillis = 10000; // 10 seconds.
|
||||||
HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection();
|
HttpURLConnection connection = (HttpURLConnection) new URL(imageURL).openConnection();
|
||||||
connection.setConnectTimeout(connectionTimeoutMillis);
|
connection.setConnectTimeout(connectionTimeoutMillis);
|
||||||
connection.setReadTimeout(connectionTimeoutMillis);
|
connection.setReadTimeout(connectionTimeoutMillis);
|
||||||
connection.setRequestMethod("HEAD");
|
connection.setRequestMethod("HEAD");
|
||||||
|
|
@ -638,13 +632,13 @@ public final class AlternativeThumbnailsPatch {
|
||||||
return (contentType != null && contentType.startsWith("image"));
|
return (contentType != null && contentType.startsWith("image"));
|
||||||
}
|
}
|
||||||
if (responseCode != HttpURLConnection.HTTP_NOT_FOUND) {
|
if (responseCode != HttpURLConnection.HTTP_NOT_FOUND) {
|
||||||
Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for url: " + imageUrl);
|
Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for URL: " + imageURL);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).get();
|
}).get();
|
||||||
Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl);
|
Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageURL);
|
||||||
} catch (ExecutionException | InterruptedException ex) {
|
} catch (ExecutionException | InterruptedException ex) {
|
||||||
Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex);
|
Logger.printInfo(() -> "Could not verify alt URL: " + imageURL, ex);
|
||||||
imageFileFound = false;
|
imageFileFound = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -656,11 +650,11 @@ public final class AlternativeThumbnailsPatch {
|
||||||
/**
|
/**
|
||||||
* YouTube video thumbnail url, decoded into it's relevant parts.
|
* YouTube video thumbnail url, decoded into it's relevant parts.
|
||||||
*/
|
*/
|
||||||
private static class DecodedThumbnailUrl {
|
private static class DecodedThumbnailURL {
|
||||||
private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/";
|
private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/";
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
static DecodedThumbnailUrl decodeImageUrl(String url) {
|
static DecodedThumbnailURL decodeImageURL(String url) {
|
||||||
final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1;
|
final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1;
|
||||||
if (urlPathStartIndex <= 0) return null;
|
if (urlPathStartIndex <= 0) return null;
|
||||||
|
|
||||||
|
|
@ -680,14 +674,14 @@ public final class AlternativeThumbnailsPatch {
|
||||||
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
|
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
|
||||||
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
|
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
|
||||||
|
|
||||||
return new DecodedThumbnailUrl(url, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex,
|
return new DecodedThumbnailURL(url, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex,
|
||||||
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
|
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String originalFullUrl;
|
final String originalFullURL;
|
||||||
/** Full usable url, but stripped of any tracking information. */
|
/** Full usable url, but stripped of any tracking information. */
|
||||||
final String sanitizedUrl;
|
final String sanitizedURL;
|
||||||
/** Url path, such as 'vi' or 'vi_webp' */
|
/** URL path, such as 'vi' or 'vi_webp' */
|
||||||
final String urlPath;
|
final String urlPath;
|
||||||
final String videoId;
|
final String videoId;
|
||||||
/** Quality, such as hq720 or sddefault. */
|
/** Quality, such as hq720 or sddefault. */
|
||||||
|
|
@ -697,25 +691,25 @@ public final class AlternativeThumbnailsPatch {
|
||||||
/** User view tracking parameters, only present on some images. */
|
/** User view tracking parameters, only present on some images. */
|
||||||
final String viewTrackingParameters;
|
final String viewTrackingParameters;
|
||||||
|
|
||||||
DecodedThumbnailUrl(String fullUrl, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex,
|
DecodedThumbnailURL(String fullURL, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex,
|
||||||
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
|
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
|
||||||
originalFullUrl = fullUrl;
|
originalFullURL = fullURL;
|
||||||
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
|
sanitizedURL = fullURL.substring(0, imageExtensionEndIndex);
|
||||||
urlPath = fullUrl.substring(urlPathStartIndex, urlPathEndIndex);
|
urlPath = fullURL.substring(urlPathStartIndex, urlPathEndIndex);
|
||||||
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
|
videoId = fullURL.substring(videoIdStartIndex, videoIdEndIndex);
|
||||||
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
|
imageQuality = fullURL.substring(imageSizeStartIndex, imageSizeEndIndex);
|
||||||
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
|
imageExtension = fullURL.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
|
||||||
viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length())
|
viewTrackingParameters = (imageExtensionEndIndex == fullURL.length())
|
||||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
? "" : fullURL.substring(imageExtensionEndIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("SameParameterValue")
|
@SuppressWarnings("SameParameterValue")
|
||||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
String createStillsURL(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||||
// especially for new videos uploaded in the last hour.
|
// especially for new videos uploaded in the last hour.
|
||||||
// And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images.
|
// And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images.
|
||||||
// (as much as 4x slower network response has been observed, despite the alt webp image being a smaller file).
|
// (as much as 4x slower network response has been observed, despite the alt webp image being a smaller file).
|
||||||
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
|
StringBuilder builder = new StringBuilder(originalFullURL.length() + 2);
|
||||||
// Many different "i.ytimage.com" domains exist such as "i9.ytimg.com",
|
// Many different "i.ytimage.com" domains exist such as "i9.ytimg.com",
|
||||||
// but still captures are frequently not available on the other domains (especially newly uploaded videos).
|
// but still captures are frequently not available on the other domains (especially newly uploaded videos).
|
||||||
// So always use the primary domain for a higher success rate.
|
// So always use the primary domain for a higher success rate.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public class BackgroundPlaybackPatch {
|
||||||
|
|
||||||
// Steps to verify most edge cases (with Shorts background playback set to off):
|
// Steps to verify most edge cases (with Shorts background playback set to off):
|
||||||
// 1. Open a regular video
|
// 1. Open a regular video
|
||||||
// 2. Minimize app (PIP should appear)
|
// 2. Minimize app (PiP should appear)
|
||||||
// 3. Reopen app
|
// 3. Reopen app
|
||||||
// 4. Open a Short (without closing the regular video)
|
// 4. Open a Short (without closing the regular video)
|
||||||
// (try opening both Shorts in the video player suggestions AND Shorts from the home feed)
|
// (try opening both Shorts in the video player suggestions AND Shorts from the home feed)
|
||||||
|
|
@ -23,7 +23,7 @@ public class BackgroundPlaybackPatch {
|
||||||
// 6. Reopen app
|
// 6. Reopen app
|
||||||
// 7. Close the Short
|
// 7. Close the Short
|
||||||
// 8. Resume playing the regular video
|
// 8. Resume playing the regular video
|
||||||
// 9. Minimize the app (PIP should appear)
|
// 9. Minimize the app (PiP should appear)
|
||||||
if (ShortsPlayerState.isOpen()) {
|
if (ShortsPlayerState.isOpen()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,24 +15,24 @@ public final class BypassImageRegionRestrictionsPatch {
|
||||||
private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com";
|
private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* YouTube static images domain. Includes user and channel avatar images and community post images.
|
* YouTube static images' domain. Includes user and channel avatar images and community post images.
|
||||||
*/
|
*/
|
||||||
private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
||||||
= Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com");
|
= Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point. Called off the main thread and by multiple threads at the same time.
|
* Injection point. Called off the main thread and by multiple threads at the same time.
|
||||||
*
|
*
|
||||||
* @param originalUrl Image url for all image urls loaded.
|
* @param originalURL Image URL for all image URLs loaded.
|
||||||
*/
|
*/
|
||||||
public static String overrideImageURL(String originalUrl) {
|
public static String overrideImageURL(String originalURL) {
|
||||||
try {
|
try {
|
||||||
if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) {
|
if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) {
|
||||||
String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
|
||||||
.matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN);
|
.matcher(originalURL).replaceFirst(REPLACEMENT_IMAGE_DOMAIN);
|
||||||
|
|
||||||
if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) {
|
if (Settings.DEBUG.get() && !replacement.equals(originalURL)) {
|
||||||
Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'");
|
Logger.printDebug(() -> "Replaced: '" + originalURL + "' with: '" + replacement + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
return replacement;
|
return replacement;
|
||||||
|
|
@ -41,6 +41,6 @@ public final class BypassImageRegionRestrictionsPatch {
|
||||||
Logger.printException(() -> "overrideImageURL failure", ex);
|
Logger.printException(() -> "overrideImageURL failure", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalUrl;
|
return originalURL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ public class ChangeFormFactorPatch {
|
||||||
public static void navigationTabCreated(NavigationButton button, View tabView) {
|
public static void navigationTabCreated(NavigationButton button, View tabView) {
|
||||||
// On first startup of the app the navigation buttons are fetched and updated.
|
// On first startup of the app the navigation buttons are fetched and updated.
|
||||||
// If the user immediately opens the 'You' or opens a video, then the call to
|
// If the user immediately opens the 'You' or opens a video, then the call to
|
||||||
// update the navigtation buttons will use the non automotive form factor
|
// update the navigation buttons will use the non-automotive form factor
|
||||||
// and the explore tab is missing.
|
// and the explore tab is missing.
|
||||||
// Fixing this is not so simple because of the concurrent calls for the player and You tab.
|
// Fixing this is not so simple because of the concurrent calls for the player and You tab.
|
||||||
// For now, always hide the explore tab.
|
// For now, always hide the explore tab.
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.ResourceType;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
|
@ -50,7 +51,7 @@ public class ChangeHeaderPatch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int identifier = Utils.getResourceIdentifier(attributeName, "attr");
|
final int identifier = Utils.getResourceIdentifier(ResourceType.ATTR, attributeName);
|
||||||
if (identifier == 0) {
|
if (identifier == 0) {
|
||||||
// Should never happen.
|
// Should never happen.
|
||||||
Logger.printException(() -> "Could not find attribute: " + drawableName);
|
Logger.printException(() -> "Could not find attribute: " + drawableName);
|
||||||
|
|
@ -71,7 +72,7 @@ public class ChangeHeaderPatch {
|
||||||
? "_dark"
|
? "_dark"
|
||||||
: "_light");
|
: "_light");
|
||||||
|
|
||||||
final int identifier = Utils.getResourceIdentifier(drawableFullName, "drawable");
|
final int identifier = Utils.getResourceIdentifier(ResourceType.DRAWABLE, drawableFullName);
|
||||||
if (identifier != 0) {
|
if (identifier != 0) {
|
||||||
return Utils.getContext().getDrawable(identifier);
|
return Utils.getContext().getDrawable(identifier);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public final class ChangeStartPagePatch {
|
||||||
DEFAULT("", null),
|
DEFAULT("", null),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse id.
|
* BrowseId.
|
||||||
*/
|
*/
|
||||||
ALL_SUBSCRIPTIONS("FEchannels", TRUE),
|
ALL_SUBSCRIPTIONS("FEchannels", TRUE),
|
||||||
BROWSE("FEguide_builder", TRUE),
|
BROWSE("FEguide_builder", TRUE),
|
||||||
|
|
@ -39,7 +39,7 @@ public final class ChangeStartPagePatch {
|
||||||
YOUR_CLIPS("FEclips", TRUE),
|
YOUR_CLIPS("FEclips", TRUE),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel id, this can be used as a browseId.
|
* Channel ID, this can be used as a browseId.
|
||||||
*/
|
*/
|
||||||
COURSES("UCtFRv9O2AHqOZjjynzrv-xg", TRUE),
|
COURSES("UCtFRv9O2AHqOZjjynzrv-xg", TRUE),
|
||||||
FASHION("UCrpQ4p1Ql_hG8rKXIKM1MOQ", TRUE),
|
FASHION("UCrpQ4p1Ql_hG8rKXIKM1MOQ", TRUE),
|
||||||
|
|
@ -52,7 +52,7 @@ public final class ChangeStartPagePatch {
|
||||||
VIRTUAL_REALITY("UCzuqhhs6NWbgTzMuM09WKDQ", TRUE),
|
VIRTUAL_REALITY("UCzuqhhs6NWbgTzMuM09WKDQ", TRUE),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playlist id, this can be used as a browseId.
|
* Playlist ID, this can be used as a browseId.
|
||||||
*/
|
*/
|
||||||
LIKED_VIDEO("VLLL", TRUE),
|
LIKED_VIDEO("VLLL", TRUE),
|
||||||
WATCH_LATER("VLWL", TRUE),
|
WATCH_LATER("VLWL", TRUE),
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import android.os.Build;
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
public class CopyVideoUrlPatch {
|
public class CopyVideoURLPatch {
|
||||||
|
|
||||||
public static void copyUrl(boolean withTimestamp) {
|
public static void copyURL(boolean withTimestamp) {
|
||||||
try {
|
try {
|
||||||
StringBuilder builder = new StringBuilder("https://youtu.be/");
|
StringBuilder builder = new StringBuilder("https://youtu.be/");
|
||||||
builder.append(VideoInformation.getVideoId());
|
builder.append(VideoInformation.getVideoId());
|
||||||
|
|
@ -31,7 +31,7 @@ public class CopyVideoUrlPatch {
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.setClipboard(builder.toString());
|
Utils.setClipboard(builder.toString());
|
||||||
// Do not show a toast if using Android 13+ as it shows it's own toast.
|
// Do not show a toast if using Android 13+ as it shows its own toast.
|
||||||
// But if the user copied with a timestamp then show a toast.
|
// But if the user copied with a timestamp then show a toast.
|
||||||
// Unfortunately this will show 2 toasts on Android 13+, but no way around this.
|
// Unfortunately this will show 2 toasts on Android 13+, but no way around this.
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || (withTimestamp && currentVideoTimeInSeconds > 0)) {
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || (withTimestamp && currentVideoTimeInSeconds > 0)) {
|
||||||
|
|
@ -40,7 +40,7 @@ public class CopyVideoUrlPatch {
|
||||||
: str("revanced_share_copy_url_success"));
|
: str("revanced_share_copy_url_success"));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Logger.printException(() -> "Failed to generate video url", e);
|
Logger.printException(() -> "Failed to generate video URL", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
|
@ -12,6 +15,13 @@ public class DisableHapticFeedbackPatch {
|
||||||
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
|
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean disablePreciseSeekingVibrate() {
|
||||||
|
return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
|
|
@ -22,8 +32,10 @@ public class DisableHapticFeedbackPatch {
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean disablePreciseSeekingVibrate() {
|
public static Object disableTapAndHoldVibrate(Object vibrator) {
|
||||||
return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get();
|
return Settings.DISABLE_HAPTIC_FEEDBACK_TAP_AND_HOLD.get()
|
||||||
|
? null
|
||||||
|
: vibrator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,4 +44,29 @@ public class DisableHapticFeedbackPatch {
|
||||||
public static boolean disableZoomVibrate() {
|
public static boolean disableZoomVibrate() {
|
||||||
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
|
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void vibrate(Vibrator vibrator, VibrationEffect vibrationEffect) {
|
||||||
|
if (disableVibrate()) return;
|
||||||
|
vibrator.vibrate(vibrationEffect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public static void vibrate(Vibrator vibrator, long milliseconds) {
|
||||||
|
if (disableVibrate()) return;
|
||||||
|
vibrator.vibrate(milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean disableVibrate() {
|
||||||
|
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get()
|
||||||
|
&& Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get()
|
||||||
|
&& Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get()
|
||||||
|
&& Settings.DISABLE_HAPTIC_FEEDBACK_TAP_AND_HOLD.get()
|
||||||
|
&& Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue