Compare commits

..

1 commit

Author SHA1 Message Date
xehpuk
19337d619e fix(Strava): Fix Overwrite media upload parameters patch 2026-01-16 01:50:35 +01:00
1101 changed files with 98877 additions and 117011 deletions

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*.{kt,kts}]
ktlint_code_style = intellij_idea
ktlint_standard_no-wildcard-imports = disabled

View file

@ -2,10 +2,6 @@ name: Build pull request
on:
workflow_dispatch:
inputs:
pr:
description: "PR to build"
required: true
pull_request:
branches:
- dev
@ -14,14 +10,9 @@ jobs:
release:
name: Build
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ inputs.pr && format('refs/pull/{0}/merge', inputs.pr) || github.ref }}
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v5
@ -34,13 +25,12 @@ jobs:
- name: Build
env:
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid --no-daemon
- name: Upload artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v5
with:
name: revanced-patches
path: patches/build/libs
archive: false

View file

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Open pull request
uses: repo-sync/pull-request@v2

View file

@ -1,6 +1,8 @@
name: Pull strings
on:
schedule:
- cron: "0 0 * * 0"
workflow_dispatch:
jobs:
@ -12,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
with:
ref: dev
persist-credentials: true
clean: true
- name: Pull strings
uses: crowdin/github-action@v2
@ -23,29 +25,16 @@ jobs:
config: crowdin.yml
upload_sources: false
download_translations: true
push_translations: false
skip_ref_checkout: true
localization_branch_name: feat/translations
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Process strings
run: |
chmod -R 777 patches/src/main/resources
./gradlew processStringsFromCrowdin
env:
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore: Sync translations from Crowdin"
push_options: '--force'
branch: feat/translations
- name: Open pull request
if: github.event_name == 'workflow_dispatch'
uses: repo-sync/pull-request@v2
with:
source_branch: feat/translations

View file

@ -14,13 +14,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Process strings
- name: Preprocess strings
env:
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew processStringsForCrowdin
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew clean preprocessCrowdinStrings
- name: Push strings
uses: crowdin/github-action@v2

View file

@ -15,11 +15,10 @@ jobs:
packages: write
id-token: write
attestations: write
artifact-metadata: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Setup Java
uses: actions/setup-java@v5
@ -32,7 +31,7 @@ jobs:
- name: Build
env:
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid clean
@ -57,12 +56,10 @@ jobs:
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
- name: Attest
if: steps.release.outputs.new_release_published == 'true'
uses: actions/attest@v4
uses: actions/attest-build-provenance@v3
with:
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
subject-path: patches/build/libs/patches-*.rvp

View file

@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v5
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2
uses: gradle-update/update-gradle-wrapper-action@v1
with:
target-branch: dev

View file

@ -1,763 +1,3 @@
# [6.1.0](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.1.0) (2026-03-18)
### Bug Fixes
* **Export internal data documents provider:** Correct S_IFLNK constant and symlink detection mask ([#6819](https://github.com/ReVanced/revanced-patches/issues/6819)) ([252617b](https://github.com/ReVanced/revanced-patches/commit/252617b8dd3f24e1ff9a04ba1d91b43dc29bd757))
* **YouTube - Custom branding:** Fix double icons and change default branding to ReVanced ([#6806](https://github.com/ReVanced/revanced-patches/issues/6806)) ([e51c529](https://github.com/ReVanced/revanced-patches/commit/e51c5292c171325e7cfa0f5ee85474d9b3961a34))
### Features
* Add `Spoof root of trust` and `Spoof keystore security level` patch ([#6751](https://github.com/ReVanced/revanced-patches/issues/6751)) ([4bc8c7c](https://github.com/ReVanced/revanced-patches/commit/4bc8c7c0f60a095533f07dc281f0320f8eb22f3c))
* **Announcements:** Support ReVanced API v5 announcements ([a05386e](https://github.com/ReVanced/revanced-patches/commit/a05386e8bc24c085b5c74f3674c402c5dd5ad468))
* Change contact email in patches about ([df1c3a4](https://github.com/ReVanced/revanced-patches/commit/df1c3a4a70fd2595d77b539299f1f7301bc60d24))
* **Instagram:** Add `Enable location sticker redesign` patch ([#6808](https://github.com/ReVanced/revanced-patches/issues/6808)) ([4b699da](https://github.com/ReVanced/revanced-patches/commit/4b699da220e5d1527c390792b6228e2d9cffedb7))
* **Spoof video streams:** Add Android Reel client to fix playback issues ([#6830](https://github.com/ReVanced/revanced-patches/issues/6830)) ([4b6c3e3](https://github.com/ReVanced/revanced-patches/commit/4b6c3e312328fbf6a1c7065e27d8ff04573e58be))
# [6.1.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.3...v6.1.0-dev.4) (2026-03-18)
### Bug Fixes
* **YouTube - Custom branding:** Fix double icons and change default branding to ReVanced ([#6806](https://github.com/ReVanced/revanced-patches/issues/6806)) ([e51c529](https://github.com/ReVanced/revanced-patches/commit/e51c5292c171325e7cfa0f5ee85474d9b3961a34))
### Features
* Add `Spoof root of trust` and `Spoof keystore security level` patch ([#6751](https://github.com/ReVanced/revanced-patches/issues/6751)) ([4bc8c7c](https://github.com/ReVanced/revanced-patches/commit/4bc8c7c0f60a095533f07dc281f0320f8eb22f3c))
* **Instagram:** Add `Enable location sticker redesign` patch ([#6808](https://github.com/ReVanced/revanced-patches/issues/6808)) ([4b699da](https://github.com/ReVanced/revanced-patches/commit/4b699da220e5d1527c390792b6228e2d9cffedb7))
# [6.1.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.2...v6.1.0-dev.3) (2026-03-18)
### Features
* **Spoof video streams:** Add Android Reel client to fix playback issues ([#6830](https://github.com/ReVanced/revanced-patches/issues/6830)) ([4b6c3e3](https://github.com/ReVanced/revanced-patches/commit/4b6c3e312328fbf6a1c7065e27d8ff04573e58be))
# [6.1.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v6.1.0-dev.1...v6.1.0-dev.2) (2026-03-17)
### Features
* **Announcements:** Support ReVanced API v5 announcements ([a05386e](https://github.com/ReVanced/revanced-patches/commit/a05386e8bc24c085b5c74f3674c402c5dd5ad468))
# [6.1.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.2-dev.1...v6.1.0-dev.1) (2026-03-16)
### Features
* Change contact email in patches about ([df1c3a4](https://github.com/ReVanced/revanced-patches/commit/df1c3a4a70fd2595d77b539299f1f7301bc60d24))
## [6.0.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.1...v6.0.2-dev.1) (2026-03-16)
### Bug Fixes
* **Export internal data documents provider:** Correct S_IFLNK constant and symlink detection mask ([#6819](https://github.com/ReVanced/revanced-patches/issues/6819)) ([252617b](https://github.com/ReVanced/revanced-patches/commit/252617b8dd3f24e1ff9a04ba1d91b43dc29bd757))
## [6.0.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.0...v6.0.1) (2026-03-15)
### Bug Fixes
* **GmsCore support:** use `prefixOrReplace` for non-matching APP_AUTHORITIES in content URL transformation ([#6801](https://github.com/ReVanced/revanced-patches/issues/6801)) ([8f6f128](https://github.com/ReVanced/revanced-patches/commit/8f6f128d718c20c56668ed3801b434a5cbb04dfd))
* **YouTube Music - Hide buttons:** Crashes on startup due to null LayoutParams ([#6799](https://github.com/ReVanced/revanced-patches/issues/6799)) ([3e32c38](https://github.com/ReVanced/revanced-patches/commit/3e32c387328b061f33b361ed022ae18e447a7904))
* **YouTube:** Use correct query parameters for DeArrow requests ([#6780](https://github.com/ReVanced/revanced-patches/issues/6780)) ([02a48e7](https://github.com/ReVanced/revanced-patches/commit/02a48e7a5f2b1ffd64a80651b49666de27ab7014))
## [6.0.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v6.0.1-dev.2...v6.0.1-dev.3) (2026-03-15)
### Bug Fixes
* **GmsCore support:** use `prefixOrReplace` for non-matching APP_AUTHORITIES in content URL transformation ([#6801](https://github.com/ReVanced/revanced-patches/issues/6801)) ([8f6f128](https://github.com/ReVanced/revanced-patches/commit/8f6f128d718c20c56668ed3801b434a5cbb04dfd))
## [6.0.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v6.0.1-dev.1...v6.0.1-dev.2) (2026-03-15)
### Bug Fixes
* **YouTube Music - Hide buttons:** Crashes on startup due to null LayoutParams ([#6799](https://github.com/ReVanced/revanced-patches/issues/6799)) ([3e32c38](https://github.com/ReVanced/revanced-patches/commit/3e32c387328b061f33b361ed022ae18e447a7904))
## [6.0.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v6.0.0...v6.0.1-dev.1) (2026-03-15)
### Bug Fixes
* **YouTube:** Use correct query parameters for DeArrow requests ([#6780](https://github.com/ReVanced/revanced-patches/issues/6780)) ([02a48e7](https://github.com/ReVanced/revanced-patches/commit/02a48e7a5f2b1ffd64a80651b49666de27ab7014))
# [6.0.0](https://github.com/ReVanced/revanced-patches/compare/v5.50.2...v6.0.0) (2026-03-14)
* 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))
### 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))
* **Check environment:** Use another (also more suitable) API to circumvent a bug ([393700f](https://github.com/ReVanced/revanced-patches/commit/393700f74ac141bfa109988202707b40d35a64ea))
* **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))
* **Enable debugging:** Add missing preference to log protocol buffer ([26d8a9e](https://github.com/ReVanced/revanced-patches/commit/26d8a9e5f891e08fe3c23601e8238de6a301b8df))
* Fix return type check to match method successfully ([0a73452](https://github.com/ReVanced/revanced-patches/commit/0a734528dc4407571ae1dba3e80347bc9f236e3e))
* **GmsCore support:** Handle GmsCore flavors when checking for updates ([2aa19f5](https://github.com/ReVanced/revanced-patches/commit/2aa19f5995fd050c40b15331a77d58144a5a1f69))
* **GmsCore support:** Insert check after another missing necessary context hook ([3c0c5a8](https://github.com/ReVanced/revanced-patches/commit/3c0c5a86d8e24b47b1c30bc5a7fe994240014e2d))
* **GmsCore support:** Insert check after necessary context hook ([03e8e3d](https://github.com/ReVanced/revanced-patches/commit/03e8e3d75cb3b03987299885cea5eb615a5cef23))
* **GmsCore support:** Rename MicroG GmsCore specific strings as well and rename app specific strings correctly ([c2ac1f0](https://github.com/ReVanced/revanced-patches/commit/c2ac1f04a0ac180555a9d19e7ff41525487fbc6d))
* **GmsCore support:** Try replacing in strings before prefixing to handle more edge cases ([4d94a41](https://github.com/ReVanced/revanced-patches/commit/4d94a41c46f2d4e1bf33debc95b8aa84a64964bb))
* **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))
* **Hex:** Add back name, which was accidentally removed from the patch ([6a547a9](https://github.com/ReVanced/revanced-patches/commit/6a547a97e52b7914bb6602f3ecc2c6cecd50e946))
* **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))
* Move strings to correct patch ([4dfe3fb](https://github.com/ReVanced/revanced-patches/commit/4dfe3fb08812ed572e01e58a8604c1be9e989438))
* **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))
* **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))
* Rename string keys correctly ([16e00ab](https://github.com/ReVanced/revanced-patches/commit/16e00ab4c0ff10e58adea40c7de72658788fcd97))
* **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))
* Use correct string key ([9d55d00](https://github.com/ReVanced/revanced-patches/commit/9d55d00ff46a2cd18111a91a98dbc8e3137dd0ed))
* Use custom comparison block for strings in `anyOf` ([56a087d](https://github.com/ReVanced/revanced-patches/commit/56a087dbacf331ccadfe753cbc1ced77e318fc27))
* Use positional substitutes in strings where multiple are present ([aa8c87f](https://github.com/ReVanced/revanced-patches/commit/aa8c87f8650bd5def5f726f02be5d62d72a3007b))
* **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))
* **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:** Find resource id only for 21.05+ ([63161e9](https://github.com/ReVanced/revanced-patches/commit/63161e9fb357387685294e4a80de94cb351c6713))
* **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 - Playback speed:** Use correct extension method name ([b8b4cfb](https://github.com/ReVanced/revanced-patches/commit/b8b4cfbd016058a158364f4549e7ef6ed4d154e0))
* **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 - 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))
* **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 - 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 - 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))
* **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 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))
* **YouTube:** Add back missing custom filter by adding the preference to the correct screen ([2a10489](https://github.com/ReVanced/revanced-patches/commit/2a10489a869cbab1ed01502bc6fe9330c4052e06))
* **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))
* **Check environment patch:** Support another ReVanced Manager debug variant package name ([e4dea68](https://github.com/ReVanced/revanced-patches/commit/e4dea682c6640ce817d5e30cfddec953fe85436f))
* **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))
* **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))
* **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))
* 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))
* **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))
* Perform full search of free registers ([#6571](https://github.com/ReVanced/revanced-patches/issues/6571)) ([01ef43a](https://github.com/ReVanced/revanced-patches/commit/01ef43ababdf015f1ad3edaf45445da0e72199f2))
* **Photoshop Mix:** Add `Bypass login` patch ([#6745](https://github.com/ReVanced/revanced-patches/issues/6745)) ([24caae9](https://github.com/ReVanced/revanced-patches/commit/24caae98b7b4d61b388f644cc1512438e408e6b1))
* 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))
### BREAKING CHANGES
* Deprecated APIs have been removed, and various APIs now use the updated ReVanced Patcher v22 APIs.
# [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)
### Bug Fixes
* Add missing patch option descriptions ([16e42a7](https://github.com/ReVanced/revanced-patches/commit/16e42a75ecbf51e06432f1f6c96758f8d9bdb771))
## [5.50.1](https://github.com/ReVanced/revanced-patches/compare/v5.50.0...v5.50.1) (2026-02-15)
### Bug Fixes
* Fix broken release by bumping to v5.50.1 ([d416609](https://github.com/ReVanced/revanced-patches/commit/d4166092571b542925a59328d3d59fbc42eb29e3))
## [5.50.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.50.0...v5.50.1-dev.1) (2026-02-15)
### Bug Fixes
* Fix broken release by bumping to v5.50.1 ([d416609](https://github.com/ReVanced/revanced-patches/commit/d4166092571b542925a59328d3d59fbc42eb29e3))
# [5.49.0](https://github.com/ReVanced/revanced-patches/compare/v5.48.0...v5.49.0) (2026-02-15)
### Bug Fixes
* Disable `Prevent screenshot detection` by default ([#6511](https://github.com/ReVanced/revanced-patches/issues/6511)) ([5b5c502](https://github.com/ReVanced/revanced-patches/commit/5b5c50254d533faa0e04d542f4859cbef610713e))
* **Instagram - Open links externally:** Fix patch by handling >4-bit register ([#6538](https://github.com/ReVanced/revanced-patches/issues/6538)) ([f681a6f](https://github.com/ReVanced/revanced-patches/commit/f681a6ffd45f05a61743e7d272cd68c4b743be42))
* **Instagram:** Make `Change link sharing domain` and `Sanitize sharing links` work with latest versions again ([#6518](https://github.com/ReVanced/revanced-patches/issues/6518)) ([85a9079](https://github.com/ReVanced/revanced-patches/commit/85a9079c25760d0329e518e379eeefe3beeea143))
* **Letterboxd - Hide ads:** Fix patch by returning the correct return type ([#6527](https://github.com/ReVanced/revanced-patches/issues/6527)) ([80c34b9](https://github.com/ReVanced/revanced-patches/commit/80c34b9d74a42018a0cd52b4a584ee71206bf963))
* Process strings from Crowdin to strip the app/patch prefixes again ([e566efc](https://github.com/ReVanced/revanced-patches/commit/e566efc51fca45c6284406245a360685a8e90d74))
* **Strava:** Fix `Add media download` patch ([#6526](https://github.com/ReVanced/revanced-patches/issues/6526)) ([dc9e68b](https://github.com/ReVanced/revanced-patches/commit/dc9e68ba574dd9f35cd742cb63193c5d875addde))
### Features
* **FotMob:** Add `Hide ads` patch ([#6566](https://github.com/ReVanced/revanced-patches/issues/6566)) ([4b0b737](https://github.com/ReVanced/revanced-patches/commit/4b0b7374f21d13599ef2f1e2f5880e7589b0874e))
* **GmsCore support:** Reduce amount of necessary changes and add update check ([#6582](https://github.com/ReVanced/revanced-patches/issues/6582)) ([650e6a2](https://github.com/ReVanced/revanced-patches/commit/650e6a271075b57368432cd9d4294fd1ce26cceb))
* **Instagram:** Add `Disable analytics` patch ([#6531](https://github.com/ReVanced/revanced-patches/issues/6531)) ([ad92864](https://github.com/ReVanced/revanced-patches/commit/ad92864483a21d7eae7952c8f8429cde3d44e848))
* **Kleinanzeigen:** Add `Hide ads` patch ([#6533](https://github.com/ReVanced/revanced-patches/issues/6533)) ([bd6e544](https://github.com/ReVanced/revanced-patches/commit/bd6e544007d539ac2eb890d9bdcb6850435f96cb))
* **Kleinanzeigen:** Add `Hide PUR` patch ([#6558](https://github.com/ReVanced/revanced-patches/issues/6558)) ([4958ecf](https://github.com/ReVanced/revanced-patches/commit/4958ecf10c880e9e7f15dd2e58ebaefbf49e417a))
* **Microsoft Lens:** Remove migration to OneDrive ([#6551](https://github.com/ReVanced/revanced-patches/issues/6551)) ([e389632](https://github.com/ReVanced/revanced-patches/commit/e389632afd52403aba26b6981d098b93cea45e00))
* **Nothing X:** Add `Show K1 token(s)` patch ([#6490](https://github.com/ReVanced/revanced-patches/issues/6490)) ([421cb28](https://github.com/ReVanced/revanced-patches/commit/421cb2899ef5c0f100fb8007bae8b89137d0e41c))
* **Strava:** Add `Hide distractions` patch ([#6479](https://github.com/ReVanced/revanced-patches/issues/6479)) ([66b0852](https://github.com/ReVanced/revanced-patches/commit/66b0852f8fa57c82b09997337a304374883d8ba5))
* **YouTube Music:** Add `Hide layout components` patch ([#6365](https://github.com/ReVanced/revanced-patches/issues/6365)) ([71ce823](https://github.com/ReVanced/revanced-patches/commit/71ce8230a959dcaf2d8cd5dad1a4f21b88819aa0))
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([5edd9dc](https://github.com/ReVanced/revanced-patches/commit/5edd9dccae3b1ab4edf19771a771812e3c9ccf80))
# [5.50.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.7...v5.50.0-dev.8) (2026-02-15)
### Features
* **GmsCore support:** Reduce amount of necessary changes and add update check ([#6582](https://github.com/ReVanced/revanced-patches/issues/6582)) ([650e6a2](https://github.com/ReVanced/revanced-patches/commit/650e6a271075b57368432cd9d4294fd1ce26cceb))
# [5.50.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.6...v5.50.0-dev.7) (2026-02-12)
### Bug Fixes
* **Instagram:** Make `Change link sharing domain` and `Sanitize sharing links` work with latest versions again ([#6518](https://github.com/ReVanced/revanced-patches/issues/6518)) ([85a9079](https://github.com/ReVanced/revanced-patches/commit/85a9079c25760d0329e518e379eeefe3beeea143))
### Features
* **Instagram:** Add `Disable analytics` patch ([#6531](https://github.com/ReVanced/revanced-patches/issues/6531)) ([ad92864](https://github.com/ReVanced/revanced-patches/commit/ad92864483a21d7eae7952c8f8429cde3d44e848))
* **Kleinanzeigen:** Add `Hide PUR` patch ([#6558](https://github.com/ReVanced/revanced-patches/issues/6558)) ([4958ecf](https://github.com/ReVanced/revanced-patches/commit/4958ecf10c880e9e7f15dd2e58ebaefbf49e417a))
* **Microsoft Lens:** Remove migration to OneDrive ([#6551](https://github.com/ReVanced/revanced-patches/issues/6551)) ([e389632](https://github.com/ReVanced/revanced-patches/commit/e389632afd52403aba26b6981d098b93cea45e00))
# [5.50.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.5...v5.50.0-dev.6) (2026-02-06)
### Features
* **FotMob:** Add `Hide ads` patch ([#6566](https://github.com/ReVanced/revanced-patches/issues/6566)) ([4b0b737](https://github.com/ReVanced/revanced-patches/commit/4b0b7374f21d13599ef2f1e2f5880e7589b0874e))
# [5.50.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.4...v5.50.0-dev.5) (2026-02-01)
### Bug Fixes
* Process strings from Crowdin to strip the app/patch prefixes again ([e566efc](https://github.com/ReVanced/revanced-patches/commit/e566efc51fca45c6284406245a360685a8e90d74))
# [5.50.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.3...v5.50.0-dev.4) (2026-01-27)
### Bug Fixes
* **Instagram - Open links externally:** Fix patch by handling >4-bit register ([#6538](https://github.com/ReVanced/revanced-patches/issues/6538)) ([f681a6f](https://github.com/ReVanced/revanced-patches/commit/f681a6ffd45f05a61743e7d272cd68c4b743be42))
# [5.50.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.2...v5.50.0-dev.3) (2026-01-26)
### Features
* **Kleinanzeigen:** Add `Hide ads` patch ([#6533](https://github.com/ReVanced/revanced-patches/issues/6533)) ([bd6e544](https://github.com/ReVanced/revanced-patches/commit/bd6e544007d539ac2eb890d9bdcb6850435f96cb))
# [5.50.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.50.0-dev.1...v5.50.0-dev.2) (2026-01-25)
### Bug Fixes
* **Letterboxd - Hide ads:** Fix patch by returning the correct return type ([#6527](https://github.com/ReVanced/revanced-patches/issues/6527)) ([80c34b9](https://github.com/ReVanced/revanced-patches/commit/80c34b9d74a42018a0cd52b4a584ee71206bf963))
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.49.0-dev.1...v5.50.0-dev.1) (2026-01-25)
### Bug Fixes
* **Strava:** Fix `Add media download` patch ([#6526](https://github.com/ReVanced/revanced-patches/issues/6526)) ([dc9e68b](https://github.com/ReVanced/revanced-patches/commit/dc9e68ba574dd9f35cd742cb63193c5d875addde))
### Features
* **Nothing X:** Add `Show K1 token(s)` patch ([#6490](https://github.com/ReVanced/revanced-patches/issues/6490)) ([421cb28](https://github.com/ReVanced/revanced-patches/commit/421cb2899ef5c0f100fb8007bae8b89137d0e41c))
* **Strava:** Add `Hide distractions` patch ([#6479](https://github.com/ReVanced/revanced-patches/issues/6479)) ([66b0852](https://github.com/ReVanced/revanced-patches/commit/66b0852f8fa57c82b09997337a304374883d8ba5))
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([5edd9dc](https://github.com/ReVanced/revanced-patches/commit/5edd9dccae3b1ab4edf19771a771812e3c9ccf80))
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.49.0-dev.1...v5.50.0-dev.1) (2026-01-22)
### Features
* **Nothing X:** Add `Show K1 token(s)` patch ([#6490](https://github.com/ReVanced/revanced-patches/issues/6490)) ([421cb28](https://github.com/ReVanced/revanced-patches/commit/421cb2899ef5c0f100fb8007bae8b89137d0e41c))
* **Strava:** Add `Hide distractions` patch ([#6479](https://github.com/ReVanced/revanced-patches/issues/6479)) ([66b0852](https://github.com/ReVanced/revanced-patches/commit/66b0852f8fa57c82b09997337a304374883d8ba5))
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([5edd9dc](https://github.com/ReVanced/revanced-patches/commit/5edd9dccae3b1ab4edf19771a771812e3c9ccf80))
# [5.50.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.0...v5.50.0-dev.1) (2026-01-22)
### Features
* **YouTube Music:** Add `Unlock Android Auto Media Browser` patch ([#6477](https://github.com/ReVanced/revanced-patches/issues/6477)) ([89645dc](https://github.com/ReVanced/revanced-patches/commit/89645dcc2e13603b8f2fedb5e16231cb396e5965))
# [5.49.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.1-dev.1...v5.49.0-dev.1) (2026-01-22)
### Features
* **YouTube Music:** Add `Hide layout components` patch ([#6365](https://github.com/ReVanced/revanced-patches/issues/6365)) ([71ce823](https://github.com/ReVanced/revanced-patches/commit/71ce8230a959dcaf2d8cd5dad1a4f21b88819aa0))
## [5.48.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.48.0...v5.48.1-dev.1) (2026-01-21)
### Bug Fixes
* Disable `Prevent screenshot detection` by default ([#6511](https://github.com/ReVanced/revanced-patches/issues/6511)) ([5b5c502](https://github.com/ReVanced/revanced-patches/commit/5b5c50254d533faa0e04d542f4859cbef610713e))
# [5.48.0](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0) (2026-01-19)
### Bug Fixes
* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](https://github.com/ReVanced/revanced-patches/commit/eecc44b9567bf2ca72ac99e0dafa483a6803c0f9))
* **Disney+ - Skip ads:** Remove unsupported package names ([#6422](https://github.com/ReVanced/revanced-patches/issues/6422)) ([44e7dbc](https://github.com/ReVanced/revanced-patches/commit/44e7dbcf4d7eaf94dd0164baba847d3e19250154))
* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8))
* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](https://github.com/ReVanced/revanced-patches/commit/71c6cb569ebf7b93cf73ee391839e5220557ce7c))
* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](https://github.com/ReVanced/revanced-patches/commit/a429824bb77b49aea14b0b54f2204ae24d5209a1))
* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](https://github.com/ReVanced/revanced-patches/commit/87247590de3db74680cb02ba1d87bf683b2269e2))
* **YouTube - Hide layout components:** Hide new type of crowdfunding box ([#6380](https://github.com/ReVanced/revanced-patches/issues/6380)) ([dc69f24](https://github.com/ReVanced/revanced-patches/commit/dc69f2433e2650654e2dffdd76b0b0c8a52bf515))
### Features
* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305))
* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](https://github.com/ReVanced/revanced-patches/commit/83c0127ebb8f53ab8a067758619faaac5596c145))
* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c))
* **Instagram - Hides navigation buttons:** Add more buttons to hide ([#6390](https://github.com/ReVanced/revanced-patches/issues/6390)) ([6bb6281](https://github.com/ReVanced/revanced-patches/commit/6bb62811493da04812cc3e392e68d874f95cbef9))
* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](https://github.com/ReVanced/revanced-patches/commit/8725a49ba3a06fee0280ffcf4be62cd960cd301e))
* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](https://github.com/ReVanced/revanced-patches/commit/18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48))
* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](https://github.com/ReVanced/revanced-patches/commit/3401467a6d49fc75b6757a15e5c848330c1b7307))
* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc))
* **ProtonVPN:** Add `Unlock split tunneling` patch ([#6353](https://github.com/ReVanced/revanced-patches/issues/6353)) ([e0f3346](https://github.com/ReVanced/revanced-patches/commit/e0f33468e6e96b9f10cf35ec67622d6488528c90))
* **SBS On Demand:** Add `Remove ads` patch ([#6378](https://github.com/ReVanced/revanced-patches/issues/6378)) ([315931c](https://github.com/ReVanced/revanced-patches/commit/315931cbf8f61cd4b3a54ace1ff03685d748614c))
* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](https://github.com/ReVanced/revanced-patches/commit/4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f))
* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](https://github.com/ReVanced/revanced-patches/commit/778d13ce8b28ca6df3a665530320e4a21a27ae44))
* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](https://github.com/ReVanced/revanced-patches/commit/c47beae21376dd17ab8bc09afe73e9094481bde9))
* **Strava:** Add `Disable Quick Edit` patch ([#6452](https://github.com/ReVanced/revanced-patches/issues/6452)) ([f5cbb31](https://github.com/ReVanced/revanced-patches/commit/f5cbb31724d15f7e939b96ee0186fd0a108f9fdc))
* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](https://github.com/ReVanced/revanced-patches/commit/8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc))
* **Strava:** Add `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567))
* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](https://github.com/ReVanced/revanced-patches/commit/19f146c01dc381b3cccd61e61ba4901872ff12d8))
# [5.48.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.12...v5.48.0-dev.13) (2026-01-19)
### Features
* Add `Prevent screenshot detection` patch ([#6482](https://github.com/ReVanced/revanced-patches/issues/6482)) ([83c0127](https://github.com/ReVanced/revanced-patches/commit/83c0127ebb8f53ab8a067758619faaac5596c145))
# [5.48.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.11...v5.48.0-dev.12) (2026-01-19)
### Features
* **Instagram:** Add `Remove build expired popup` patch ([#6488](https://github.com/ReVanced/revanced-patches/issues/6488)) ([18c0b04](https://github.com/ReVanced/revanced-patches/commit/18c0b04f0cd1bf8cd78b05af3b8ebe3a6a5f9e48))
* **Strava:** Add `Add 'Give Kudos' button to 'Group Activity'` patch ([#6475](https://github.com/ReVanced/revanced-patches/issues/6475)) ([4c4ba1c](https://github.com/ReVanced/revanced-patches/commit/4c4ba1c78c9f4568a2b572f5c69e9c6c734e1a7f))
# [5.48.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.10...v5.48.0-dev.11) (2026-01-19)
### Features
* **Instagram:** Add `Hide highlights tray` patch ([#6489](https://github.com/ReVanced/revanced-patches/issues/6489)) ([8725a49](https://github.com/ReVanced/revanced-patches/commit/8725a49ba3a06fee0280ffcf4be62cd960cd301e))
# [5.48.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.9...v5.48.0-dev.10) (2026-01-19)
### Bug Fixes
* **Boost for Reddit - Fix missing audio in video downloads:** Make it work again by reflecting Reddits latest changes ([#6500](https://github.com/ReVanced/revanced-patches/issues/6500)) ([eecc44b](https://github.com/ReVanced/revanced-patches/commit/eecc44b9567bf2ca72ac99e0dafa483a6803c0f9))
* **Instagram:** `Sanitize sharing links` ([#6483](https://github.com/ReVanced/revanced-patches/issues/6483)) ([8724759](https://github.com/ReVanced/revanced-patches/commit/87247590de3db74680cb02ba1d87bf683b2269e2))
### Features
* **Instagram:** Disable `Disable Reels scrolling` by default ([3401467](https://github.com/ReVanced/revanced-patches/commit/3401467a6d49fc75b6757a15e5c848330c1b7307))
* **Strava:** Add `Add media download` patch ([#6449](https://github.com/ReVanced/revanced-patches/issues/6449)) ([778d13c](https://github.com/ReVanced/revanced-patches/commit/778d13ce8b28ca6df3a665530320e4a21a27ae44))
* **YouTube:** Add `Pause on audio interrupt` patch ([#6464](https://github.com/ReVanced/revanced-patches/issues/6464)) ([19f146c](https://github.com/ReVanced/revanced-patches/commit/19f146c01dc381b3cccd61e61ba4901872ff12d8))
# [5.48.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.8...v5.48.0-dev.9) (2026-01-08)

View file

@ -1,7 +1,14 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -1,6 +1,9 @@
android {
defaultConfig {
minSdk = 23
namespace = "app.revanced.extension"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View file

@ -1,7 +1,14 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -31,10 +31,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
private static final String[] directoryColumns =
{"document_id", "mime_type", "_display_name", "last_modified", "flags",
"_size", "full_path", "lstat_info"};
@SuppressWarnings("OctalInteger")
private static final int S_IFMT = 0170000;
@SuppressWarnings("OctalInteger")
private static final int S_IFLNK = 0120000;
private static final int S_IFLNK = 0x8000;
private String packageName;
private File dataDirectory;
@ -50,7 +47,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
if (root.isDirectory()) {
try {
// Only delete recursively if the directory is not a symlink
if ((Os.lstat(root.getPath()).st_mode & S_IFMT) != S_IFLNK) {
if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) {
File[] files = root.listFiles();
if (files != null) {
for (File file : files) {
@ -327,7 +324,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider {
sb.append(";");
sb.append(lstat.st_gid);
// Append symlink target if it is a symlink
if ((lstat.st_mode & S_IFMT) == S_IFLNK) {
if ((lstat.st_mode & S_IFLNK) == S_IFLNK) {
sb.append(";");
sb.append(Os.readlink(path));
}

View file

@ -1,8 +1,15 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
aidl = true
}

View file

@ -1,4 +1,4 @@
package app.revanced.extension.play;
package app.revanced.extension.playintegrity;
import android.content.Context;
import android.content.Intent;

View file

@ -1,7 +1,14 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -1,7 +1,14 @@
android {
namespace = "app.revanced.extension"
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {

View file

@ -3,9 +3,3 @@ dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 22
}
}

View file

@ -4,9 +4,3 @@ dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -1,10 +1,4 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:cricbuzz:stub"))
}
android {
defaultConfig {
minSdk = 21
}
}
}

View file

@ -1,9 +1,3 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -10,6 +10,6 @@ public final class SanitizeSharingLinksPatch {
* Injection point.
*/
public static String sanitizeSharingLink(String url) {
return sanitizer.sanitizeURLString(url);
return sanitizer.sanitizeUrlString(url);
}
}

View file

@ -10,6 +10,6 @@ public final class SanitizeSharingLinksPatch {
* Injection point.
*/
public static String sanitizeSharingLink(String url) {
return sanitizer.sanitizeURLString(url);
return sanitizer.sanitizeUrlString(url);
}
}

View file

@ -1,9 +1,3 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}
android {
defaultConfig {
minSdk = 24
}
}

View file

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

View file

@ -5,15 +5,14 @@ import static app.revanced.extension.shared.Utils.hideViewUnderCondition;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import app.revanced.extension.music.settings.Settings;
@SuppressWarnings("unused")
public class NavigationBarPatch {
@NonNull
private static String lastYTNavigationEnumName = "";
public static void setLastAppNavigationEnum(@Nullable Enum<?> ytNavigationEnumName) {
@ -26,7 +25,7 @@ public class NavigationBarPatch {
hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview);
}
public static void hideNavigationButton(View view) {
public static void hideNavigationButton(@NonNull View view) {
// Hide entire navigation bar.
if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) {
hideViewUnderCondition(true, (View) view.getParent());
@ -35,7 +34,7 @@ public class NavigationBarPatch {
// Hide navigation buttons based on their type.
for (NavigationButton button : NavigationButton.values()) {
if (button.ytEnumNames.contains(lastYTNavigationEnumName)) {
if (button.ytEnumNames.equals(lastYTNavigationEnumName)) {
hideViewUnderCondition(button.hidden, view);
break;
}
@ -44,41 +43,30 @@ public class NavigationBarPatch {
private enum NavigationButton {
HOME(
Arrays.asList(
"TAB_HOME"
),
"TAB_HOME",
Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get()
),
SAMPLES(
Arrays.asList(
"TAB_SAMPLES"
),
"TAB_SAMPLES",
Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get()
),
EXPLORE(
Arrays.asList(
"TAB_EXPLORE"
),
"TAB_EXPLORE",
Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get()
),
LIBRARY(
Arrays.asList(
"LIBRARY_MUSIC",
"TAB_BOOKMARK" // YouTube Music 8.24+
),
"LIBRARY_MUSIC",
Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get()
),
UPGRADE(
Arrays.asList(
"TAB_MUSIC_PREMIUM"
),
"TAB_MUSIC_PREMIUM",
Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get()
);
private final List<String> ytEnumNames;
private final String ytEnumNames;
private final boolean hidden;
NavigationButton(List<String> ytEnumNames, boolean hidden) {
NavigationButton(@NonNull String ytEnumNames, boolean hidden) {
this.ytEnumNames = ytEnumNames;
this.hidden = hidden;
}

View file

@ -1,7 +1,7 @@
package app.revanced.extension.music.patches.spoof;
import static app.revanced.extension.music.settings.Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_REEL;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_NO_SDK;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_43_32;
import static app.revanced.extension.shared.spoof.ClientType.ANDROID_VR_1_61_48;
import static app.revanced.extension.shared.spoof.ClientType.VISIONOS;
@ -18,8 +18,8 @@ public class SpoofVideoStreamsPatch {
*/
public static void setClientOrderToUse() {
List<ClientType> availableClients = List.of(
ANDROID_REEL,
ANDROID_VR_1_43_32,
ANDROID_NO_SDK,
VISIONOS,
ANDROID_VR_1_61_48
);

View file

@ -8,11 +8,9 @@ import android.preference.PreferenceFragment;
import android.view.View;
import android.widget.Toolbar;
import app.revanced.extension.music.VersionCheckUtils;
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
import app.revanced.extension.music.settings.search.MusicSearchViewController;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseActivityHook;
@ -24,24 +22,6 @@ public class MusicActivityHook extends BaseActivityHook {
@SuppressLint("StaticFieldLeak")
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.
*/
@ -66,7 +46,15 @@ public class MusicActivityHook extends BaseActivityHook {
// Override the default YouTube Music theme to increase start padding of list items.
// Custom style located in resources/music/values/style.xml
activity.setTheme(Utils.getResourceIdentifierOrThrow(
ResourceType.STYLE, "Theme.ReVanced.YouTubeMusic.Settings"));
"Theme.ReVanced.YouTubeMusic.Settings", "style"));
}
/**
* Returns the resource ID for the YouTube Music settings layout.
*/
@Override
protected int getContentViewResourceId() {
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
}
/**
@ -135,14 +123,4 @@ public class MusicActivityHook extends BaseActivityHook {
public static boolean handleFinish() {
return MusicSearchViewController.handleFinish(searchViewController);
}
/**
* Injection point.
* <p>
* Decides whether to use bold icons.
*/
@SuppressWarnings("unused")
public static boolean useBoldIcons(boolean original) {
return Utils.appIsUsingBoldIcons();
}
}

View file

@ -4,12 +4,12 @@ import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent;
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.spoof.ClientType;
public class Settings extends YouTubeAndMusicSettings {
public class Settings extends BaseSettings {
// Ads
public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true);
@ -35,7 +35,7 @@ public class Settings extends YouTubeAndMusicSettings {
// Miscellaneous
public static final EnumSetting<ClientType> SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type",
ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS));
ClientType.ANDROID_VR_1_43_32, true, parent(SPOOF_VIDEO_STREAMS));
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true);
}

View file

@ -1,10 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nothingx:stub"))
}
android {
defaultConfig {
minSdk = 23
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,9 +2,3 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nunl:stub"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -2,9 +2,3 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:primevideo:stub"))
}
android {
defaultConfig {
minSdk = 21
}
}

View file

@ -1,9 +1,3 @@
dependencies {
compileOnly(project(":extensions:reddit:stub"))
}
android {
defaultConfig {
minSdk = 28
}
}

View file

@ -2,9 +2,3 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:samsung:radio:stub"))
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -2,9 +2,3 @@ dependencies {
implementation(project(":extensions:shared:library"))
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 23
}
}

View file

@ -1,5 +1,5 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.android.library)
}
android {
@ -19,6 +19,4 @@ android {
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
compileOnly(libs.protobuf.javalite)
implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements"))
}

View file

@ -1,5 +1,8 @@
package app.revanced.extension.shared;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
@ -17,53 +20,43 @@ import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.ui.CustomDialog;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.ui.CustomDialog;
@SuppressWarnings("unused")
public class GmsCoreSupport {
private static GmsCore gmsCore = GmsCore.UNKNOWN;
static {
for (GmsCore core : GmsCore.values()) {
if (core.getGroupId().equals(getGmsCoreVendorGroupId())) {
GmsCoreSupport.gmsCore = core;
break;
}
}
}
private static final String GMS_CORE_PACKAGE_NAME
= getGmsCoreVendorGroupId() + ".android.gms";
private static final Uri GMS_CORE_PROVIDER
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
private static final String DONT_KILL_MY_APP_URL
= "https://dontkillmyapp.com/";
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
= new Route(GET, "/api/v2/{manufacturer}.json");
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
= "?app=MicroG";
private static final String BUILD_MANUFACTURER
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
/**
* Injection point.
* If a manufacturer specific page exists on DontKillMyApp.
*/
public static void checkGmsCore(Activity context) {
gmsCore.check(context);
}
@Nullable
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
private static String getOriginalPackageName() {
return null; // Modified during patching.
return null; // Modified during patching.
}
private static String getGmsCoreVendorGroupId() {
return "app.revanced"; // Modified during patching.
}
/**
* @return If the current package name is the same as the original unpatched app.
* If `GmsCore support` was not included during patching, this returns true;
* If `GmsCore support` was not included during patching, this returns true;
*/
public static boolean isPackageNameOriginal() {
String originalPackageName = getOriginalPackageName();
@ -71,342 +64,203 @@ public class GmsCoreSupport {
|| originalPackageName.equals(Utils.getContext().getPackageName());
}
private enum GmsCore {
REVANCED("app.revanced", "https://github.com/revanced/gmscore/releases/latest", () -> {
try {
HttpURLConnection connection = Requester.getConnectionFromRoute(
"https://api.github.com",
new Route(GET, "/repos/revanced/gmscore/releases/latest")
);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
private static void open(String queryOrLink) {
Logger.printInfo(() -> "Opening link: " + queryOrLink);
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
Logger.printDebug(() -> "GitHub API returned status code: " + responseCode);
return null;
}
Intent intent;
try {
// Check if queryOrLink is a valid URL.
new URL(queryOrLink);
// Parse the response
JSONObject releaseData = Requester.parseJSONObject(connection);
String tagName = releaseData.optString("tag_name", "");
connection.disconnect();
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
} catch (MalformedURLException e) {
intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.putExtra(SearchManager.QUERY, queryOrLink);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Utils.getContext().startActivity(intent);
if (tagName.isEmpty()) {
Logger.printDebug(() -> "No tag_name found in GitHub release data");
return null;
}
// Gracefully exit, otherwise the broken app will continue to run.
System.exit(0);
}
if (tagName.startsWith("v")) tagName = tagName.substring(1);
private static void showBatteryOptimizationDialog(Activity context,
String dialogMessageRef,
String positiveButtonTextRef,
DialogInterface.OnClickListener onPositiveClickListener) {
// Use a delay to allow the activity to finish initializing.
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
context,
str("gms_core_dialog_title"), // Title.
str(dialogMessageRef), // Message.
null, // No EditText.
str(positiveButtonTextRef), // OK button text.
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
null, // No Cancel button action.
null, // No Neutral button text.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
return tagName;
} catch (Exception ex) {
Logger.printInfo(() -> "Failed to fetch latest GmsCore version from GitHub", ex);
return null;
Dialog dialog = dialogPair.first;
// Do not set cancelable to false, to allow using back button to skip the action,
// just in case the battery change can never be satisfied.
dialog.setCancelable(true);
// Show the dialog
Utils.showDialog(context, dialog);
}, 100);
}
/**
* Injection point.
*/
public static void checkGmsCore(Activity context) {
try {
// Verify the user has not included GmsCore for a root installation.
// GmsCore Support changes the package name, but with a mounted installation
// all manifest changes are ignored and the original package name is used.
if (isPackageNameOriginal()) {
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
// Cannot use localize text here, since the app will load resources
// from the unpatched app and all patch strings are missing.
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
// Do not exit. If the app exits before launch completes (and without
// opening another activity), then on some devices such as Pixel phone Android 10
// no toast will be shown and the app will continually relaunch
// with the appearance of a hung app.
}
}),
UNKNOWN(getGmsCoreVendorGroupId(), getGmsCoreVendorGroupId() + "android.gms", () -> null);
private static final String DONT_KILL_MY_APP_URL
= "https://dontkillmyapp.com/";
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
= new Route(GET, "/api/v2/{manufacturer}.json");
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
= "?app=MicroG";
private static final String BUILD_MANUFACTURER
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
/**
* If a manufacturer specific page exists on DontKillMyApp.
*/
@Nullable
private volatile Boolean dontKillMyAppManufacturerSupported;
private final String groupId;
private final String packageName;
private final String downloadQuery;
private final GetLatestVersion getLatestVersion;
private final Uri gmsCoreProvider;
GmsCore(String groupId, String downloadQuery, GetLatestVersion getLatestVersion) {
this.groupId = groupId;
this.packageName = groupId + ".android.gms";
this.gmsCoreProvider = Uri.parse("content://" + groupId + ".android.gsf.gservices/prefix");
this.downloadQuery = downloadQuery;
this.getLatestVersion = getLatestVersion;
}
String getGroupId() {
return groupId;
}
void check(Activity context) {
checkInstallation(context);
checkUpdates(context);
}
private void checkInstallation(Activity context) {
// Verify GmsCore is installed.
try {
// Verify the user has not included GmsCore for a root installation.
// GmsCore Support changes the package name, but with a mounted installation
// all manifest changes are ignored and the original package name is used.
if (isPackageNameOriginal()) {
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
// Cannot use localize text here, since the app will load resources
// from the unpatched app and all patch strings are missing.
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
// Do not exit. If the app exits before launch completes (and without
// opening another activity), then on some devices such as Pixel phone Android 10
// no toast will be shown and the app will continually relaunch
// with the appearance of a hung app.
}
// Verify GmsCore is installed.
try {
PackageManager manager = context.getPackageManager();
manager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printInfo(() -> "GmsCore was not found");
// Cannot show a dialog and must show a toast,
// because on some installations the app crashes before a dialog can be displayed.
Utils.showToastLong(str("revanced_gms_core_toast_not_installed_message"));
open(downloadQuery);
return;
}
// Check if GmsCore is whitelisted from battery optimizations.
if (isAndroidAutomotive(context)) {
// Ignore Android Automotive devices (Google built-in),
// as there is no way to disable battery optimizations.
Logger.printDebug(() -> "Device is Android Automotive");
} else if (batteryOptimizationsEnabled(context)) {
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
showBatteryOptimizationDialog(context,
"revanced_gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
"revanced_gms_core_dialog_continue_text",
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
return;
}
// Check if GmsCore is currently running in the background.
var client = context.getContentResolver().acquireContentProviderClient(gmsCoreProvider);
//noinspection TryFinallyCanBeTryWithResources
try {
if (client == null) {
Logger.printInfo(() -> "GmsCore is not running in the background");
checkIfDontKillMyAppSupportsManufacturer();
showBatteryOptimizationDialog(context,
"revanced_gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
"gmsrevanced_gms_core_log_open_website_text",
(dialog, id) -> openDontKillMyApp());
}
} finally {
if (client != null) client.close();
}
} catch (Exception ex) {
Logger.printException(() -> "checkGmsCore failure", ex);
}
}
private void checkUpdates(Activity context) {
if (!BaseSettings.GMS_CORE_CHECK_UPDATES.get()) {
Logger.printDebug(() -> "GmsCore update check is disabled in settings");
PackageManager manager = context.getPackageManager();
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printInfo(() -> "GmsCore was not found");
// Cannot show a dialog and must show a toast,
// because on some installations the app crashes before a dialog can be displayed.
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
open(getGmsCoreDownload());
return;
}
Utils.runOnBackgroundThread(() -> {
try {
PackageManager manager = context.getPackageManager();
var installedVersion = manager.getPackageInfo(packageName, 0).versionName;
// Check if GmsCore is whitelisted from battery optimizations.
if (isAndroidAutomotive(context)) {
// Ignore Android Automotive devices (Google built-in),
// as there is no way to disable battery optimizations.
Logger.printDebug(() -> "Device is Android Automotive");
} else if (batteryOptimizationsEnabled(context)) {
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
// 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;
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
"gms_core_dialog_continue_text",
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
return;
}
Logger.printDebug(() -> "Installed GmsCore version: " + finalInstalledVersion);
var latestVersion = getLatestVersion.get();
if (latestVersion == null || latestVersion.isEmpty()) {
Logger.printDebug(() -> "Could not get latest GmsCore version");
Utils.showToastLong(str("revanced_gms_core_toast_update_check_failed_message"));
return;
}
Logger.printDebug(() -> "Latest GmsCore version on GitHub: " + latestVersion);
// Compare versions
if (!installedVersion.equals(latestVersion)) {
Logger.printInfo(() -> "GmsCore update available. Installed: " + finalInstalledVersion
+ ", Latest: " + latestVersion);
showUpdateDialog(context, installedVersion, latestVersion);
} else {
Logger.printDebug(() -> "GmsCore is up to date");
}
} catch (Exception ex) {
Logger.printInfo(() -> "Could not check GmsCore updates", ex);
Utils.showToastLong(str("revanced_gms_core_toast_update_check_failed_message"));
}
});
}
private void open(String queryOrLink) {
Logger.printInfo(() -> "Opening link: " + queryOrLink);
Intent intent;
// Check if GmsCore is currently running in the background.
var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER);
//noinspection TryFinallyCanBeTryWithResources
try {
// Check if queryOrLink is a valid URL.
new URL(queryOrLink);
if (client == null) {
Logger.printInfo(() -> "GmsCore is not running in the background");
checkIfDontKillMyAppSupportsManufacturer();
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
} catch (MalformedURLException e) {
intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.putExtra(SearchManager.QUERY, queryOrLink);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Utils.getContext().startActivity(intent);
// Gracefully exit, otherwise the broken app will continue to run.
System.exit(0);
}
private void showUpdateDialog(Activity context, String installedVersion, String latestVersion) {
// Use a delay to allow the activity to finish initializing.
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> {
try {
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
context,
str("revanced_gms_core_dialog_title"),
String.format(str("revanced_gms_core_update_available_message"), latestVersion, installedVersion),
null,
str("revanced_gms_core_dialog_open_website_text"),
() -> open(downloadQuery),
() -> {
},
str("revanced_gms_core_dialog_cancel_text"),
null,
true
);
Dialog dialog = dialogPair.first;
dialog.setCancelable(true);
Utils.showDialog(context, dialog);
} catch (Exception ex) {
Logger.printException(() -> "Failed to show GmsCore update dialog", ex);
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
"gms_core_dialog_open_website_text",
(dialog, id) -> openDontKillMyApp());
}
}, 100);
}
private static void showBatteryOptimizationDialog(Activity context,
String dialogMessageRef,
String positiveButtonTextRef,
DialogInterface.OnClickListener onPositiveClickListener) {
// Use a delay to allow the activity to finish initializing.
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
context,
str("revanced_gms_core_dialog_title"), // Title.
str(dialogMessageRef), // Message.
null, // No EditText.
str(positiveButtonTextRef), // OK button text.
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
null, // No Cancel button action.
null, // No Neutral button text.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
Dialog dialog = dialogPair.first;
// Do not set cancelable to false to allow using back button to skip the action,
// just in case the battery change can never be satisfied.
dialog.setCancelable(true);
// Show the dialog
Utils.showDialog(context, dialog);
}, 100);
}
@SuppressLint("BatteryLife") // Permission is part of GmsCore
private void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.fromParts("package", packageName, null));
activity.startActivityForResult(intent, 0);
}
private void checkIfDontKillMyAppSupportsManufacturer() {
Utils.runOnBackgroundThread(() -> {
try {
final long start = System.currentTimeMillis();
HttpURLConnection connection = Requester.getConnectionFromRoute(
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
final boolean supported = connection.getResponseCode() == 200;
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
dontKillMyAppManufacturerSupported = supported;
} catch (Exception ex) {
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
+ BUILD_MANUFACTURER, ex);
dontKillMyAppManufacturerSupported = null;
}
});
}
private void openDontKillMyApp() {
final Boolean manufacturerSupported = dontKillMyAppManufacturerSupported;
String manufacturerPageToOpen;
if (manufacturerSupported == null) {
// Fetch has not completed yet. Only happens on extremely slow internet connections
// and the user spends less than 1 second reading what's on screen.
// Instead of waiting for the fetch (which may timeout),
// open the website without a vendor.
manufacturerPageToOpen = "";
} else if (manufacturerSupported) {
manufacturerPageToOpen = BUILD_MANUFACTURER;
} else {
// No manufacturer specific page exists. Open the general page.
manufacturerPageToOpen = "general";
} finally {
if (client != null) client.close();
}
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
}
/**
* @return If GmsCore is not whitelisted from battery optimizations.
*/
private boolean batteryOptimizationsEnabled(Context context) {
//noinspection ObsoleteSdkInt
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Android 5.0 does not have battery optimization settings.
return false;
}
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return !powerManager.isIgnoringBatteryOptimizations(packageName);
}
private boolean isAndroidAutomotive(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
} catch (Exception ex) {
Logger.printException(() -> "checkGmsCore failure", ex);
}
}
@FunctionalInterface
private interface GetLatestVersion {
String get();
@SuppressLint("BatteryLife") // Permission is part of GmsCore
private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null));
activity.startActivityForResult(intent, 0);
}
private static void checkIfDontKillMyAppSupportsManufacturer() {
Utils.runOnBackgroundThread(() -> {
try {
final long start = System.currentTimeMillis();
HttpURLConnection connection = Requester.getConnectionFromRoute(
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
final boolean supported = connection.getResponseCode() == 200;
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
} catch (Exception ex) {
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
+ BUILD_MANUFACTURER, ex);
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
}
});
}
private static void openDontKillMyApp() {
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
String manufacturerPageToOpen;
if (manufacturerSupported == null) {
// Fetch has not completed yet. Only happens on extremely slow internet connections
// and the user spends less than 1 second reading what's on screen.
// Instead of waiting for the fetch (which may timeout),
// open the website without a vendor.
manufacturerPageToOpen = "";
} else if (manufacturerSupported) {
manufacturerPageToOpen = BUILD_MANUFACTURER;
} else {
// No manufacturer specific page exists. Open the general page.
manufacturerPageToOpen = "general";
}
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
}
/**
* @return If GmsCore is not whitelisted from battery optimizations.
*/
private static boolean batteryOptimizationsEnabled(Context context) {
//noinspection ObsoleteSdkInt
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Android 5.0 does not have battery optimization settings.
return false;
}
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
}
private static boolean isAndroidAutomotive(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
}
private static String getGmsCoreDownload() {
final var vendorGroupId = getGmsCoreVendorGroupId();
//noinspection SwitchStatementWithTooFewBranches
return switch (vendorGroupId) {
case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest";
default -> vendorGroupId + ".android.gms";
};
}
private static String getGmsCoreVendorGroupId() {
return "app.revanced"; // Modified during patching.
}
}

View file

@ -16,8 +16,8 @@ import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.preference.LogBufferManager;
/**
* ReVanced specific logger. Logging is done to standard device log (accessible through ADB),
* and additionally accessible through {@link LogBufferManager}.
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
* and additionally accessible thru {@link LogBufferManager}.
*
* All methods are thread safe, and are safe to call even
* 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.
* <p>
* If the calling code is showing its own error toast,
* If the calling code is showing it's own error toast,
* instead use {@link #printInfo(LogMessage, Exception)}
*
* @param message log message

View file

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

View file

@ -70,7 +70,7 @@ public class StringRef {
}
/**
* Creates a StringRef object that'll not change its value
* Creates a StringRef object that'll not change it's value
*
* @param value value which toString() method returns when invoked on returned object
* @return Unique StringRef instance, its value will never change
@ -102,7 +102,7 @@ public class StringRef {
public String toString() {
if (!resolved) {
if (resources == null || packageName == null) {
var context = Utils.getContext();
Context context = Utils.getContext();
resources = context.getResources();
packageName = context.getPackageName();
}

View file

@ -106,18 +106,14 @@ public abstract class TrieSearch<T> {
* Elements not contained can collide with elements the array does contain,
* so must compare the nodes character value.
*
/*
* Alternatively, this could be implemented as a sorted, densely packed array
* with lookups performed via binary search.
* This approach would save a small amount of memory by eliminating null
* child entries. However, it would result in a worst-case lookup time of
* O(n log m), where:
* - n is the number of characters in the input text, and
* - 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.
* Alternatively this array could be a sorted and densely packed array,
* and lookup is done using binary search.
* That would save a small amount of memory because there's no null children entries,
* but would give a worst case search of O(nlog(m)) where n is the number of
* characters in the searched text and m is the maximum size of the sorted character arrays.
* Using a hash table array always gives O(n) search time.
* The memory usage here is very small (all Litho filters use ~10KB of memory),
* so the more performant hash implementation is chosen.
*/
@Nullable
private TrieNode<T>[] children;

View file

@ -43,10 +43,8 @@ import java.text.Collator;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
@ -61,6 +59,7 @@ import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
import app.revanced.extension.shared.ui.Dim;
@SuppressWarnings("NewApi")
public class Utils {
@SuppressLint("StaticFieldLeak")
@ -77,8 +76,6 @@ public class Utils {
@Nullable
private static Boolean isDarkModeEnabled;
private static boolean appIsUsingBoldIcons;
// Cached Collator instance with its locale.
@Nullable
private static Locale cachedCollatorLocale;
@ -134,7 +131,6 @@ public class Utils {
return versionName;
}
@SuppressWarnings("unused")
public static String getApplicationName() {
if (applicationLabel == null) {
try {
@ -152,12 +148,12 @@ public class Utils {
/**
* Hide a view by setting its layout height and width to 1dp.
*
* @param setting The setting to check for hiding the view.
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewBy0dpUnderCondition(BooleanSetting setting, View view) {
if (hideViewBy0dpUnderCondition(setting.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + setting);
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
@ -169,7 +165,7 @@ public class Utils {
*/
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
if (condition) {
hideViewBy0dp(view);
hideViewByLayoutParams(view);
return true;
}
@ -177,33 +173,19 @@ public class Utils {
}
/**
* Hide a view by setting its layout params to 0x0
* @param view The view to hide.
*/
public static void hideViewBy0dp(View view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
if (params == null)
params = new ViewGroup.LayoutParams(0, 0);
params.width = 0;
params.height = 0;
view.setLayoutParams(params);
}
/**
* Hide a view by setting its visibility as GONE.
* Hide a view by setting its visibility to GONE.
*
* @param setting The setting to check for hiding the view.
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewUnderCondition(BooleanSetting setting, View view) {
if (hideViewUnderCondition(setting.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + setting);
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
if (hideViewUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
/**
* Hide a view by setting its visibility as GONE.
* Hide a view by setting its visibility to GONE.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
@ -217,14 +199,14 @@ public class Utils {
return false;
}
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting setting, View view) {
if (hideViewByRemovingFromParentUnderCondition(setting.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + setting);
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) {
if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
public static boolean hideViewByRemovingFromParentUnderCondition(boolean condition, View view) {
if (condition) {
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
if (setting) {
ViewParent parent = view.getParent();
if (parent instanceof ViewGroup parentGroup) {
parentGroup.removeView(view);
@ -273,7 +255,7 @@ public class Utils {
// Could do a thread sleep, but that will trigger an exception if the thread is interrupted.
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
}
// Return the value, otherwise the compiler or VM might optimize and remove the meaningless time-wasting work,
// Return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
// leaving an empty loop that hammers on the System.currentTimeMillis native call.
return meaninglessValue;
}
@ -283,12 +265,10 @@ public class Utils {
}
public static int indexOfFirstFound(String value, String... targets) {
if (isNotEmpty(value)) {
for (String string : targets) {
if (!string.isEmpty()) {
final int indexOf = value.indexOf(string);
if (indexOf >= 0) return indexOf;
}
for (String string : targets) {
if (!string.isEmpty()) {
final int indexOf = value.indexOf(string);
if (indexOf >= 0) return indexOf;
}
}
return -1;
@ -298,13 +278,12 @@ public class Utils {
* @return zero, if the resource is not found.
*/
@SuppressLint("DiscouragedApi")
public static int getResourceIdentifier(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
return context.getResources().getIdentifier(resourceIdentifierName,
type == null ? null : type.value, context.getPackageName());
public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String type) {
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
}
public static int getResourceIdentifierOrThrow(Context context, @Nullable ResourceType type, String resourceIdentifierName) {
final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName);
public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) {
final int resourceId = getResourceIdentifier(context, resourceIdentifierName, type);
if (resourceId == 0) {
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
+ " type: " + type);
@ -314,18 +293,22 @@ public class Utils {
/**
* @return zero, if the resource is not found.
* @see #getResourceIdentifierOrThrow(ResourceType, String)
* @see #getResourceIdentifierOrThrow(String, String)
*/
public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) {
return getResourceIdentifier(getContext(), type, resourceIdentifierName);
public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String type) {
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
}
/**
* @return zero, if the resource is not found.
* @see #getResourceIdentifier(ResourceType, String)
* @return The resource identifier, or throws an exception if not found.
*/
public static int getResourceIdentifierOrThrow(@Nullable ResourceType type, String resourceIdentifierName) {
return getResourceIdentifierOrThrow(getContext(), type, resourceIdentifierName);
public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String type) {
final int resourceId = getResourceIdentifier(getContext(), resourceIdentifierName, type);
if (resourceId == 0) {
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
+ " type: " + type);
}
return resourceId;
}
public static String getResourceString(int id) throws Resources.NotFoundException {
@ -333,29 +316,29 @@ public class Utils {
}
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName));
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
}
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(ResourceType.ANIM, resourceIdentifierName));
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(resourceIdentifierName, "anim"));
}
@ColorInt
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
//noinspection deprecation
return getContext().getResources().getColor(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName));
return getContext().getResources().getColor(getResourceIdentifierOrThrow(resourceIdentifierName, "color"));
}
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
}
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName));
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
}
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName));
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(resourceIdentifierName, "array"));
}
public interface MatchFilter<T> {
@ -366,7 +349,7 @@ public class Utils {
* Includes sub children.
*/
public static <R extends View> R getChildViewByResourceName(View view, String str) {
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str));
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(str, "id"));
//noinspection unchecked
return (R) child;
}
@ -460,11 +443,6 @@ public class Utils {
clipboard.setPrimaryClip(clip);
}
public static boolean isNotEmpty(@Nullable String str) {
return str != null && !str.isEmpty();
}
@SuppressWarnings("unused")
public static boolean isTablet() {
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
}
@ -473,7 +451,7 @@ public class Utils {
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
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
* This is the default locale of the device, which may differ if
@ -487,7 +465,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) {
String displayLanguage = locale.getDisplayLanguage();
@ -504,7 +482,6 @@ public class Utils {
return getTextDirectionString(isRightToLeftLocale());
}
@SuppressWarnings("unused")
public static String getTextDirectionString(Locale locale) {
return getTextDirectionString(isRightToLeftLocale(locale));
}
@ -517,7 +494,7 @@ public class Utils {
/**
* @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")
public static boolean containsNumber(CharSequence text) {
@ -829,21 +806,6 @@ public class Utils {
window.setBackgroundDrawable(null); // Remove default dialog background
}
/**
* @return If the unpatched app is currently using bold icons.
*/
public static boolean appIsUsingBoldIcons() {
return appIsUsingBoldIcons;
}
/**
* Controls if ReVanced bold icons are shown in various places.
* @param boldIcons If the app is currently using bold icons.
*/
public static void setAppIsUsingBoldIcons(boolean boldIcons) {
appIsUsingBoldIcons = boldIcons;
}
/**
* Sets the theme light color used by the app.
*/
@ -1149,7 +1111,7 @@ public class Utils {
}
/**
* Uses {@link #adjustColorBrightness(int, float)} depending on if light or dark mode is active.
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
*/
@ColorInt
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
@ -1205,18 +1167,4 @@ public class Utils {
public static float clamp(float value, float lower, float upper) {
return Math.max(lower, Math.min(value, upper));
}
/**
* @param maxSize The maximum number of elements to keep in the map.
* @return A {@link LinkedHashMap} that automatically evicts the oldest entry
* when the size exceeds {@code maxSize}.
*/
public static <T, V> Map<T, V> createSizeRestrictedMap(int maxSize) {
return new LinkedHashMap<>(2 * maxSize) {
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > maxSize;
}
};
}
}

View file

@ -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.Utils.DialogFragmentOnStartAction;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Build;
import android.text.Html;
import android.util.Pair;
import android.view.Gravity;
@ -19,17 +19,14 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.Collection;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.ui.CustomDialog;
@RequiresApi(api = Build.VERSION_CODES.N)
abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
@ -78,6 +75,7 @@ abstract class Check {
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
}
@SuppressLint("NewApi")
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
final var reasons = new StringBuilder();
@ -130,7 +128,7 @@ abstract class Check {
// Add icon to the dialog.
ImageView iconView = new ImageView(activity);
iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
ResourceType.DRAWABLE, "revanced_ic_dialog_alert"));
"revanced_ic_dialog_alert", "drawable"));
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
iconView.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(

View file

@ -7,12 +7,8 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@ -31,7 +27,6 @@ import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
* <br>
* Various indicators help to detect if the app was patched by the user.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressWarnings("unused")
public final class CheckEnvironmentPatch {
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
@ -44,7 +39,6 @@ public final class CheckEnvironmentPatch {
ADB((String) null),
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
MANAGER("app.revanced.manager.flutter",
"app.revanced.manager.flutter.debug",
"app.revanced.manager",
"app.revanced.manager.debug");
@ -124,7 +118,7 @@ public final class CheckEnvironmentPatch {
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
*/
private static class CheckWasPatchedOnSameDevice extends Check {
@SuppressLint("HardwareIds")
@SuppressLint({"NewApi", "HardwareIds"})
@Override
protected Boolean check() {
if (PATCH_BOARD.isEmpty()) {
@ -198,7 +192,7 @@ public final class CheckEnvironmentPatch {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
// Duration since initial install or last update, whichever is sooner.
// Duration since initial install or last update, which ever is sooner.
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
Logger.printInfo(() -> "App was installed/updated: "
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
@ -294,8 +288,8 @@ public final class CheckEnvironmentPatch {
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
Boolean timeCheckPassed = nearPatchTime.check();
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Allow installing recently patched APKs,
// even if the installation source is not Manager or ADB.
// Allow installing recently patched apks,
// even if the install source is not Manager or ADB.
Check.disableForever();
return;
} else {

View file

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

View file

@ -7,15 +7,12 @@ import android.content.pm.PackageManager;
import android.graphics.Color;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import app.revanced.extension.shared.GmsCoreSupport;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
@ -55,35 +52,24 @@ public class CustomBrandingPatch {
}
}
@Nullable
private static Integer notificationSmallIcon;
private static final int notificationSmallIcon;
private static int getNotificationSmallIcon() {
// Cannot use static initialization block otherwise cyclic references exist
// between Settings initialization and this class.
if (notificationSmallIcon == null) {
if (GmsCoreSupport.isPackageNameOriginal()) {
Logger.printDebug(() -> "App is root mounted. Not overriding small notification icon");
return notificationSmallIcon = 0;
static {
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
if (branding == BrandingTheme.ORIGINAL) {
notificationSmallIcon = 0;
} else {
// Original icon is quantum_ic_video_youtube_white_24
String iconName = "revanced_notification_icon";
if (branding == BrandingTheme.CUSTOM) {
iconName += "_custom";
}
BrandingTheme branding = BaseSettings.CUSTOM_BRANDING_ICON.get();
if (branding == BrandingTheme.ORIGINAL) {
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");
}
notificationSmallIcon = Utils.getResourceIdentifier(iconName, "drawable");
if (notificationSmallIcon == 0) {
Logger.printException(() -> "Could not load notification small icon");
}
}
return notificationSmallIcon;
}
/**
@ -102,9 +88,8 @@ public class CustomBrandingPatch {
*/
public static void setNotificationIcon(Notification.Builder builder) {
try {
final int smallIcon = getNotificationSmallIcon();
if (smallIcon != 0) {
builder.setSmallIcon(smallIcon)
if (notificationSmallIcon != 0) {
builder.setSmallIcon(notificationSmallIcon)
.setColor(Color.TRANSPARENT); // Remove YT red tint.
}
} catch (Exception ex) {
@ -114,45 +99,12 @@ public class CustomBrandingPatch {
/**
* Injection point.
* <p>
*
* The total number of app name aliases, including dummy aliases.
*/
private static int numberOfPresetAppNames() {
// Modified during patching, but requires a default if custom branding is excluded.
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()
: 2;
}
public static BrandingTheme getDefaultIconStyle() {
return userProvidedCustomIcon()
? BrandingTheme.CUSTOM
: BrandingTheme.ROUNDED;
// Modified during patching.
throw new IllegalStateException();
}
/**

View file

@ -41,13 +41,12 @@ public final class EnableDebuggingPatch {
/**
* Injection point.
*/
public static boolean isBooleanFeatureFlagEnabled(boolean value, long flag) {
public static boolean isBooleanFeatureFlagEnabled(boolean value, Long flag) {
if (LOG_FEATURE_FLAGS && value) {
Long flagObj = flag;
if (DISABLED_FEATURE_FLAGS.contains(flagObj)) {
if (DISABLED_FEATURE_FLAGS.contains(flag)) {
return false;
}
if (featureFlags.putIfAbsent(flagObj, TRUE) == null) {
if (featureFlags.putIfAbsent(flag, TRUE) == null) {
Logger.printDebug(() -> "boolean feature is enabled: " + flag);
}
}
@ -60,8 +59,6 @@ public final class EnableDebuggingPatch {
*/
public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) {
if (LOG_FEATURE_FLAGS && defaultValue != value) {
if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
if (featureFlags.putIfAbsent(flag, true) == null) {
// Align the log outputs to make post processing easier.
Logger.printDebug(() -> " double feature is enabled: " + flag
@ -77,8 +74,6 @@ public final class EnableDebuggingPatch {
*/
public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) {
if (LOG_FEATURE_FLAGS && defaultValue != value) {
if (DISABLED_FEATURE_FLAGS.contains(flag)) return defaultValue;
if (featureFlags.putIfAbsent(flag, true) == null) {
Logger.printDebug(() -> " long feature is enabled: " + flag
+ " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue));

View file

@ -18,8 +18,8 @@ public final class SanitizeSharingLinksPatch {
* Injection point.
*/
public static String sanitize(String url) {
if (BaseSettings.SANITIZE_SHARING_LINKS.get()) {
url = sanitizer.sanitizeURLString(url);
if (BaseSettings.SANITIZE_SHARED_LINKS.get()) {
url = sanitizer.sanitizeUrlString(url);
}
if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) {

View file

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

View file

@ -1,439 +0,0 @@
package app.revanced.extension.shared.patches.litho;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.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")
public final class LithoFilterPatch {
/**
* Simple wrapper to pass the litho parameters through the prefix search.
*/
private static final class LithoFilterParameters {
final String identifier;
final String path;
final String accessibility;
final byte[] buffer;
LithoFilterParameters(String lithoIdentifier, String lithoPath,
String accessibility, byte[] buffer) {
this.identifier = lithoIdentifier;
this.path = lithoPath;
this.accessibility = accessibility;
this.buffer = buffer;
}
@NonNull
@Override
public String toString() {
// Estimate the percentage of the buffer that are Strings.
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
builder.append("ID: ");
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);
if (YouTubeAndMusicSettings.DEBUG_PROTOCOLBUFFER.get()) {
builder.append(" BufferStrings: ");
findAsciiStrings(builder, buffer);
}
return builder.toString();
}
/**
* Search through a byte array for all ASCII strings.
*/
static void findAsciiStrings(StringBuilder builder, byte[] buffer) {
// Valid ASCII values (ignore control characters).
final int minimumAscii = 32; // 32 = space character
final int maximumAscii = 126; // 127 = delete character
final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include.
String delimitingCharacter = ""; // Non ascii character, to allow easier log filtering.
final int length = buffer.length;
int start = 0;
int end = 0;
while (end < length) {
int value = buffer[end];
if (value < minimumAscii || value > maximumAscii || end == length - 1) {
if (end - start >= minimumAsciiStringLength) {
for (int i = start; i < end; i++) {
builder.append((char) buffer[i]);
}
builder.append(delimitingCharacter);
}
start = end + 1;
}
end++;
}
}
}
/**
* Placeholder for actual filters.
*/
private static final class DummyFilter extends Filter {
}
private static final Filter[] filters = new Filter[]{
new DummyFilter() // Replaced during patching, do not touch.
};
/**
* Litho layout fixed thread pool size override.
* <p>
* Unpatched YouTube uses a layout fixed thread pool between 1 and 3 threads:
* <pre>
* 1 thread - > Device has less than 6 cores
* 2 threads -> Device has over 6 cores and less than 6GB of memory
* 3 threads -> Device has over 6 cores and more than 6GB of memory
* </pre>
*
* Using more than 1 thread causes layout issues such as the You tab watch/playlist shelf
* that is sometimes incorrectly hidden (ReVanced is not hiding it), and seems to
* fix a race issue if using the active navigation tab status with litho filtering.
*/
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
/**
* 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 boolean EXTRACT_IDENTIFIER_FROM_BUFFER = false;
/**
* 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 identifierSearchTree = new StringTrieSearch();
static {
for (Filter filter : filters) {
filterUsingCallbacks(identifierSearchTree, filter,
filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER);
filterUsingCallbacks(pathSearchTree, filter,
filter.pathCallbacks, Filter.FilterContentType.PATH);
}
Logger.printDebug(() -> "Using: "
+ identifierSearchTree.numberOfPatterns() + " identifier filters"
+ " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), "
+ pathSearchTree.numberOfPatterns() + " path filters"
+ " (" + pathSearchTree.getEstimatedMemorySize() + " KB)");
}
private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
Filter filter, List<StringFilterGroup> groups,
Filter.FilterContentType type) {
String filterSimpleName = filter.getClass().getSimpleName();
for (StringFilterGroup group : groups) {
if (!group.includeInSearch()) {
continue;
}
for (String pattern : group.filters) {
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
matchedLength, callbackParameter) -> {
if (!group.isEnabled()) return false;
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
final boolean isFiltered = filter.isFiltered(parameters.identifier,
parameters.accessibility, parameters.path, parameters.buffer,
group, type, matchedStartIndex);
if (isFiltered && BaseSettings.DEBUG.get()) {
Logger.printDebug(() -> type == Filter.FilterContentType.IDENTIFIER
? filterSimpleName + " filtered identifier: " + parameters.identifier
: filterSimpleName + " filtered path: " + parameters.path);
}
return isFiltered;
}
);
}
}
}
private static Map<String, byte[]> createIdentifierToBufferMap() {
// It's unclear how many items should be cached. This is a guess.
return Utils.createSizeRestrictedMap(100);
}
/**
* Helper function that differs from {@link Character#isDigit(char)}
* as this only matches ascii and not Unicode numbers.
*/
private static boolean isAsciiNumber(byte character) {
return '0' <= character && character <= '9';
}
private static boolean isAsciiLowerCaseLetter(byte character) {
return 'a' <= character && character <= 'z';
}
/**
* Injection point. Called off the main thread.
* Targets 20.22+
*/
public static void setProtoBuffer(byte[] buffer) {
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
StringBuilder builder = new StringBuilder();
LithoFilterParameters.findAsciiStrings(builder, buffer);
Logger.printDebug(() -> "New buffer: " + builder);
}
// 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);
}
/**
* Injection point. Called off the main thread.
* Targets 20.21 and lower.
*/
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
if (buffer == null || !buffer.hasArray()) {
// It appears the buffer can be cleared out just before the call to #filter()
// Ignore this null value and retain the last buffer that was set.
Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer);
} else {
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
// This is intentional, as it appears the buffer can be set once and then filtered multiple times.
// The buffer will be cleared from memory after a new buffer is set by the same thread,
// or when the calling thread eventually dies.
bufferThreadLocal.set(buffer.array());
}
}
/**
* Injection point.
*/
public static boolean isFiltered(String identifier, @Nullable String accessibilityId,
@Nullable String accessibilityText, StringBuilder pathBuilder) {
try {
if (identifier.isEmpty() || pathBuilder.length() == 0) {
return false;
}
byte[] buffer = null;
if (EXTRACT_IDENTIFIER_FROM_BUFFER) {
final int pipeIndex = identifier.indexOf('|');
if (pipeIndex >= 0) {
// If the identifier contains no pipe, then it's not an ".eml" identifier
// and the buffer is not uniquely identified. Typically, this only happens
// for subcomponents where buffer filtering is not used.
String identifierKey = identifier.substring(0, pipeIndex);
var map = identifierToBufferThread.get();
if (map != null) {
buffer = map.get(identifierKey);
}
if (buffer == null) {
// Buffer for thread local not found. Use the last buffer found from any thread.
buffer = identifierToBufferGlobal.get(identifierKey);
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER && buffer == null) {
// No buffer is found for some components, such as
// shorts_lockup_cell.eml on channel profiles.
// For now, just ignore this and filter without a buffer.
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.
// Use an empty buffer so the litho id/path filters that do not use a buffer still work.
if (buffer == null) {
buffer = EMPTY_BYTE_ARRAY;
}
String path = pathBuilder.toString();
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);
return identifierSearchTree.matches(identifier, parameter)
|| pathSearchTree.matches(path, parameter);
} catch (Exception ex) {
Logger.printException(() -> "isFiltered failure", ex);
}
return false;
}
/**
* Injection point.
*/
public static int getExecutorCorePoolSize(int originalCorePoolSize) {
if (originalCorePoolSize != LITHO_LAYOUT_THREAD_POOL_SIZE) {
Logger.printDebug(() -> "Overriding core thread pool size from: " + originalCorePoolSize
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
}
return LITHO_LAYOUT_THREAD_POOL_SIZE;
}
/**
* Injection point.
*/
public static int getExecutorMaxThreads(int originalMaxThreads) {
if (originalMaxThreads != LITHO_LAYOUT_THREAD_POOL_SIZE) {
Logger.printDebug(() -> "Overriding max thread pool size from: " + originalMaxThreads
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
}
return LITHO_LAYOUT_THREAD_POOL_SIZE;
}
}

View file

@ -24,23 +24,23 @@ public class LinkSanitizer {
: List.of(parametersToRemove);
}
public String sanitizeURLString(String url) {
public String sanitizeUrlString(String url) {
try {
return sanitizeURI(Uri.parse(url)).toString();
return sanitizeUri(Uri.parse(url)).toString();
} catch (Exception ex) {
Logger.printException(() -> "sanitizeURLString failure: " + url, ex);
Logger.printException(() -> "sanitizeUrlString failure: " + url, ex);
return url;
}
}
public Uri sanitizeURI(Uri uri) {
public Uri sanitizeUri(Uri uri) {
try {
String scheme = uri.getScheme();
if (scheme == null || !(scheme.equals("http") || scheme.equals("https"))) {
// Opening YouTube share sheet 'other' option passes the video title as a URI.
// Checking !uri.isHierarchical() works for all cases, except if the
// 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;
}
@ -56,12 +56,12 @@ public class LinkSanitizer {
}
}
Uri sanitizedURL = builder.build();
Logger.printInfo(() -> "Sanitized URL: " + uri + " to: " + sanitizedURL);
Uri sanitizedUrl = builder.build();
Logger.printInfo(() -> "Sanitized url: " + uri + " to: " + sanitizedUrl);
return sanitizedURL;
return sanitizedUrl;
} catch (Exception ex) {
Logger.printException(() -> "sanitizeURI failure: " + uri, ex);
Logger.printException(() -> "sanitizeUri failure: " + uri, ex);
return uri;
}
}

View file

@ -23,8 +23,8 @@ public class Requester {
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
String url = apiUrl + route.getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
// This request sends data via URL query parameters. No request body is included.
// If a request body is added, the caller must set the appropriate Content-Length header.
// Request data is in the URL parameters and no body is sent.
// The calling code must set a length if using a request body.
connection.setFixedLengthStreamingMode(0);
connection.setRequestMethod(route.getMethod().name());
String agentString = System.getProperty("http.agent")

View file

@ -6,17 +6,13 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceFragment;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.RequiresApi;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
import app.revanced.extension.shared.ui.Dim;
@ -25,18 +21,17 @@ import app.revanced.extension.shared.ui.Dim;
* 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.
*/
@SuppressWarnings("deprecation")
@RequiresApi(api = Build.VERSION_CODES.O)
@SuppressWarnings({"deprecation", "NewApi"})
public abstract class BaseActivityHook extends Activity {
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_settings_fragments");
getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
private static final int ID_REVANCED_TOOLBAR_PARENT =
getResourceIdentifierOrThrow(ResourceType.ID, "revanced_toolbar_parent");
getResourceIdentifierOrThrow("revanced_toolbar_parent", "id");
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_settings_with_toolbar");
getResourceIdentifierOrThrow("revanced_settings_with_toolbar", "layout");
private static final int STRING_REVANCED_SETTINGS_TITLE =
getResourceIdentifierOrThrow(ResourceType.STRING, "revanced_settings_title");
getResourceIdentifierOrThrow("revanced_settings_title", "string");
/**
* Layout parameters for the toolbar, extracted from the dummy toolbar.
@ -100,15 +95,15 @@ public abstract class BaseActivityHook extends Activity {
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
// Replace dummy placeholder toolbar.
// This is required to fix submenu title alignment issue with Android ASOP 15+
ViewGroup toolbarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolbarParent, "revanced_toolbar");
ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
toolbarLayoutParams = dummyToolbar.getLayoutParams();
toolbarParent.removeView(dummyToolbar);
toolBarParent.removeView(dummyToolbar);
// Sets appropriate system navigation bar color for the activity.
ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow());
Toolbar toolbar = new Toolbar(toolbarParent.getContext());
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
toolbar.setBackgroundColor(getToolbarBackgroundColor());
toolbar.setNavigationIcon(getNavigationIcon());
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
@ -125,14 +120,7 @@ public abstract class BaseActivityHook extends Activity {
onPostToolbarSetup(activity, toolbar, fragment);
toolbarParent.addView(toolbar, 0);
}
/**
* Returns the resource ID for the content view layout.
*/
protected int getContentViewResourceId() {
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
toolBarParent.addView(toolbar, 0);
}
/**
@ -140,6 +128,11 @@ public abstract class BaseActivityHook extends 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.
*/

View file

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

View file

@ -43,7 +43,7 @@ public class BooleanSetting extends Setting<Boolean> {
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Boolean)} was intended.
* accidental usage when {@link #save(Boolean)} was intnded.
*/
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
setting.value = Objects.requireNonNull(newValue);

View file

@ -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.
* @throws IllegalArgumentException if the name is not a valid enum of this type.
*/

View file

@ -274,6 +274,60 @@ public abstract class Setting<T> {
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.
* This method is only to be used by the Settings preference code.
@ -365,7 +419,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() {
return value.equals(defaultValue);
@ -396,7 +450,7 @@ public abstract class Setting<T> {
/**
* @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;

View file

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

View file

@ -23,7 +23,6 @@ import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
@ -104,16 +103,10 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* so all app specific {@link Setting} instances are loaded before this method returns.
*/
protected void initialize() {
String preferenceResourceName;
if (BaseSettings.SHOW_MENU_ICONS.get()) {
preferenceResourceName = Utils.appIsUsingBoldIcons()
? "revanced_prefs_icons_bold"
: "revanced_prefs_icons";
} else {
preferenceResourceName = "revanced_prefs";
}
final var identifier = Utils.getResourceIdentifier(ResourceType.XML, preferenceResourceName);
String preferenceResourceName = BaseSettings.SHOW_MENU_ICONS.get()
? "revanced_prefs_icons"
: "revanced_prefs";
final var identifier = Utils.getResourceIdentifier(preferenceResourceName, "xml");
if (identifier == 0) return;
addPreferencesFromResource(identifier);
@ -208,7 +201,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
private void updatePreferenceScreen(@NonNull PreferenceGroup group,
boolean syncSettingValue,
boolean applySettingToPreference) {
// Alternatively this could iterate through all Settings and check for any matching Preferences,
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
// but there are many more Settings than UI preferences so it's more efficient to only check
// the Preferences.
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {

View file

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

View file

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

View file

@ -38,7 +38,6 @@ import java.util.Set;
import java.util.TreeSet;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.patches.EnableDebuggingPatch;
import app.revanced.extension.shared.settings.BaseSettings;
@ -53,26 +52,25 @@ import app.revanced.extension.shared.ui.Dim;
public class FeatureFlagsManagerPreference extends Preference {
private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_select_all");
getResourceIdentifierOrThrow("revanced_settings_select_all", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_deselect_all");
getResourceIdentifierOrThrow("revanced_settings_deselect_all", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_copy_all");
getResourceIdentifierOrThrow("revanced_settings_copy_all", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_one");
getResourceIdentifierOrThrow("revanced_settings_arrow_right_one", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_double");
getResourceIdentifierOrThrow("revanced_settings_arrow_right_double", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_one");
getResourceIdentifierOrThrow("revanced_settings_arrow_left_one", "drawable");
private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE =
getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_double");
getResourceIdentifierOrThrow("revanced_settings_arrow_left_double", "drawable");
/**
* Flags to hide from the UI.
*/
private static final Set<Long> FLAGS_TO_IGNORE = Set.of(
45386834L, // 'You' tab settings icon.
45532100L // Cairo flag. Turning this off with all other flags causes the settings menu to be a mix of old/new.
45386834L // 'You' tab settings icon.
);
/**
@ -131,10 +129,9 @@ public class FeatureFlagsManagerPreference extends Preference {
disabledFlags.removeAll(FLAGS_TO_IGNORE);
if (allKnownFlags.isEmpty() && disabledFlags.isEmpty()) {
// It's impossible to reach the settings menu without reaching at least one flag.
// So if theres no flags, then that means the user has just enabled debugging
// but has not restarted the app yet.
Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_no_flags"));
// String does not need to be localized because it's basically impossible
// to reach the settings menu without encountering at least 1 flag.
Utils.showToastShort("No feature flags logged yet");
return;
}

View file

@ -36,18 +36,18 @@ public class SharedPrefCategory {
}
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
preferences.edit().putString(key, (value == null ? null : value.toString())).commit();
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
}
/**
* Removes any preference data type that has the specified key.
*/
public void removeKey(@NonNull String key) {
preferences.edit().remove(Objects.requireNonNull(key)).commit();
preferences.edit().remove(Objects.requireNonNull(key)).apply();
}
public void saveBoolean(@NonNull String key, boolean value) {
preferences.edit().putBoolean(key, value).commit();
preferences.edit().putBoolean(key, value).apply();
}
/**

View file

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

View file

@ -9,36 +9,36 @@ import android.util.AttributeSet;
import app.revanced.extension.shared.Logger;
/**
* Simple preference that opens a URL when clicked.
* Simple preference that opens a url when clicked.
*/
@SuppressWarnings("deprecation")
public class URLLinkPreference extends Preference {
public class UrlLinkPreference extends Preference {
protected String externalURL;
protected String externalUrl;
{
setOnPreferenceClickListener(pref -> {
if (externalURL == null) {
if (externalUrl == null) {
Logger.printException(() -> "URL not set " + getClass().getSimpleName());
return false;
}
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(externalURL));
i.setData(Uri.parse(externalUrl));
pref.getContext().startActivity(i);
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);
}
public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public URLLinkPreference(Context context, AttributeSet attrs) {
public UrlLinkPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public URLLinkPreference(Context context) {
public UrlLinkPreference(Context context) {
super(context);
}
}

View file

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

View file

@ -1,6 +1,7 @@
package app.revanced.extension.shared.settings.search;
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
import static app.revanced.extension.shared.settings.search.BaseSearchViewController.DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
@ -32,11 +33,10 @@ import java.lang.reflect.Method;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
import app.revanced.extension.shared.settings.preference.URLLinkPreference;
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
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 ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_title");
"preference_title", "id");
protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_summary");
"preference_summary", "id");
protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_path");
"preference_path", "id");
protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_switch");
"preference_switch", "id");
protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
ResourceType.ID, "preference_color_dot");
"preference_color_dot", "id");
protected static class RegularViewHolder {
TextView titleView;
@ -275,7 +275,7 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
holder.titleView.setText(item.highlightedTitle);
holder.summaryView.setText(item.highlightedSummary);
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
holder.iconView.setImageResource(BaseSearchViewController.getSearchIcon());
holder.iconView.setImageResource(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
}
/**
@ -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) {
if (TextUtils.isEmpty(input)) return "";
@ -609,8 +609,8 @@ public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchRe
boolean hasNavigationCapability(Preference preference) {
// PreferenceScreen always allows navigation.
if (preference instanceof PreferenceScreen) return true;
// URLLinkPreference does not navigate to a new screen, it opens an external URL.
if (preference instanceof URLLinkPreference) return false;
// UrlLinkPreference does not navigate to a new screen, it opens an external URL.
if (preference instanceof UrlLinkPreference) return false;
// Other group types that might have their own screens.
if (preference instanceof PreferenceGroup) {
// Check if it has its own fragment or intent.

View file

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

View file

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

View file

@ -9,34 +9,9 @@ import java.util.Locale;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@SuppressWarnings("ConstantLocale")
public enum ClientType {
/**
* Video not playable: Paid, Movie, Private, Age-restricted.
* Uses non-adaptive bitrate.
* AV1 codec available.
*/
ANDROID_REEL(
3,
"ANDROID",
"com.google.android.youtube",
Build.MANUFACTURER,
Build.MODEL,
"Android",
Build.VERSION.RELEASE,
String.valueOf(Build.VERSION.SDK_INT),
Build.ID,
"20.44.38",
// This client has been used by most open-source YouTube stream extraction tools since 2024, including NewPipe Extractor, SmartTube, and Grayjay.
// This client can log in, but if an access token is used in the request, GVS can more easily identify the request as coming from ReVanced.
// This means that the GVS server can strengthen its validation of the ANDROID_REEL client.
true,
true,
false,
"Android Reel"
),
/**
* Video not playable: Kids / Paid / Movie / Private / Age-restricted.
* This client can only be used when logged out.
@ -53,10 +28,10 @@ public enum ClientType {
// Android 12.1
"32",
"SQ3A.220605.009.A1",
"132.0.6808.3",
"1.61.48",
false,
false,
true,
"Android VR 1.61"
),
/**
@ -73,12 +48,39 @@ public enum ClientType {
ANDROID_VR_1_61_48.osVersion,
Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion),
Objects.requireNonNull(ANDROID_VR_1_61_48.buildId),
"107.0.5284.2",
"1.43.32",
ANDROID_VR_1_61_48.useAuth,
ANDROID_VR_1_61_48.supportsMultiAudioTracks,
ANDROID_VR_1_61_48.usePlayerEndpoint,
"Android VR 1.43"
),
/**
* Video not playable: Paid / Movie / Private / Age-restricted.
* Note: The 'Authorization' key must be excluded from the header.
*
* According to TeamNewPipe in 2022, if the 'androidSdkVersion' field is missing,
* the GVS did not return a valid response:
* [NewPipe#8713 (comment)](https://github.com/TeamNewPipe/NewPipe/issues/8713#issuecomment-1207443550).
*
* According to the latest commit in yt-dlp, the GVS returns a valid response
* even if the 'androidSdkVersion' field is missing:
* [yt-dlp#14693](https://github.com/yt-dlp/yt-dlp/pull/14693).
*
* For some reason, PoToken is not required.
*/
ANDROID_NO_SDK(
3,
"ANDROID",
"",
"",
"",
Build.VERSION.RELEASE,
"20.05.46",
"com.google.android.youtube/20.05.46 (Linux; U; Android " + Build.VERSION.RELEASE + ") gzip",
false,
true,
"Android No SDK"
),
/**
* Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children".
* <a href="https://dumps.tadiphone.dev/dumps/google/barbet">Google Pixel 9 Pro Fold</a>
@ -93,10 +95,10 @@ public enum ClientType {
"15",
"35",
"AP3A.241005.015.A2",
"132.0.6779.0",
"23.47.101",
true,
false,
true,
"Android Studio"
),
/**
@ -112,8 +114,32 @@ public enum ClientType {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
false,
false,
true,
"visionOS"
),
/**
* The device machine id for the iPad 6th Gen (iPad7,6).
* AV1 hardware decoding is not supported.
* See [this GitHub Gist](https://gist.github.com/adamawolf/3048717) for more information.
*
* Based on Google's actions to date, PoToken may not be required on devices with very low specs.
* For example, suppose the User-Agent for a PlayStation 3 (with 256MB of RAM) is used.
* Accessing 'Web' (https://www.youtube.com) will redirect to 'TV' (https://www.youtube.com/tv).
* 'TV' target devices with very low specs, such as embedded devices, game consoles, and blu-ray players, so PoToken is not required.
*
* For this reason, the device machine id for the iPad 6th Gen (with 2GB of RAM),
* the lowest spec device capable of running iPadOS 17, was used.
*/
IPADOS(5,
"IOS",
"Apple",
"iPad7,6",
"iPadOS",
"17.7.10.21H450",
"19.22.3",
"com.google.ios.youtube/19.22.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; " + Locale.getDefault() + ")",
false,
true,
"iPadOS"
);
/**
@ -169,6 +195,13 @@ public enum ClientType {
@Nullable
private final String buildId;
/**
* Cronet release version, as found in decompiled client apk.
* Field is null if not applicable.
*/
@Nullable
private final String cronetVersion;
/**
* App version.
*/
@ -184,11 +217,6 @@ public enum ClientType {
*/
public final boolean supportsMultiAudioTracks;
/**
* If the client should use the player endpoint for stream extraction.
*/
public final boolean usePlayerEndpoint;
/**
* Friendly name displayed in stats for nerds.
*/
@ -206,10 +234,10 @@ public enum ClientType {
String osVersion,
@NonNull String androidSdkVersion,
@NonNull String buildId,
@NonNull String cronetVersion,
String clientVersion,
boolean useAuth,
boolean supportsMultiAudioTracks,
boolean usePlayerEndpoint,
String friendlyName) {
this.id = id;
this.clientName = clientName;
@ -220,20 +248,21 @@ public enum ClientType {
this.osVersion = osVersion;
this.androidSdkVersion = androidSdkVersion;
this.buildId = buildId;
this.cronetVersion = cronetVersion;
this.clientVersion = clientVersion;
this.useAuth = useAuth;
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
this.usePlayerEndpoint = usePlayerEndpoint;
this.friendlyName = friendlyName;
Locale defaultLocale = Locale.getDefault();
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s)",
this.userAgent = String.format("%s/%s (Linux; U; Android %s; %s; %s; Build/%s; Cronet/%s)",
packageName,
clientVersion,
osVersion,
defaultLocale,
deviceModel,
buildId
Objects.requireNonNull(buildId),
Objects.requireNonNull(cronetVersion)
);
Logger.printDebug(() -> "userAgent: " + this.userAgent);
}
@ -249,7 +278,6 @@ public enum ClientType {
String userAgent,
boolean useAuth,
boolean supportsMultiAudioTracks,
boolean usePlayerEndpoint,
String friendlyName) {
this.id = id;
this.clientName = clientName;
@ -261,10 +289,10 @@ public enum ClientType {
this.userAgent = userAgent;
this.useAuth = useAuth;
this.supportsMultiAudioTracks = supportsMultiAudioTracks;
this.usePlayerEndpoint = usePlayerEndpoint;
this.friendlyName = friendlyName;
this.packageName = null;
this.androidSdkVersion = null;
this.buildId = null;
this.cronetVersion = null;
}
}

View file

@ -5,6 +5,7 @@ import android.text.TextUtils;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -38,7 +39,7 @@ public class SpoofVideoStreamsPatch {
@Nullable
private static volatile AppLanguage languageOverride;
private static volatile ClientType preferredClient = ClientType.ANDROID_REEL;
private static volatile ClientType preferredClient = ClientType.ANDROID_VR_1_43_32;
/**
* @return If this patch was included during patching.
@ -249,7 +250,7 @@ public class SpoofVideoStreamsPatch {
* Called after {@link #fetchStreams(String, Map)}.
*/
@Nullable
public static byte[] getStreamingData(String videoId) {
public static ByteBuffer getStreamingData(String videoId) {
if (SPOOF_STREAMING_DATA) {
try {
StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId);

View file

@ -15,20 +15,13 @@ import app.revanced.extension.shared.spoof.ClientType;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
final class PlayerRoutes {
static final Route.CompiledRoute GET_PLAYER_STREAMING_DATA = new Route(
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
Route.Method.POST,
"player" +
"?fields=streamingData" +
"&alt=proto"
).compile();
static final Route.CompiledRoute GET_REEL_STREAMING_DATA = new Route(
Route.Method.POST,
"reel/reel_item_watch" +
"?fields=playerResponse.playabilityStatus,playerResponse.streamingData" +
"&alt=proto"
).compile();
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
/**
@ -54,7 +47,6 @@ final class PlayerRoutes {
Locale streamLocale = language.getLocale();
JSONObject client = new JSONObject();
client.put("deviceMake", clientType.deviceMake);
client.put("deviceModel", clientType.deviceModel);
client.put("clientName", clientType.clientName);
@ -69,19 +61,9 @@ final class PlayerRoutes {
context.put("client", client);
innerTubeBody.put("context", context);
if (clientType.usePlayerEndpoint) {
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", videoId);
} else {
JSONObject playerRequest = new JSONObject();
playerRequest.put("contentCheckOk", true);
playerRequest.put("racyCheckOk", true);
playerRequest.put("videoId", videoId);
innerTubeBody.put("playerRequest", playerRequest);
innerTubeBody.put("disablePlayerResponse", false);
}
innerTubeBody.put("contentCheckOk", true);
innerTubeBody.put("racyCheckOk", true);
innerTubeBody.put("videoId", videoId);
} catch (JSONException e) {
Logger.printException(() -> "Failed to create innerTubeBody", e);
}

View file

@ -1,20 +1,22 @@
package app.revanced.extension.shared.spoof.requests;
import static app.revanced.extension.shared.ByteTrieSearch.convertStringsToBytes;
import static app.revanced.extension.shared.Utils.isNotEmpty;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_PLAYER_STREAMING_DATA;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_REEL_STREAMING_DATA;
import static app.revanced.extension.shared.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -26,16 +28,11 @@ import java.util.concurrent.TimeoutException;
import app.revanced.extension.shared.ByteTrieSearch;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.PlayerResponse;
import app.revanced.extension.shared.innertube.PlayerResponseOuterClass.StreamingData;
import app.revanced.extension.shared.innertube.ReelItemWatchResponseOuterClass.ReelItemWatchResponse;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.settings.BaseSettings;
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.
* <p>
* Effectively the cache expiration of these fetches is the same as the stock app,
@ -45,7 +42,7 @@ import app.revanced.extension.shared.spoof.ClientType;
*/
public class StreamingDataRequest {
private static volatile ClientType[] clientOrderToUse = ClientType.values();
private static volatile ClientType[] clientOrderToUse = ClientType.values();
public static void setClientOrderToUse(List<ClientType> availableClients, ClientType preferredClient) {
Objects.requireNonNull(preferredClient);
@ -86,15 +83,22 @@ public class StreamingDataRequest {
*/
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
/**
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
Utils.createSizeRestrictedMap(50));
new LinkedHashMap<>(100) {
/**
* Cache limit must be greater than the maximum number of videos open at once,
* which theoretically is more than 4 (3 Shorts + one regular minimized video).
* But instead use a much larger value, to handle if a video viewed a while ago
* is somehow still referenced. Each stream is a small array of Strings
* so memory usage is not a concern.
*/
private static final int CACHE_LIMIT = 50;
@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});
/**
* Strings found in the response if the video is a livestream.
@ -115,7 +119,7 @@ public class StreamingDataRequest {
private final String videoId;
private final Future<byte[]> future;
private final Future<ByteBuffer> future;
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
Objects.requireNonNull(playerHeaders);
@ -138,12 +142,6 @@ public class StreamingDataRequest {
Logger.printInfo(() -> toastMessage, ex);
}
private static void handleDebugToast(String toastMessage, ClientType clientType) {
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastShort(String.format(toastMessage, clientType));
}
}
@Nullable
private static HttpURLConnection send(ClientType clientType,
String videoId,
@ -156,10 +154,7 @@ public class StreamingDataRequest {
final long startTime = System.currentTimeMillis();
try {
Route.CompiledRoute route = clientType.usePlayerEndpoint ?
GET_PLAYER_STREAMING_DATA : GET_REEL_STREAMING_DATA;
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(route, clientType);
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
@ -216,7 +211,7 @@ public class StreamingDataRequest {
return null;
}
private static byte[] fetch(String videoId, Map<String, String> playerHeaders) {
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
final boolean debugEnabled = BaseSettings.DEBUG.get();
// Retry with different client if empty response body is received.
@ -227,11 +222,33 @@ public class StreamingDataRequest {
HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast);
if (connection != null) {
byte[] playerResponseBuffer = buildPlayerResponseBuffer(clientType, connection);
if (playerResponseBuffer != null) {
lastSpoofedClientType = clientType;
try {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() == 0) {
if (BaseSettings.DEBUG.get() && BaseSettings.DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastShort("Debug: Ignoring empty spoof stream client " + clientType);
}
} else {
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
return playerResponseBuffer;
byte[] buffer = new byte[2048];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) >= 0) {
baos.write(buffer, 0, bytesRead);
}
if (clientType == ClientType.ANDROID_CREATOR && liveStreamBufferSearch.matches(buffer)) {
Logger.printDebug(() -> "Skipping Android Studio as video is a livestream: " + videoId);
} else {
lastSpoofedClientType = clientType;
return ByteBuffer.wrap(baos.toByteArray());
}
}
}
} catch (IOException ex) {
Logger.printException(() -> "Fetch failed while processing response data", ex);
}
}
}
@ -241,61 +258,12 @@ public class StreamingDataRequest {
return null;
}
@Nullable
private static byte[] buildPlayerResponseBuffer(ClientType clientType,
HttpURLConnection connection) {
// gzip encoding doesn't response with content length (-1),
// but empty response body does.
if (connection.getContentLength() == 0) {
handleDebugToast("Debug: Ignoring empty spoof stream client (%s)", clientType);
return null;
}
try (InputStream inputStream = connection.getInputStream()) {
PlayerResponse playerResponse = clientType.usePlayerEndpoint
? PlayerResponse.parseFrom(inputStream)
: ReelItemWatchResponse.parseFrom(inputStream).getPlayerResponse();
var playabilityStatus = playerResponse.getPlayabilityStatus();
if (playabilityStatus.getStatus() != PlayerResponseOuterClass.Status.OK) {
handleDebugToast("Debug: Ignoring unplayable video (%s)", clientType);
String reason = playabilityStatus.getReason();
if (isNotEmpty(reason)) {
Logger.printDebug(() -> String.format("Debug: Ignoring unplayable video (%s), reason: %s", clientType, reason));
}
return null;
}
PlayerResponse.Builder responseBuilder = playerResponse.toBuilder();
if (!playerResponse.hasStreamingData()) {
handleDebugToast("Debug: Ignoring empty streaming data (%s)", clientType);
return null;
}
// Android Studio only supports the HLS protocol for live streams.
// HLS protocol can theoretically be played with ExoPlayer,
// but the related code has not yet been implemented.
// If DASH protocol is not available, the client will be skipped.
StreamingData streamingData = playerResponse.getStreamingData();
if (streamingData.getAdaptiveFormatsCount() == 0) {
handleDebugToast("Debug: Ignoring empty adaptiveFormat (%s)", clientType);
return null;
}
return responseBuilder.build().toByteArray();
} catch (IOException ex) {
Logger.printException(() -> "Failed to write player response to buffer array", ex);
return null;
}
}
public boolean fetchCompleted() {
return future.isDone();
}
@Nullable
public byte[] getStream() {
public ByteBuffer getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {

View file

@ -1,55 +0,0 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
kotlin("jvm")
alias(libs.plugins.protobuf)
alias(libs.plugins.shadow)
}
val shade: Configuration by configurations.creating {
configurations.getByName("compileClasspath").extendsFrom(this)
configurations.getByName("runtimeClasspath").extendsFrom(this)
}
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
shade(libs.protobuf.javalite)
}
sourceSets {
// Make sure generated proto sources are compiled and end up in the shaded jar
main {
java.srcDir("$buildDir/generated/source/proto/main/java")
}
}
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
named("java") {
option("lite")
}
}
}
}
}
val shadowJar = tasks.named<ShadowJar>("shadowJar") {
configurations = listOf(shade)
relocate("com.google.protobuf", "app.revanced.com.google.protobuf")
}
configurations.named("runtimeElements") {
isCanBeConsumed = true
isCanBeResolved = false
outgoing.artifacts.clear()
outgoing.artifact(shadowJar)
}!!.let { artifacts { add(it.name, shadowJar) } }

View file

@ -1,42 +0,0 @@
syntax = "proto3";
package app.revanced.extension.shared.innertube;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.shared.innertube";
message PlayerResponse {
oneof data {
PlayabilityStatus playability_status = 2;
StreamingData streaming_data = 4;
}
}
message PlayabilityStatus {
Status status = 1;
string reason = 2;
}
enum Status {
OK = 0;
ERROR = 1;
UNPLAYABLE = 2;
LOGIN_REQUIRED = 3;
CONTENT_CHECK_REQUIRED = 4;
AGE_CHECK_REQUIRED = 5;
LIVE_STREAM_OFFLINE = 6;
FULLSCREEN_ONLY = 7;
GL_PLAYBACK_REQUIRED = 8;
AGE_VERIFICATION_REQUIRED = 9;
}
message StreamingData {
repeated Format formats = 2;
repeated Format adaptiveFormats = 3;
string serverAbrStreamingUrl = 15;
}
message Format {
string url = 2;
string signatureCipher = 48;
}

View file

@ -1,14 +0,0 @@
syntax = "proto3";
import "app/revanced/extension/shared/innertube/player_response.proto";
package app.revanced.extension.shared.innertube;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.shared.innertube";
message ReelItemWatchResponse {
oneof data {
PlayerResponse player_response = 4;
}
}

View file

@ -1,11 +1,39 @@
plugins {
alias(libs.plugins.protobuf)
}
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:spotify:stub"))
compileOnly(libs.annotation)
implementation(libs.nanohttpd)
implementation(libs.protobuf.javalite)
}
android {
defaultConfig {
minSdk = 24
minSdk = 21
}
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")
}
}
}
}
}

View file

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

View file

@ -0,0 +1,115 @@
package app.revanced.extension.spotify.misc.fix;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import static app.revanced.extension.spotify.misc.fix.Constants.*;
class ClientTokenService {
private static final String IOS_CLIENT_ID = "58bd3c95768941ea9eb4350aaa033eb3";
private static final String IOS_USER_AGENT;
static {
String clientVersion = getClientVersion();
int commitHashIndex = clientVersion.lastIndexOf(".");
String version = clientVersion.substring(
clientVersion.indexOf("-") + 1,
clientVersion.lastIndexOf(".", commitHashIndex - 1)
);
IOS_USER_AGENT = "Spotify/" + version + " iOS/" + getSystemVersion() + " (" + getHardwareMachine() + ")";
}
private static final ConnectivitySdkData.Builder IOS_CONNECTIVITY_SDK_DATA =
ConnectivitySdkData.newBuilder()
.setPlatformSpecificData(PlatformSpecificData.newBuilder()
.setIos(NativeIOSData.newBuilder()
.setHwMachine(getHardwareMachine())
.setSystemVersion(getSystemVersion())
)
);
private static final ClientDataRequest.Builder IOS_CLIENT_DATA_REQUEST =
ClientDataRequest.newBuilder()
.setClientVersion(getClientVersion())
.setClientId(IOS_CLIENT_ID);
private static final ClientTokenRequest.Builder IOS_CLIENT_TOKEN_REQUEST =
ClientTokenRequest.newBuilder()
.setRequestType(ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST);
@NonNull
static ClientTokenRequest newIOSClientTokenRequest(String deviceId) {
Logger.printInfo(() -> "Creating new iOS client token request with device ID: " + deviceId);
return IOS_CLIENT_TOKEN_REQUEST
.setClientData(IOS_CLIENT_DATA_REQUEST
.setConnectivitySdkData(IOS_CONNECTIVITY_SDK_DATA
.setDeviceId(deviceId)
)
)
.build();
}
@Nullable
static ClientTokenResponse getClientTokenResponse(@NonNull ClientTokenRequest request) {
if (request.getRequestType() == ClientTokenRequestType.REQUEST_CLIENT_DATA_REQUEST) {
Logger.printInfo(() -> "Requesting iOS client token");
String deviceId = request.getClientData().getConnectivitySdkData().getDeviceId();
request = newIOSClientTokenRequest(deviceId);
}
ClientTokenResponse response;
try {
response = requestClientToken(request);
} catch (IOException ex) {
Logger.printException(() -> "Failed to handle request", ex);
return null;
}
return response;
}
@NonNull
private static ClientTokenResponse requestClientToken(@NonNull ClientTokenRequest request) throws IOException {
HttpURLConnection urlConnection = (HttpURLConnection) new URL(CLIENT_TOKEN_API_URL).openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setDoOutput(true);
urlConnection.setRequestProperty("Content-Type", "application/x-protobuf");
urlConnection.setRequestProperty("Accept", "application/x-protobuf");
urlConnection.setRequestProperty("User-Agent", IOS_USER_AGENT);
byte[] requestArray = request.toByteArray();
urlConnection.setFixedLengthStreamingMode(requestArray.length);
urlConnection.getOutputStream().write(requestArray);
try (InputStream inputStream = urlConnection.getInputStream()) {
return ClientTokenResponse.parseFrom(inputStream);
}
}
@Nullable
static ClientTokenResponse serveClientTokenRequest(@NonNull InputStream inputStream) {
ClientTokenRequest request;
try {
request = ClientTokenRequest.parseFrom(inputStream);
} catch (IOException ex) {
Logger.printException(() -> "Failed to parse request from input stream", ex);
return null;
}
Logger.printInfo(() -> "Request of type: " + request.getRequestType());
ClientTokenResponse response = getClientTokenResponse(request);
if (response != null) Logger.printInfo(() -> "Response of type: " + response.getResponseType());
return response;
}
}

View file

@ -0,0 +1,26 @@
package app.revanced.extension.spotify.misc.fix;
import androidx.annotation.NonNull;
class Constants {
static final String CLIENT_TOKEN_API_PATH = "/v1/clienttoken";
static final String CLIENT_TOKEN_API_URL = "https://clienttoken.spotify.com" + CLIENT_TOKEN_API_PATH;
// Modified by a patch. Do not touch.
@NonNull
static String getClientVersion() {
return "";
}
// Modified by a patch. Do not touch.
@NonNull
static String getSystemVersion() {
return "";
}
// Modified by a patch. Do not touch.
@NonNull
static String getHardwareMachine() {
return "";
}
}

View file

@ -0,0 +1,94 @@
package app.revanced.extension.spotify.misc.fix;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.spotify.misc.fix.clienttoken.data.v0.ClienttokenHttp.ClientTokenResponse;
import com.google.protobuf.MessageLite;
import fi.iki.elonen.NanoHTTPD;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import static app.revanced.extension.spotify.misc.fix.ClientTokenService.serveClientTokenRequest;
import static app.revanced.extension.spotify.misc.fix.Constants.CLIENT_TOKEN_API_PATH;
import static fi.iki.elonen.NanoHTTPD.Response.Status.INTERNAL_ERROR;
class RequestListener extends NanoHTTPD {
RequestListener(int port) {
super(port);
try {
start();
} catch (IOException ex) {
Logger.printException(() -> "Failed to start request listener on port " + port, ex);
throw new RuntimeException(ex);
}
}
@NonNull
@Override
public Response serve(@NonNull IHTTPSession session) {
String uri = session.getUri();
if (!uri.equals(CLIENT_TOKEN_API_PATH)) return INTERNAL_ERROR_RESPONSE;
Logger.printInfo(() -> "Serving request for URI: " + uri);
ClientTokenResponse response = serveClientTokenRequest(getInputStream(session));
if (response != null) return newResponse(Response.Status.OK, response);
Logger.printException(() -> "Failed to serve client token request");
return INTERNAL_ERROR_RESPONSE;
}
@NonNull
private static InputStream newLimitedInputStream(InputStream inputStream, long contentLength) {
return new FilterInputStream(inputStream) {
private long remaining = contentLength;
@Override
public int read() throws IOException {
if (remaining <= 0) return -1;
int result = super.read();
if (result != -1) remaining--;
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (remaining <= 0) return -1;
len = (int) Math.min(len, remaining);
int result = super.read(b, off, len);
if (result != -1) remaining -= result;
return result;
}
};
}
@NonNull
private static InputStream getInputStream(@NonNull IHTTPSession session) {
long requestContentLength = Long.parseLong(Objects.requireNonNull(session.getHeaders().get("content-length")));
return newLimitedInputStream(session.getInputStream(), requestContentLength);
}
private static final Response INTERNAL_ERROR_RESPONSE = newResponse(INTERNAL_ERROR);
@SuppressWarnings("SameParameterValue")
@NonNull
private static Response newResponse(Response.Status status) {
return newResponse(status, null);
}
@NonNull
private static Response newResponse(Response.IStatus status, MessageLite messageLite) {
if (messageLite == null) {
return newFixedLengthResponse(status, "application/x-protobuf", null);
}
byte[] messageBytes = messageLite.toByteArray();
InputStream stream = new ByteArrayInputStream(messageBytes);
return newFixedLengthResponse(status, "application/x-protobuf", stream, messageBytes.length);
}
}

View file

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

View file

@ -14,6 +14,6 @@ public final class SanitizeSharingLinksPatch {
* Injection point.
*/
public static String sanitizeSharingLink(String url) {
return sanitizer.sanitizeURLString(url);
return sanitizer.sanitizeUrlString(url);
}
}

View file

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

View file

@ -0,0 +1,73 @@
syntax = "proto3";
package spotify.clienttoken.data.v0;
option optimize_for = LITE_RUNTIME;
option java_package = "app.revanced.extension.spotify.misc.fix.clienttoken.data.v0";
message ClientTokenRequest {
ClientTokenRequestType request_type = 1;
oneof request {
ClientDataRequest client_data = 2;
}
}
enum ClientTokenRequestType {
REQUEST_UNKNOWN = 0;
REQUEST_CLIENT_DATA_REQUEST = 1;
REQUEST_CHALLENGE_ANSWERS_REQUEST = 2;
}
message ClientDataRequest {
string client_version = 1;
string client_id = 2;
oneof data {
ConnectivitySdkData connectivity_sdk_data = 3;
}
}
message ConnectivitySdkData {
PlatformSpecificData platform_specific_data = 1;
string device_id = 2;
}
message PlatformSpecificData {
oneof data {
NativeIOSData ios = 2;
}
}
message NativeIOSData {
int32 user_interface_idiom = 1;
bool target_iphone_simulator = 2;
string hw_machine = 3;
string system_version = 4;
string simulator_model_identifier = 5;
}
message ClientTokenResponse {
ClientTokenResponseType response_type = 1;
oneof response {
GrantedTokenResponse granted_token = 2;
}
}
enum ClientTokenResponseType {
RESPONSE_UNKNOWN = 0;
RESPONSE_GRANTED_TOKEN_RESPONSE = 1;
RESPONSE_CHALLENGES_RESPONSE = 2;
}
message GrantedTokenResponse {
string token = 1;
int32 expires_after_seconds = 2;
int32 refresh_after_seconds = 3;
repeated TokenDomain domains = 4;
}
message TokenDomain {
string domain = 1;
}

View file

@ -7,11 +7,11 @@ android {
compileSdk = 34
defaultConfig {
minSdk = 24
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}

View file

@ -3,9 +3,3 @@ dependencies {
compileOnly(project(":extensions:strava:stub"))
compileOnly(libs.okhttp)
}
android {
defaultConfig {
minSdk = 26
}
}

View file

@ -1,15 +1,16 @@
package app.revanced.extension.strava;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
import app.revanced.extension.shared.ResourceType;
import com.strava.mediamodels.data.MediaType;
import com.strava.core.data.MediaType;
import com.strava.photos.data.Media;
import okhttp3.*;
@ -26,6 +27,7 @@ import java.util.stream.Stream;
import app.revanced.extension.shared.Utils;
@SuppressLint("NewApi")
public final class AddMediaDownloadPatch {
public static final int ACTION_DOWNLOAD = -1;
public static final int ACTION_OPEN_LINK = -2;
@ -82,7 +84,7 @@ public final class AddMediaDownloadPatch {
} finally {
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(row, values, null, null);
resolver.update(row, values, null);
}
showInfoToast("yis_2024_local_save_image_success", "✔️");
} catch (IOException e) {
@ -148,7 +150,7 @@ public final class AddMediaDownloadPatch {
} finally {
values.clear();
values.put(MediaStore.Video.Media.IS_PENDING, 0);
resolver.update(row, values, null, null);
resolver.update(row, values, null);
}
showInfoToast("yis_2024_local_save_video_success", "✔️");
} catch (IOException e) {
@ -164,7 +166,7 @@ public final class AddMediaDownloadPatch {
}
private static String getString(String name, String fallback) {
int id = Utils.getResourceIdentifier(ResourceType.STRING, name);
int id = Utils.getResourceIdentifier(name, "string");
return id != 0
? Utils.getResourceString(id)
: fallback;

View file

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

View file

@ -7,6 +7,6 @@ android {
compileSdk = 34
defaultConfig {
minSdk = 26
minSdk = 21
}
}

View file

@ -1,4 +1,4 @@
package com.strava.mediamodels.data;
package com.strava.core.data;
import java.io.Serializable;

View file

@ -1,4 +1,4 @@
package com.strava.mediamodels.data;
package com.strava.core.data;
import java.io.Serializable;

View file

@ -1,4 +1,4 @@
package com.strava.mediamodels.data;
package com.strava.core.data;
public enum MediaType {
PHOTO(1),

View file

@ -1,4 +1,4 @@
package com.strava.mediamodels.data;
package com.strava.core.data;
import java.util.SortedMap;

View file

@ -1,4 +1,4 @@
package com.strava.mediamodels.data;
package com.strava.core.data;
public enum RemoteMediaStatus {
NEW,

View file

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

View file

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

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