diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 2d6d258f47..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,3 +0,0 @@ -[*.{kt,kts}] -ktlint_code_style = intellij_idea -ktlint_standard_no-wildcard-imports = disabled \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f623d8a579..98b14a097d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -72,6 +72,7 @@ body: - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22). - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md). + - **Check the troubleshooting guide**: A solution to your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md). - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). - type: textarea attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index f49436ec6b..13d436ba29 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -72,6 +72,7 @@ body: - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22). - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md). + - **Check the troubleshooting guide**: Information about your issue might be found in the [FAQ](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/questions.md) or the [troubleshooting guide](https://github.com/ReVanced/revanced-documentation/blob/main/docs/revanced-resources/troubleshooting.md). - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). - type: textarea attributes: diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 193a26af05..32c03cbe64 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -2,6 +2,10 @@ name: Build pull request on: workflow_dispatch: + inputs: + pr: + description: "PR to build" + required: true pull_request: branches: - dev @@ -10,22 +14,33 @@ jobs: release: name: Build runs-on: ubuntu-latest + permissions: + contents: read + steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: - fetch-depth: 0 + ref: ${{ inputs.pr && format('refs/pull/{0}/merge', inputs.pr) || github.ref }} - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - distribution: "temurin" - java-version: "17" + distribution: 'temurin' + java-version: '17' - name: Cache Gradle - uses: burrunan/gradle-cache-action@v1 + uses: burrunan/gradle-cache-action@v3 - name: Build env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew build --no-daemon + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew :patches:buildAndroid --no-daemon + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: revanced-patches + path: patches/build/libs + archive: false diff --git a/.github/workflows/open_pull_request.yml b/.github/workflows/open_pull_request.yml index 33c8a7211f..c1402f5490 100644 --- a/.github/workflows/open_pull_request.yml +++ b/.github/workflows/open_pull_request.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Open pull request uses: repo-sync/pull-request@v2 diff --git a/.github/workflows/pull_strings.yml b/.github/workflows/pull_strings.yml index b27d9166aa..f2da51ec74 100644 --- a/.github/workflows/pull_strings.yml +++ b/.github/workflows/pull_strings.yml @@ -12,25 +12,43 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: - fetch-depth: 0 ref: dev + persist-credentials: true - name: Pull strings uses: crowdin/github-action@v2 with: config: crowdin.yml + upload_sources: false download_translations: true - localization_branch_name: feat/translations - create_pull_request: true - pull_request_title: "chore: Sync translations" - pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)" - pull_request_base_branch_name: "dev" - commit_message: "chore: Sync translations" - github_user_name: revanced-bot - github_user_email: github@revanced.app + push_translations: false + skip_ref_checkout: true 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 + uses: repo-sync/pull-request@v2 + with: + source_branch: feat/translations + destination_branch: dev + pr_title: "chore: Sync translations" + pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)" diff --git a/.github/workflows/push_strings.yml b/.github/workflows/push_strings.yml index 0cd3b4eb64..d13f88573d 100644 --- a/.github/workflows/push_strings.yml +++ b/.github/workflows/push_strings.yml @@ -14,9 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 + uses: actions/checkout@v6 + + - name: Process strings + env: + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew processStringsForCrowdin - name: Push strings uses: crowdin/github-action@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 498cca4135..27a19a17c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,34 +13,33 @@ jobs: permissions: contents: write packages: write + id-token: write + attestations: write + artifact-metadata: write runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 - with: - # Make sure the release step uses its own credentials: - # https://github.com/cycjimmy/semantic-release-action#private-packages - persist-credentials: false - fetch-depth: 0 + uses: actions/checkout@v6 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: - distribution: "temurin" - java-version: "17" + distribution: 'temurin' + java-version: '17' - name: Cache Gradle - uses: burrunan/gradle-cache-action@v1 + uses: burrunan/gradle-cache-action@v3 - name: Build env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew build clean + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew :patches:buildAndroid clean - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "lts/*" + node-version: 'lts/*' cache: 'npm' - name: Install dependencies @@ -54,6 +53,16 @@ jobs: fingerprint: ${{ vars.GPG_FINGERPRINT }} - name: Release + uses: cycjimmy/semantic-release-action@v5 + id: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npm exec semantic-release + 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 + with: + subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}' + subject-path: patches/build/libs/patches-*.rvp diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml index 8136ad5f31..f02668b990 100644 --- a/.github/workflows/update-gradle-wrapper.yml +++ b/.github/workflows/update-gradle-wrapper.yml @@ -10,9 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Update Gradle Wrapper - uses: gradle-update/update-gradle-wrapper-action@v1 + uses: gradle-update/update-gradle-wrapper-action@v2 with: target-branch: dev diff --git a/.releaserc b/.releaserc index b3d61b10b1..ee495bf966 100644 --- a/.releaserc +++ b/.releaserc @@ -22,7 +22,7 @@ { "assets": [ "CHANGELOG.md", - "gradle.properties", + "gradle.properties" ], "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } @@ -33,16 +33,16 @@ "assets": [ { "path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)" - }, + } ], - successComment: false + "successComment": false } ], [ "@saithodev/semantic-release-backmerge", { - backmergeBranches: [{"from": "main", "to": "dev"}], - clearWorkspace: true + "backmergeBranches": [{"from": "main", "to": "dev"}], + "clearWorkspace": true } ] ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a12103dd..bfebce7e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5256 @@ +# [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) + + +### Features + +* Add `Disable Sentry telemetry` patch ([#6416](https://github.com/ReVanced/revanced-patches/issues/6416)) ([4cc3159](https://github.com/ReVanced/revanced-patches/commit/4cc315952db557c565872de9e8484805f2e42305)) +* Disable Play Integrity patch ([#6412](https://github.com/ReVanced/revanced-patches/issues/6412)) ([6312fe8](https://github.com/ReVanced/revanced-patches/commit/6312fe8d60da24465c0c1b0fa4e94ceb79873d9c)) + +# [5.48.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.7...v5.48.0-dev.8) (2026-01-04) + + +### Features + +* **Letterboxd:** Add `Unlock app icons` patch ([#6415](https://github.com/ReVanced/revanced-patches/issues/6415)) ([d25dcfe](https://github.com/ReVanced/revanced-patches/commit/d25dcfe49ac331c9b3dca739ba0be95dbab669cc)) + +# [5.48.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.6...v5.48.0-dev.7) (2026-01-04) + + +### Features + +* **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 `Overwrite media upload parameters` patch ([#6410](https://github.com/ReVanced/revanced-patches/issues/6410)) ([b42ae27](https://github.com/ReVanced/revanced-patches/commit/b42ae27ce66ebad9e9cfc5b70fc121df5bad7567)) + +# [5.48.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.5...v5.48.0-dev.6) (2026-01-04) + + +### Bug Fixes + +* Fix build error introduced in `4046bee` ([#6417](https://github.com/ReVanced/revanced-patches/issues/6417)) ([789f0a5](https://github.com/ReVanced/revanced-patches/commit/789f0a562861825065633d172445ebf35a1ba8d8)) + +# [5.48.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.4...v5.48.0-dev.5) (2025-12-30) + + +### Bug Fixes + +* **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)) + +# [5.48.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.3...v5.48.0-dev.4) (2025-12-29) + + +### Features + +* **Strava:** Add `Block Snowplow tracking` patch ([#6413](https://github.com/ReVanced/revanced-patches/issues/6413)) ([c47beae](https://github.com/ReVanced/revanced-patches/commit/c47beae21376dd17ab8bc09afe73e9094481bde9)) + +# [5.48.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.2...v5.48.0-dev.3) (2025-12-28) + + +### Bug Fixes + +* Fix compilation error introduced in `6bb6281` ([#6409](https://github.com/ReVanced/revanced-patches/issues/6409)) ([71c6cb5](https://github.com/ReVanced/revanced-patches/commit/71c6cb569ebf7b93cf73ee391839e5220557ce7c)) + + +### Features + +* **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)) + +# [5.48.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.48.0-dev.1...v5.48.0-dev.2) (2025-12-27) + + +### Features + +* **Strava:** Add `Enable password login` patch ([#6396](https://github.com/ReVanced/revanced-patches/issues/6396)) ([8f3f4c9](https://github.com/ReVanced/revanced-patches/commit/8f3f4c95bb8f151fc9a2c272bf7d0e905c2f01fc)) + +# [5.48.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.47.0...v5.48.0-dev.1) (2025-12-23) + + +### Bug Fixes + +* Fix compilation error introduced in dc69f243 ([#6392](https://github.com/ReVanced/revanced-patches/issues/6392)) ([a429824](https://github.com/ReVanced/revanced-patches/commit/a429824bb77b49aea14b0b54f2204ae24d5209a1)) +* **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 + +* **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)) + +# [5.47.0](https://github.com/ReVanced/revanced-patches/compare/v5.46.0...v5.47.0) (2025-12-18) + + +### Bug Fixes + +* **Instagram - Disable signature check:** Change patch to default excluded ([#6283](https://github.com/ReVanced/revanced-patches/issues/6283)) ([bb745b5](https://github.com/ReVanced/revanced-patches/commit/bb745b555b3808b7679c5995319aa365630fbd76)) +* **Lightroom:** Add `Disable version check` patch to fix opening the app ([#6315](https://github.com/ReVanced/revanced-patches/issues/6315)) ([018d176](https://github.com/ReVanced/revanced-patches/commit/018d176914a06a30e9007a3eb2e6b0f459078413)) +* **Reddit - Hide ads:** Update patch for new versions of Reddit ([#6342](https://github.com/ReVanced/revanced-patches/issues/6342)) ([f8bd123](https://github.com/ReVanced/revanced-patches/commit/f8bd1239cc0f0bd1c2dca39f846951bf512891e3)) +* **Spotify:** Make patches work with latest versions again ([#6359](https://github.com/ReVanced/revanced-patches/issues/6359)) ([34830ba](https://github.com/ReVanced/revanced-patches/commit/34830ba63b436146064f0f89f948d51cd0cb9146)) +* **YouTube - Hide layout components:** Fix "Hide Subscribe button" in channel page not working ([#6363](https://github.com/ReVanced/revanced-patches/issues/6363)) ([ded8370](https://github.com/ReVanced/revanced-patches/commit/ded83702077701aac8a8749d71bf7376427f37d6)) +* **YouTube - Hide player flyout menu items:** Allow hiding audio menu with 'Android No SDK' client type ([9495cf4](https://github.com/ReVanced/revanced-patches/commit/9495cf49ef8a872be64de6c971c1919b4b9a8720)) +* **YouTube - Sanitize sharing links:** Handle non hierarchical urls ([654d091](https://github.com/ReVanced/revanced-patches/commit/654d091e650cda37650b57cbf3ba6f1cdd6d47d3)) + + +### Features + +* **Disney+ - SkipAds:** Add other package names the patch is compatible with ([#6372](https://github.com/ReVanced/revanced-patches/issues/6372)) ([1f4f252](https://github.com/ReVanced/revanced-patches/commit/1f4f252c81e9a89267f6e37548e66027b1bc1a1a)) +* **Disney+:** Add `Skip ads` patch ([#6343](https://github.com/ReVanced/revanced-patches/issues/6343)) ([6bd7dca](https://github.com/ReVanced/revanced-patches/commit/6bd7dca75bd2ea335a596aa93a8b767d39be5f83)) +* **IdAustria - Remove device integrity check:** Update patch to work with latest version ([#6360](https://github.com/ReVanced/revanced-patches/issues/6360)) ([0ea3491](https://github.com/ReVanced/revanced-patches/commit/0ea3491227fc50c03555d43d3fec78eb82906b26)) +* **Instagram:** Add `Anonymous story viewing` patch ([#6263](https://github.com/ReVanced/revanced-patches/issues/6263)) ([94ae84a](https://github.com/ReVanced/revanced-patches/commit/94ae84ad0fc3a9197c82d5356301d464730c3b17)) +* **Instagram:** Add `Disable auto story flipping` patch ([#6262](https://github.com/ReVanced/revanced-patches/issues/6262)) ([2f0de15](https://github.com/ReVanced/revanced-patches/commit/2f0de15e67e4f99ed6ecdc136d04cceb23b0d069)) +* **Instagram:** Add `Disable Reels scrolling` patch ([#6317](https://github.com/ReVanced/revanced-patches/issues/6317)) ([0928dcd](https://github.com/ReVanced/revanced-patches/commit/0928dcd00dc2a9c1eef9a23c1e26ff5dc9ee670a)) +* **Letterboxd:** Add `Hide ads` patch ([#6309](https://github.com/ReVanced/revanced-patches/issues/6309)) ([0af0ee9](https://github.com/ReVanced/revanced-patches/commit/0af0ee92c48bb2ffc332197e05439e20c5c05d83)) +* **Peacock TV:** Add `Hide ads` patch ([#6348](https://github.com/ReVanced/revanced-patches/issues/6348)) ([847ee18](https://github.com/ReVanced/revanced-patches/commit/847ee189a971e6d4a99823998569f8e561b8319c)) +* **ProtonVPN:** Add `Remove delay` patch ([#6326](https://github.com/ReVanced/revanced-patches/issues/6326)) ([bbd8932](https://github.com/ReVanced/revanced-patches/commit/bbd8932b2e740aff96ba047332e541bff3e09436)) +* **Spoof SIM provider:** Spoof additional TelephonyManager methods ([#6293](https://github.com/ReVanced/revanced-patches/issues/6293)) ([ac583d4](https://github.com/ReVanced/revanced-patches/commit/ac583d40d0f4c0e6544e3661ff3e82a25912f2b0)) +* **YouTube - Hide layout components:** Add "Hide cell divider", "Hide featured links", and "Hide featured videos" options ([#6335](https://github.com/ReVanced/revanced-patches/issues/6335)) ([a5d197b](https://github.com/ReVanced/revanced-patches/commit/a5d197b9775b98d7a37bfdee9e5f726d5e04d8cf)) +* **YouTube - Hide layout components:** Add "Hide Join button" and "Hide Subscribe button" options for channel page ([#6345](https://github.com/ReVanced/revanced-patches/issues/6345)) ([02831a6](https://github.com/ReVanced/revanced-patches/commit/02831a6069fc30ffa3a87f8e4de653d003a2187e)) +* **YouTube - Hide Shorts components:** Add "Hide auto-dubbed label" and "Hide live preview" options ([#6334](https://github.com/ReVanced/revanced-patches/issues/6334)) ([a7c220a](https://github.com/ReVanced/revanced-patches/commit/a7c220a4aea93ea7ae7005b5760443d7571c4228)) + +# [5.47.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.17...v5.47.0-dev.18) (2025-12-18) + + +### Features + +* **Disney+ - SkipAds:** Add other package names the patch is compatible with ([#6372](https://github.com/ReVanced/revanced-patches/issues/6372)) ([1f4f252](https://github.com/ReVanced/revanced-patches/commit/1f4f252c81e9a89267f6e37548e66027b1bc1a1a)) + +# [5.47.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.16...v5.47.0-dev.17) (2025-12-18) + + +### Bug Fixes + +* **Reddit - Hide ads:** Update patch for new versions of Reddit ([#6342](https://github.com/ReVanced/revanced-patches/issues/6342)) ([f8bd123](https://github.com/ReVanced/revanced-patches/commit/f8bd1239cc0f0bd1c2dca39f846951bf512891e3)) + +# [5.47.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.15...v5.47.0-dev.16) (2025-12-15) + + +### Bug Fixes + +* **Lightroom:** Add `Disable version check` patch to fix opening the app ([#6315](https://github.com/ReVanced/revanced-patches/issues/6315)) ([018d176](https://github.com/ReVanced/revanced-patches/commit/018d176914a06a30e9007a3eb2e6b0f459078413)) + + +### Features + +* **IdAustria - Remove device integrity check:** Update patch to work with latest version ([#6360](https://github.com/ReVanced/revanced-patches/issues/6360)) ([0ea3491](https://github.com/ReVanced/revanced-patches/commit/0ea3491227fc50c03555d43d3fec78eb82906b26)) + +# [5.47.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.14...v5.47.0-dev.15) (2025-12-13) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix "Hide Subscribe button" in channel page not working ([#6363](https://github.com/ReVanced/revanced-patches/issues/6363)) ([ded8370](https://github.com/ReVanced/revanced-patches/commit/ded83702077701aac8a8749d71bf7376427f37d6)) + +# [5.47.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.13...v5.47.0-dev.14) (2025-12-13) + + +### Bug Fixes + +* **Spotify:** Make patches work with latest versions again ([#6359](https://github.com/ReVanced/revanced-patches/issues/6359)) ([34830ba](https://github.com/ReVanced/revanced-patches/commit/34830ba63b436146064f0f89f948d51cd0cb9146)) + +# [5.47.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.12...v5.47.0-dev.13) (2025-12-10) + + +### Features + +* **Peacock TV:** Add `Hide ads` patch ([#6348](https://github.com/ReVanced/revanced-patches/issues/6348)) ([847ee18](https://github.com/ReVanced/revanced-patches/commit/847ee189a971e6d4a99823998569f8e561b8319c)) + +# [5.47.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.11...v5.47.0-dev.12) (2025-12-08) + + +### Features + +* **YouTube - Hide layout components:** Add "Hide Join button" and "Hide Subscribe button" options for channel page ([#6345](https://github.com/ReVanced/revanced-patches/issues/6345)) ([02831a6](https://github.com/ReVanced/revanced-patches/commit/02831a6069fc30ffa3a87f8e4de653d003a2187e)) + +# [5.47.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.10...v5.47.0-dev.11) (2025-12-08) + + +### Features + +* **Disney+:** Add `Skip ads` patch ([#6343](https://github.com/ReVanced/revanced-patches/issues/6343)) ([6bd7dca](https://github.com/ReVanced/revanced-patches/commit/6bd7dca75bd2ea335a596aa93a8b767d39be5f83)) + +# [5.47.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.9...v5.47.0-dev.10) (2025-12-08) + + +### Features + +* **YouTube - Hide Shorts components:** Add "Hide auto-dubbed label" and "Hide live preview" options ([#6334](https://github.com/ReVanced/revanced-patches/issues/6334)) ([a7c220a](https://github.com/ReVanced/revanced-patches/commit/a7c220a4aea93ea7ae7005b5760443d7571c4228)) + +# [5.47.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.8...v5.47.0-dev.9) (2025-12-08) + + +### Features + +* **YouTube - Hide layout components:** Add "Hide cell divider", "Hide featured links", and "Hide featured videos" options ([#6335](https://github.com/ReVanced/revanced-patches/issues/6335)) ([a5d197b](https://github.com/ReVanced/revanced-patches/commit/a5d197b9775b98d7a37bfdee9e5f726d5e04d8cf)) + +# [5.47.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.7...v5.47.0-dev.8) (2025-12-08) + + +### Features + +* **Instagram:** Add `Disable Reels scrolling` patch ([#6317](https://github.com/ReVanced/revanced-patches/issues/6317)) ([0928dcd](https://github.com/ReVanced/revanced-patches/commit/0928dcd00dc2a9c1eef9a23c1e26ff5dc9ee670a)) +* **ProtonVPN:** Add `Remove delay` patch ([#6326](https://github.com/ReVanced/revanced-patches/issues/6326)) ([bbd8932](https://github.com/ReVanced/revanced-patches/commit/bbd8932b2e740aff96ba047332e541bff3e09436)) + +# [5.47.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.6...v5.47.0-dev.7) (2025-12-03) + + +### Features + +* **Spoof SIM provider:** Spoof additional TelephonyManager methods ([#6293](https://github.com/ReVanced/revanced-patches/issues/6293)) ([ac583d4](https://github.com/ReVanced/revanced-patches/commit/ac583d40d0f4c0e6544e3661ff3e82a25912f2b0)) + +# [5.47.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.5...v5.47.0-dev.6) (2025-11-24) + + +### Features + +* **Letterboxd:** Add `Hide ads` patch ([#6309](https://github.com/ReVanced/revanced-patches/issues/6309)) ([0af0ee9](https://github.com/ReVanced/revanced-patches/commit/0af0ee92c48bb2ffc332197e05439e20c5c05d83)) + +# [5.47.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.4...v5.47.0-dev.5) (2025-11-13) + + +### Bug Fixes + +* **YouTube - Hide player flyout menu items:** Allow hiding audio menu with 'Android No SDK' client type ([9495cf4](https://github.com/ReVanced/revanced-patches/commit/9495cf49ef8a872be64de6c971c1919b4b9a8720)) + +# [5.47.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.3...v5.47.0-dev.4) (2025-11-12) + + +### Bug Fixes + +* **YouTube - Sanitize sharing links:** Handle non hierarchical urls ([654d091](https://github.com/ReVanced/revanced-patches/commit/654d091e650cda37650b57cbf3ba6f1cdd6d47d3)) + +# [5.47.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.2...v5.47.0-dev.3) (2025-11-12) + + +### Features + +* **Instagram:** Add `Disable auto story flipping` patch ([#6262](https://github.com/ReVanced/revanced-patches/issues/6262)) ([2f0de15](https://github.com/ReVanced/revanced-patches/commit/2f0de15e67e4f99ed6ecdc136d04cceb23b0d069)) + +# [5.47.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.47.0-dev.1...v5.47.0-dev.2) (2025-11-12) + + +### Bug Fixes + +* **Instagram - Disable signature check:** Change patch to default excluded ([#6283](https://github.com/ReVanced/revanced-patches/issues/6283)) ([bb745b5](https://github.com/ReVanced/revanced-patches/commit/bb745b555b3808b7679c5995319aa365630fbd76)) + +# [5.47.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.46.0...v5.47.0-dev.1) (2025-11-12) + + +### Features + +* **Instagram:** Add `Anonymous story viewing` patch ([#6263](https://github.com/ReVanced/revanced-patches/issues/6263)) ([94ae84a](https://github.com/ReVanced/revanced-patches/commit/94ae84ad0fc3a9197c82d5356301d464730c3b17)) + +# [5.46.0](https://github.com/ReVanced/revanced-patches/compare/v5.45.0...v5.46.0) (2025-11-10) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Constrain patch to last working app target ([f238ae9](https://github.com/ReVanced/revanced-patches/commit/f238ae9895000f01d1dccb800cc8efde0d5362bd)) +* **Instagram - Hide navigation buttons:** Constrain patch to last working app target ([e030e9c](https://github.com/ReVanced/revanced-patches/commit/e030e9c07a7748e117ac44f6776a9f6317b20623)) +* **Spotify - Hide Create button:** Remove obsolete patch that is no longer needed ([#6252](https://github.com/ReVanced/revanced-patches/issues/6252)) ([59d85b2](https://github.com/ReVanced/revanced-patches/commit/59d85b28a7fcb285ff5f2bb6ae654020d76b2019)) +* **YouTube - Check watch history domain name resolution:** Fix false positive warning message if the internet connection fails halfway into the DNS check ([5726353](https://github.com/ReVanced/revanced-patches/commit/57263538c79f5a561c449229ac8e068c641285d3)) +* **YouTube - Hide layout components:** Fix "Hide Hype points" ([#6247](https://github.com/ReVanced/revanced-patches/issues/6247)) ([5821440](https://github.com/ReVanced/revanced-patches/commit/582144026d28e57bb7adcbba39244f3c7cdbc0f3)) +* **YouTube - Settings:** Add additional languages to ReVanced language preference ([d390b54](https://github.com/ReVanced/revanced-patches/commit/d390b54dab92d75b4e0d3e38344eae489dd69d98)) +* **YouTube - Settings:** Resolve settings search crash when searching for specific words ([#6231](https://github.com/ReVanced/revanced-patches/issues/6231)) ([76dcfae](https://github.com/ReVanced/revanced-patches/commit/76dcfaefd8679e45a70f265b0239436e60c055cf)) + + +### Features + +* **YouTube - Debugging:** Add setting to block experimental client flags ([#6196](https://github.com/ReVanced/revanced-patches/issues/6196)) ([2e9d695](https://github.com/ReVanced/revanced-patches/commit/2e9d6959c94df7588b9e34b18770e9f437e91926)) +* **YouTube - Hide layout components:** Add "Hide Hype points" ([#6230](https://github.com/ReVanced/revanced-patches/issues/6230)) ([a52c015](https://github.com/ReVanced/revanced-patches/commit/a52c0153b12c3f6f0ad260e03d2e9850c0466392)) +* **YouTube - Hide layout components:** Add video description "Hide Featured content" and "Hide Subscribe button" ([#6253](https://github.com/ReVanced/revanced-patches/issues/6253)) ([da4cf94](https://github.com/ReVanced/revanced-patches/commit/da4cf940911a4406e2c9dd558b60305385a80c61)) +* **YouTube - Hide player flyout menu items:** Add "Hide Listen with YouTube Music" ([#6232](https://github.com/ReVanced/revanced-patches/issues/6232)) ([858edbf](https://github.com/ReVanced/revanced-patches/commit/858edbf3e7f394fcc766d767c8dc54cf5ba24370)) +* **YouTube Music:** Add `Change miniplayer color` patch ([#6259](https://github.com/ReVanced/revanced-patches/issues/6259)) ([ab808ae](https://github.com/ReVanced/revanced-patches/commit/ab808aeb773592cb26c848d8456478a346ec3bad)) +* **YouTube Music:** Add `Hide buttons` patch ([#6255](https://github.com/ReVanced/revanced-patches/issues/6255)) ([7a18ebc](https://github.com/ReVanced/revanced-patches/commit/7a18ebc7ab74ba30c5d5284a4856c55cdfc31097)) + +# [5.46.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.9...v5.46.0-dev.10) (2025-11-09) + + +### Features + +* **YouTube - Hide layout components:** Add video description "Hide Featured content" and "Hide Subscribe button" ([#6253](https://github.com/ReVanced/revanced-patches/issues/6253)) ([da4cf94](https://github.com/ReVanced/revanced-patches/commit/da4cf940911a4406e2c9dd558b60305385a80c61)) + +# [5.46.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.8...v5.46.0-dev.9) (2025-11-09) + + +### Features + +* **YouTube Music:** Add `Change miniplayer color` patch ([#6259](https://github.com/ReVanced/revanced-patches/issues/6259)) ([ab808ae](https://github.com/ReVanced/revanced-patches/commit/ab808aeb773592cb26c848d8456478a346ec3bad)) + +# [5.46.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.7...v5.46.0-dev.8) (2025-11-09) + + +### Features + +* **YouTube Music:** Add `Hide buttons` patch ([#6255](https://github.com/ReVanced/revanced-patches/issues/6255)) ([7a18ebc](https://github.com/ReVanced/revanced-patches/commit/7a18ebc7ab74ba30c5d5284a4856c55cdfc31097)) + +# [5.46.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.6...v5.46.0-dev.7) (2025-11-08) + + +### Bug Fixes + +* **YouTube - Settings:** Add additional languages to ReVanced language preference ([d390b54](https://github.com/ReVanced/revanced-patches/commit/d390b54dab92d75b4e0d3e38344eae489dd69d98)) + +# [5.46.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.5...v5.46.0-dev.6) (2025-11-08) + + +### Features + +* **YouTube - Debugging:** Add setting to block experimental client flags ([#6196](https://github.com/ReVanced/revanced-patches/issues/6196)) ([2e9d695](https://github.com/ReVanced/revanced-patches/commit/2e9d6959c94df7588b9e34b18770e9f437e91926)) + +# [5.46.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.4...v5.46.0-dev.5) (2025-11-07) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Constrain patch to last working app target ([f238ae9](https://github.com/ReVanced/revanced-patches/commit/f238ae9895000f01d1dccb800cc8efde0d5362bd)) +* **Instagram - Hide navigation buttons:** Constrain patch to last working app target ([e030e9c](https://github.com/ReVanced/revanced-patches/commit/e030e9c07a7748e117ac44f6776a9f6317b20623)) +* **Spotify - Hide Create button:** Remove obsolete patch that is no longer needed ([#6252](https://github.com/ReVanced/revanced-patches/issues/6252)) ([59d85b2](https://github.com/ReVanced/revanced-patches/commit/59d85b28a7fcb285ff5f2bb6ae654020d76b2019)) + +# [5.46.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.3...v5.46.0-dev.4) (2025-11-07) + + +### Bug Fixes + +* **YouTube - Check watch history domain name resolution:** Fix false positive warning message if the internet connection fails halfway into the DNS check ([5726353](https://github.com/ReVanced/revanced-patches/commit/57263538c79f5a561c449229ac8e068c641285d3)) + +# [5.46.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.2...v5.46.0-dev.3) (2025-11-06) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix "Hide Hype points" ([#6247](https://github.com/ReVanced/revanced-patches/issues/6247)) ([5821440](https://github.com/ReVanced/revanced-patches/commit/582144026d28e57bb7adcbba39244f3c7cdbc0f3)) + +# [5.46.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.46.0-dev.1...v5.46.0-dev.2) (2025-11-04) + + +### Bug Fixes + +* **YouTube - Settings:** Resolve settings search crash when searching for specific words ([#6231](https://github.com/ReVanced/revanced-patches/issues/6231)) ([76dcfae](https://github.com/ReVanced/revanced-patches/commit/76dcfaefd8679e45a70f265b0239436e60c055cf)) + +# [5.46.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.45.0...v5.46.0-dev.1) (2025-11-04) + + +### Features + +* **YouTube - Hide layout components:** Add "Hide Hype points" ([#6230](https://github.com/ReVanced/revanced-patches/issues/6230)) ([a52c015](https://github.com/ReVanced/revanced-patches/commit/a52c0153b12c3f6f0ad260e03d2e9850c0466392)) +* **YouTube - Hide player flyout menu items:** Add "Hide Listen with YouTube Music" ([#6232](https://github.com/ReVanced/revanced-patches/issues/6232)) ([858edbf](https://github.com/ReVanced/revanced-patches/commit/858edbf3e7f394fcc766d767c8dc54cf5ba24370)) + +# [5.45.0](https://github.com/ReVanced/revanced-patches/compare/v5.44.0...v5.45.0) (2025-11-01) + + +### Bug Fixes + +* **Instagram:** Update failing fingerprints on newer versions ([#6181](https://github.com/ReVanced/revanced-patches/issues/6181)) ([c73a03c](https://github.com/ReVanced/revanced-patches/commit/c73a03c9e18a12262939c974cdf16221221d1487)) +* **TikTok - Downloads:** Fix download path setting ([#6191](https://github.com/ReVanced/revanced-patches/issues/6191)) ([3e4990a](https://github.com/ReVanced/revanced-patches/commit/3e4990afff4c86b93970b153db713ad0f813124d)) +* **YouTube - Change header:** Do not mirror header graphic with RTL languages ([a0c5604](https://github.com/ReVanced/revanced-patches/commit/a0c56049510ce040e1ccd49257864672c343344d)) +* **YouTube - Force original audio:** Fall back to visionOS and not Android Studio if Android VR is not available ([6d01863](https://github.com/ReVanced/revanced-patches/commit/6d01863ec70617d9abc864ce6686ed9764dd151d)) +* **YouTube - Spoof video streams:** Remove spoof stream audio selector that no longer works ([292fae4](https://github.com/ReVanced/revanced-patches/commit/292fae440c6d5694c5e84407becec2d91f1fd156)) +* **YouTube Music - Hide category bar:** Correctly hide the category bar in newer app targets ([#6175](https://github.com/ReVanced/revanced-patches/issues/6175)) ([13cf172](https://github.com/ReVanced/revanced-patches/commit/13cf1724bf2f946c7129cab0db96721c90f9fe89)) + + +### Features + +* **Spoof video streams:** Add experimental "Android No SDK" client type ([5f23bfe](https://github.com/ReVanced/revanced-patches/commit/5f23bfe833c6e01617a7dbc5325b4a3fb931e53e)) +* **TikTok:** Add `Sanitize sharing links` patch ([#6176](https://github.com/ReVanced/revanced-patches/issues/6176)) ([ef44eaa](https://github.com/ReVanced/revanced-patches/commit/ef44eaa119b9d6c5faec051e22d20f883d0da4f1)) +* **YouTube - Change Header:** Use SVG for header logo ([#6178](https://github.com/ReVanced/revanced-patches/issues/6178)) ([e9f45ce](https://github.com/ReVanced/revanced-patches/commit/e9f45ce92695d5857473ff71c14b190bded28a73)) + +# [5.45.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.5...v5.45.0-dev.6) (2025-11-01) + + +### Features + +* **Spoof video streams:** Add experimental "Android No SDK" client type ([5f23bfe](https://github.com/ReVanced/revanced-patches/commit/5f23bfe833c6e01617a7dbc5325b4a3fb931e53e)) + +# [5.45.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.4...v5.45.0-dev.5) (2025-11-01) + + +### Bug Fixes + +* **TikTok - Downloads:** Fix download path setting ([#6191](https://github.com/ReVanced/revanced-patches/issues/6191)) ([3e4990a](https://github.com/ReVanced/revanced-patches/commit/3e4990afff4c86b93970b153db713ad0f813124d)) +* **YouTube - Spoof video streams:** Remove spoof stream audio selector that no longer works ([292fae4](https://github.com/ReVanced/revanced-patches/commit/292fae440c6d5694c5e84407becec2d91f1fd156)) + +# [5.45.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.3...v5.45.0-dev.4) (2025-10-30) + + +### Bug Fixes + +* **YouTube - Change header:** Do not mirror header graphic with RTL languages ([a0c5604](https://github.com/ReVanced/revanced-patches/commit/a0c56049510ce040e1ccd49257864672c343344d)) + +# [5.45.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.2...v5.45.0-dev.3) (2025-10-27) + + +### Features + +* **YouTube - Change Header:** Use SVG for header logo ([#6178](https://github.com/ReVanced/revanced-patches/issues/6178)) ([e9f45ce](https://github.com/ReVanced/revanced-patches/commit/e9f45ce92695d5857473ff71c14b190bded28a73)) + +# [5.45.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.45.0-dev.1...v5.45.0-dev.2) (2025-10-26) + + +### Bug Fixes + +* **YouTube - Force original audio:** Fall back to visionOS and not Android Studio if Android VR is not available ([6d01863](https://github.com/ReVanced/revanced-patches/commit/6d01863ec70617d9abc864ce6686ed9764dd151d)) +* **YouTube Music - Hide category bar:** Correctly hide the category bar in newer app targets ([#6175](https://github.com/ReVanced/revanced-patches/issues/6175)) ([13cf172](https://github.com/ReVanced/revanced-patches/commit/13cf1724bf2f946c7129cab0db96721c90f9fe89)) + +# [5.45.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.44.0...v5.45.0-dev.1) (2025-10-26) + + +### Bug Fixes + +* **Instagram:** Update failing fingerprints on newer versions ([#6181](https://github.com/ReVanced/revanced-patches/issues/6181)) ([c73a03c](https://github.com/ReVanced/revanced-patches/commit/c73a03c9e18a12262939c974cdf16221221d1487)) + + +### Features + +* **TikTok:** Add `Sanitize sharing links` patch ([#6176](https://github.com/ReVanced/revanced-patches/issues/6176)) ([ef44eaa](https://github.com/ReVanced/revanced-patches/commit/ef44eaa119b9d6c5faec051e22d20f883d0da4f1)) + +# [5.44.0](https://github.com/ReVanced/revanced-patches/compare/v5.43.1...v5.44.0) (2025-10-24) + + +### Bug Fixes + +* **Google Photos - Spoof features:** Add support for Pixel 10 devices ([#6161](https://github.com/ReVanced/revanced-patches/issues/6161)) ([754b719](https://github.com/ReVanced/revanced-patches/commit/754b71959a0155413eb33cf1bdc2c8976eaca634)) +* **X / Twitter - Change link sharing domain:** Use bytecode patching to resolve patching with Manager ([#6125](https://github.com/ReVanced/revanced-patches/issues/6125)) ([0af8c8a](https://github.com/ReVanced/revanced-patches/commit/0af8c8a766ae4ba6926404d59da2f14d649f91f7)) +* **YouTube - Hide layout components:** Hide new kind of community post ([#6146](https://github.com/ReVanced/revanced-patches/issues/6146)) ([cfd244b](https://github.com/ReVanced/revanced-patches/commit/cfd244b4088daacd2788ec38357ac521e4b296d5)) +* **YouTube Music:** Resolve patching 7.29 target ([2e4c6fd](https://github.com/ReVanced/revanced-patches/commit/2e4c6fdcadeef45a80733e374421d52e5e8af910)) + + +### Features + +* Add `Custom network security` patch ([#6151](https://github.com/ReVanced/revanced-patches/issues/6151)) ([e7336d2](https://github.com/ReVanced/revanced-patches/commit/e7336d2ef361cc5d6fe6e8442b36d9cf1f542931)) +* **Duolingo - Enable debug menu:** Support latest app target ([#6163](https://github.com/ReVanced/revanced-patches/issues/6163)) ([08baa19](https://github.com/ReVanced/revanced-patches/commit/08baa19b4a62e62bd103d177c3f4454de199cf16)) +* **Duolingo:** Add `Skip energy recharge ads` patch ([#6167](https://github.com/ReVanced/revanced-patches/issues/6167)) ([591e106](https://github.com/ReVanced/revanced-patches/commit/591e106098c6eff431b8b7ac7d985ce7373d701e)) +* **Samsung Radio:** Add `Disable device checks` patch ([#6145](https://github.com/ReVanced/revanced-patches/issues/6145)) ([de97562](https://github.com/ReVanced/revanced-patches/commit/de97562c5ddc8ec707761c1e04e74c4e18f9c158)) + +# [5.44.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.44.0-dev.3...v5.44.0-dev.4) (2025-10-24) + + +### Features + +* Add `Custom network security` patch ([#6151](https://github.com/ReVanced/revanced-patches/issues/6151)) ([e7336d2](https://github.com/ReVanced/revanced-patches/commit/e7336d2ef361cc5d6fe6e8442b36d9cf1f542931)) +* **Duolingo:** Add `Skip energy recharge ads` patch ([#6167](https://github.com/ReVanced/revanced-patches/issues/6167)) ([591e106](https://github.com/ReVanced/revanced-patches/commit/591e106098c6eff431b8b7ac7d985ce7373d701e)) + +# [5.44.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.44.0-dev.2...v5.44.0-dev.3) (2025-10-22) + + +### Features + +* **Duolingo - Enable debug menu:** Support latest app target ([#6163](https://github.com/ReVanced/revanced-patches/issues/6163)) ([08baa19](https://github.com/ReVanced/revanced-patches/commit/08baa19b4a62e62bd103d177c3f4454de199cf16)) + +# [5.44.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.44.0-dev.1...v5.44.0-dev.2) (2025-10-22) + + +### Bug Fixes + +* **Google Photos - Spoof features:** Add support for Pixel 10 devices ([#6161](https://github.com/ReVanced/revanced-patches/issues/6161)) ([754b719](https://github.com/ReVanced/revanced-patches/commit/754b71959a0155413eb33cf1bdc2c8976eaca634)) + +# [5.44.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.2-dev.3...v5.44.0-dev.1) (2025-10-22) + + +### Features + +* **Samsung Radio:** Add `Disable device checks` patch ([#6145](https://github.com/ReVanced/revanced-patches/issues/6145)) ([de97562](https://github.com/ReVanced/revanced-patches/commit/de97562c5ddc8ec707761c1e04e74c4e18f9c158)) + +## [5.43.2-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.43.2-dev.2...v5.43.2-dev.3) (2025-10-19) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new kind of community post ([#6146](https://github.com/ReVanced/revanced-patches/issues/6146)) ([cfd244b](https://github.com/ReVanced/revanced-patches/commit/cfd244b4088daacd2788ec38357ac521e4b296d5)) + +## [5.43.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.43.2-dev.1...v5.43.2-dev.2) (2025-10-17) + + +### Bug Fixes + +* **YouTube Music:** Resolve patching 7.29 target ([2e4c6fd](https://github.com/ReVanced/revanced-patches/commit/2e4c6fdcadeef45a80733e374421d52e5e8af910)) + +## [5.43.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.1...v5.43.2-dev.1) (2025-10-16) + + +### Bug Fixes + +* **X / Twitter - Change link sharing domain:** Use bytecode patching to resolve patching with Manager ([#6125](https://github.com/ReVanced/revanced-patches/issues/6125)) ([0af8c8a](https://github.com/ReVanced/revanced-patches/commit/0af8c8a766ae4ba6926404d59da2f14d649f91f7)) + +## [5.43.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.0...v5.43.1) (2025-10-15) + + +### Bug Fixes + +* **X / Twitter - Change link sharing domain:** Resolve duplicate patch option ([#6119](https://github.com/ReVanced/revanced-patches/issues/6119)) ([7563990](https://github.com/ReVanced/revanced-patches/commit/75639907502382f63fa127a886362d4a4573e6e3)) +* **X / Twitter:** Do not crash Manager when clicking on domain patch option ([2a1e318](https://github.com/ReVanced/revanced-patches/commit/2a1e31860f22f537d51b40a5b71d9ad9d538789e)) + +## [5.43.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.43.1-dev.1...v5.43.1-dev.2) (2025-10-14) + + +### Bug Fixes + +* **X / Twitter:** Do not crash Manager when clicking on domain patch option ([2a1e318](https://github.com/ReVanced/revanced-patches/commit/2a1e31860f22f537d51b40a5b71d9ad9d538789e)) + +## [5.43.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.43.0...v5.43.1-dev.1) (2025-10-14) + + +### Bug Fixes + +* **X / Twitter - Change link sharing domain:** Resolve duplicate patch option ([#6119](https://github.com/ReVanced/revanced-patches/issues/6119)) ([7563990](https://github.com/ReVanced/revanced-patches/commit/75639907502382f63fa127a886362d4a4573e6e3)) + +# [5.43.0](https://github.com/ReVanced/revanced-patches/compare/v5.42.1...v5.43.0) (2025-10-14) + + +### Bug Fixes + +* **Custom branding:** Use white notification icon for expanded status bar panel ([95eee59](https://github.com/ReVanced/revanced-patches/commit/95eee59a87a680e212a3ba06e1afefee8d91ee9d)) +* **Instagram - Change sharing domain:** Display patch option ([#6089](https://github.com/ReVanced/revanced-patches/issues/6089)) ([be2b144](https://github.com/ReVanced/revanced-patches/commit/be2b144cc9c4108ec37e16f3dd20573d88ffaa2b)) +* **X / Twitter - Change Link Sharing Domain:** Change link domain of share copy action ([#6091](https://github.com/ReVanced/revanced-patches/issues/6091)) ([5484625](https://github.com/ReVanced/revanced-patches/commit/54846253d748f4e7e30b2bba427c7d2fb9c341e2)) +* **YouTube - Custom branding:** Do not add a broken custom icon if the user provides an invalid custom icon path ([6555f6e](https://github.com/ReVanced/revanced-patches/commit/6555f6e6f8b52c2f1ddab1f52c6704cd2d8cfc12)) +* **YouTube - Custom branding:** Use ReVanced icon for status bar notification icon ([#6108](https://github.com/ReVanced/revanced-patches/issues/6108)) ([10ea250](https://github.com/ReVanced/revanced-patches/commit/10ea250d4a91f8ab3b7f865612a403fc93a857b5)) +* **YouTube - Force original audio:** Do not use translated audio if stream spoofing is off and force audio is on ([0c19dba](https://github.com/ReVanced/revanced-patches/commit/0c19dbaf30bcb95a29448d98b028ebeea54cc7d3)) + + +### Features + +* **Instagram:** Add `Hide suggested content` patch ([#6075](https://github.com/ReVanced/revanced-patches/issues/6075)) ([50f0b9c](https://github.com/ReVanced/revanced-patches/commit/50f0b9c5eee95ff5f9974e344802e1d2a4aab47b)) + +# [5.43.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.43.0-dev.3...v5.43.0-dev.4) (2025-10-14) + + +### Bug Fixes + +* **YouTube - Force original audio:** Do not use translated audio if stream spoofing is off and force audio is on ([0c19dba](https://github.com/ReVanced/revanced-patches/commit/0c19dbaf30bcb95a29448d98b028ebeea54cc7d3)) + +# [5.43.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.43.0-dev.2...v5.43.0-dev.3) (2025-10-14) + + +### Bug Fixes + +* **Custom branding:** Use white notification icon for expanded status bar panel ([95eee59](https://github.com/ReVanced/revanced-patches/commit/95eee59a87a680e212a3ba06e1afefee8d91ee9d)) + +# [5.43.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.43.0-dev.1...v5.43.0-dev.2) (2025-10-14) + + +### Bug Fixes + +* **YouTube - Custom branding:** Use ReVanced icon for status bar notification icon ([#6108](https://github.com/ReVanced/revanced-patches/issues/6108)) ([10ea250](https://github.com/ReVanced/revanced-patches/commit/10ea250d4a91f8ab3b7f865612a403fc93a857b5)) + +# [5.43.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.2-dev.3...v5.43.0-dev.1) (2025-10-11) + + +### Features + +* **Instagram:** Add `Hide suggested content` patch ([#6075](https://github.com/ReVanced/revanced-patches/issues/6075)) ([50f0b9c](https://github.com/ReVanced/revanced-patches/commit/50f0b9c5eee95ff5f9974e344802e1d2a4aab47b)) + +## [5.42.2-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.42.2-dev.2...v5.42.2-dev.3) (2025-10-11) + + +### Bug Fixes + +* **YouTube - Custom branding:** Do not add a broken custom icon if the user provides an invalid custom icon path ([6555f6e](https://github.com/ReVanced/revanced-patches/commit/6555f6e6f8b52c2f1ddab1f52c6704cd2d8cfc12)) + +## [5.42.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.42.2-dev.1...v5.42.2-dev.2) (2025-10-10) + + +### Bug Fixes + +* **X / Twitter - Change Link Sharing Domain:** Change link domain of share copy action ([#6091](https://github.com/ReVanced/revanced-patches/issues/6091)) ([5484625](https://github.com/ReVanced/revanced-patches/commit/54846253d748f4e7e30b2bba427c7d2fb9c341e2)) + +## [5.42.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.1...v5.42.2-dev.1) (2025-10-09) + + +### Bug Fixes + +* **Instagram - Change sharing domain:** Display patch option ([#6089](https://github.com/ReVanced/revanced-patches/issues/6089)) ([be2b144](https://github.com/ReVanced/revanced-patches/commit/be2b144cc9c4108ec37e16f3dd20573d88ffaa2b)) + +## [5.42.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.0...v5.42.1) (2025-10-08) + + +### Bug Fixes + +* **YouTube - Custom Branding:** Resolve startup crash with root installation ([fd4b2e1](https://github.com/ReVanced/revanced-patches/commit/fd4b2e1bb98c6e507178e5b46b896ef7d320bc3d)) + +## [5.42.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.42.0...v5.42.1-dev.1) (2025-10-08) + + +### Bug Fixes + +* **YouTube - Custom Branding:** Resolve startup crash with root installation ([fd4b2e1](https://github.com/ReVanced/revanced-patches/commit/fd4b2e1bb98c6e507178e5b46b896ef7d320bc3d)) + +# [5.42.0](https://github.com/ReVanced/revanced-patches/compare/v5.41.0...v5.42.0) (2025-10-08) + + +### Bug Fixes + +* **Custom branding:** Update ReVanced logo ([#6049](https://github.com/ReVanced/revanced-patches/issues/6049)) ([9441e7a](https://github.com/ReVanced/revanced-patches/commit/9441e7acb4817e12d1443d438ef6c448518bd614)) +* **Custom branding:** Update ReVanced logo sizing ([#6029](https://github.com/ReVanced/revanced-patches/issues/6029)) ([ae4b947](https://github.com/ReVanced/revanced-patches/commit/ae4b9474d3fb62528fc21397c19954d31605e9da)) +* **Instagram - Hide navigation buttons:** Resolve app startup crash ([080a226](https://github.com/ReVanced/revanced-patches/commit/080a2266146798be71789c939deef2f289697523)) +* **Spotify:** Change `Hide Create button` patch to default off ([#6067](https://github.com/ReVanced/revanced-patches/issues/6067)) ([19949e1](https://github.com/ReVanced/revanced-patches/commit/19949e1695cc252ff0f94a33b6e3fb62e967d7fd)) +* **X / Twitter:** Remove non functional and obsolete patch `Open links with app chooser` ([#6033](https://github.com/ReVanced/revanced-patches/issues/6033)) ([673609c](https://github.com/ReVanced/revanced-patches/commit/673609c2aa87988cdc138eab101b9750fe6a7b62)) +* **YouTube - Force original audio:** Change patch to default on ([#6070](https://github.com/ReVanced/revanced-patches/issues/6070)) ([bd4ba2d](https://github.com/ReVanced/revanced-patches/commit/bd4ba2dae85ee6fd8d7e6078c3de775ca336e0b6)) +* **YouTube - Force original language:** Resolve some videos using Swedish audio track ([9d67316](https://github.com/ReVanced/revanced-patches/commit/9d6731660ba0e19b863d05d54aa04f74a879f69b)) +* **YouTube - Hide end screen cards:** Hide new type of end screen card ([#6027](https://github.com/ReVanced/revanced-patches/issues/6027)) ([76b0364](https://github.com/ReVanced/revanced-patches/commit/76b0364c5b5562c6a0d178d2bbe5b220f48aaca9)) +* **YouTube - Spoof video streams:** Add "Allow Android VR AV1" setting ([#6071](https://github.com/ReVanced/revanced-patches/issues/6071)) ([f03256c](https://github.com/ReVanced/revanced-patches/commit/f03256c471e1ee6a12267c1b56b531ca8f89278c)) +* **YouTube - Spoof video streams:** Do not allow VR AV1 if "Force AVC" is enabled ([7afeaeb](https://github.com/ReVanced/revanced-patches/commit/7afeaebb5cc22eb4f4512d8aa0cf4e835e7a2daf)) +* **YouTube - Spoof video streams:** Resolve playback dropping frames ([#6051](https://github.com/ReVanced/revanced-patches/issues/6051)) ([a62ee43](https://github.com/ReVanced/revanced-patches/commit/a62ee43441b197f5c8352ae373bb8919ad66f0bd)) +* **YouTube Music - GmsCore support:** Handle sharing links to certain apps such as Instagram ([#6026](https://github.com/ReVanced/revanced-patches/issues/6026)) ([328234f](https://github.com/ReVanced/revanced-patches/commit/328234f39ada81542e596f04e8ce410c787c15c8)) +* **YouTube Music - Hide cast button:** Fix patching error ([28799a5](https://github.com/ReVanced/revanced-patches/commit/28799a548a73651134ef304cb6cb542cf8e55abe)) +* **YouTube Music - Hide cast button:** Resolve button not hiding ([7817885](https://github.com/ReVanced/revanced-patches/commit/7817885cffed66608039ab45881537cbd3069c9d)) +* **YouTube:** Resolve UI components not hiding for some users ([#6054](https://github.com/ReVanced/revanced-patches/issues/6054)) ([6b26346](https://github.com/ReVanced/revanced-patches/commit/6b2634691423f5ce25a28b3f2fbc420977b81748)) + + +### Features + +* **Custom branding:** Add in-app settings to change icon and name ([#6059](https://github.com/ReVanced/revanced-patches/issues/6059)) ([a50f3b5](https://github.com/ReVanced/revanced-patches/commit/a50f3b5177808f07d84041c946caccb5a08ad387)) +* **Instagram:** Add `Custom share domain` patch ([#5998](https://github.com/ReVanced/revanced-patches/issues/5998)) ([20c4131](https://github.com/ReVanced/revanced-patches/commit/20c413120bad97af6121718e76b22a1b5540aa44)) +* **Instagram:** Add `Enable developer menu` patch ([#6043](https://github.com/ReVanced/revanced-patches/issues/6043)) ([2154d89](https://github.com/ReVanced/revanced-patches/commit/2154d89242fd8d7f7460145d5d35a4f1986944a3)) +* **Instagram:** Add `Open links externally` patch ([#6012](https://github.com/ReVanced/revanced-patches/issues/6012)) ([08e8ead](https://github.com/ReVanced/revanced-patches/commit/08e8ead04ffff47a4608a3db7aadc8d5feccd4ad)) +* **Instagram:** Add `Sanitize sharing links` patch ([#5986](https://github.com/ReVanced/revanced-patches/issues/5986)) ([963a4ef](https://github.com/ReVanced/revanced-patches/commit/963a4ef43fd513de7a2d7d019992f06b62fdcc10)) +* **Viber:** Add `Hide navigation buttons` patch ([#5991](https://github.com/ReVanced/revanced-patches/issues/5991)) ([5cb46c4](https://github.com/ReVanced/revanced-patches/commit/5cb46c4e9180ebc16eddb983dad73d137d8ec047)) +* **YouTube Music:** Add `Custom branding` patch ([#6007](https://github.com/ReVanced/revanced-patches/issues/6007)) ([4c8b56f](https://github.com/ReVanced/revanced-patches/commit/4c8b56f5466b244737f501654eb7c5d34b6b2f88)) +* **YouTube Music:** Add `Force original audio` patch ([#6036](https://github.com/ReVanced/revanced-patches/issues/6036)) ([d0d53d1](https://github.com/ReVanced/revanced-patches/commit/d0d53d109e451759a029326873adfa36fba12b23)) + +# [5.42.0](https://github.com/ReVanced/revanced-patches/compare/v5.41.0...v5.42.0) (2025-10-08) + + +### Bug Fixes + +* **Custom branding:** Update ReVanced logo ([#6049](https://github.com/ReVanced/revanced-patches/issues/6049)) ([9441e7a](https://github.com/ReVanced/revanced-patches/commit/9441e7acb4817e12d1443d438ef6c448518bd614)) +* **Custom branding:** Update ReVanced logo sizing ([#6029](https://github.com/ReVanced/revanced-patches/issues/6029)) ([ae4b947](https://github.com/ReVanced/revanced-patches/commit/ae4b9474d3fb62528fc21397c19954d31605e9da)) +* **Instagram - Hide navigation buttons:** Resolve app startup crash ([080a226](https://github.com/ReVanced/revanced-patches/commit/080a2266146798be71789c939deef2f289697523)) +* **Spotify:** Change `Hide Create button` patch to default off ([#6067](https://github.com/ReVanced/revanced-patches/issues/6067)) ([19949e1](https://github.com/ReVanced/revanced-patches/commit/19949e1695cc252ff0f94a33b6e3fb62e967d7fd)) +* **X / Twitter:** Remove non functional and obsolete patch `Open links with app chooser` ([#6033](https://github.com/ReVanced/revanced-patches/issues/6033)) ([673609c](https://github.com/ReVanced/revanced-patches/commit/673609c2aa87988cdc138eab101b9750fe6a7b62)) +* **YouTube - Force original audio:** Change patch to default on ([#6070](https://github.com/ReVanced/revanced-patches/issues/6070)) ([bd4ba2d](https://github.com/ReVanced/revanced-patches/commit/bd4ba2dae85ee6fd8d7e6078c3de775ca336e0b6)) +* **YouTube - Force original language:** Resolve some videos using Swedish audio track ([9d67316](https://github.com/ReVanced/revanced-patches/commit/9d6731660ba0e19b863d05d54aa04f74a879f69b)) +* **YouTube - Hide end screen cards:** Hide new type of end screen card ([#6027](https://github.com/ReVanced/revanced-patches/issues/6027)) ([76b0364](https://github.com/ReVanced/revanced-patches/commit/76b0364c5b5562c6a0d178d2bbe5b220f48aaca9)) +* **YouTube - Spoof video streams:** Add "Allow Android VR AV1" setting ([#6071](https://github.com/ReVanced/revanced-patches/issues/6071)) ([f03256c](https://github.com/ReVanced/revanced-patches/commit/f03256c471e1ee6a12267c1b56b531ca8f89278c)) +* **YouTube - Spoof video streams:** Do not allow VR AV1 if "Force AVC" is enabled ([7afeaeb](https://github.com/ReVanced/revanced-patches/commit/7afeaebb5cc22eb4f4512d8aa0cf4e835e7a2daf)) +* **YouTube - Spoof video streams:** Resolve playback dropping frames ([#6051](https://github.com/ReVanced/revanced-patches/issues/6051)) ([a62ee43](https://github.com/ReVanced/revanced-patches/commit/a62ee43441b197f5c8352ae373bb8919ad66f0bd)) +* **YouTube Music - GmsCore support:** Handle sharing links to certain apps such as Instagram ([#6026](https://github.com/ReVanced/revanced-patches/issues/6026)) ([328234f](https://github.com/ReVanced/revanced-patches/commit/328234f39ada81542e596f04e8ce410c787c15c8)) +* **YouTube Music - Hide cast button:** Fix patching error ([28799a5](https://github.com/ReVanced/revanced-patches/commit/28799a548a73651134ef304cb6cb542cf8e55abe)) +* **YouTube Music - Hide cast button:** Resolve button not hiding ([7817885](https://github.com/ReVanced/revanced-patches/commit/7817885cffed66608039ab45881537cbd3069c9d)) +* **YouTube:** Resolve UI components not hiding for some users ([#6054](https://github.com/ReVanced/revanced-patches/issues/6054)) ([6b26346](https://github.com/ReVanced/revanced-patches/commit/6b2634691423f5ce25a28b3f2fbc420977b81748)) + + +### Features + +* **Custom branding:** Add in-app settings to change icon and name ([#6059](https://github.com/ReVanced/revanced-patches/issues/6059)) ([a50f3b5](https://github.com/ReVanced/revanced-patches/commit/a50f3b5177808f07d84041c946caccb5a08ad387)) +* **Instagram:** Add `Custom share domain` patch ([#5998](https://github.com/ReVanced/revanced-patches/issues/5998)) ([20c4131](https://github.com/ReVanced/revanced-patches/commit/20c413120bad97af6121718e76b22a1b5540aa44)) +* **Instagram:** Add `Enable developer menu` patch ([#6043](https://github.com/ReVanced/revanced-patches/issues/6043)) ([2154d89](https://github.com/ReVanced/revanced-patches/commit/2154d89242fd8d7f7460145d5d35a4f1986944a3)) +* **Instagram:** Add `Open links externally` patch ([#6012](https://github.com/ReVanced/revanced-patches/issues/6012)) ([08e8ead](https://github.com/ReVanced/revanced-patches/commit/08e8ead04ffff47a4608a3db7aadc8d5feccd4ad)) +* **Instagram:** Add `Sanitize sharing links` patch ([#5986](https://github.com/ReVanced/revanced-patches/issues/5986)) ([963a4ef](https://github.com/ReVanced/revanced-patches/commit/963a4ef43fd513de7a2d7d019992f06b62fdcc10)) +* **Viber:** Add `Hide navigation buttons` patch ([#5991](https://github.com/ReVanced/revanced-patches/issues/5991)) ([5cb46c4](https://github.com/ReVanced/revanced-patches/commit/5cb46c4e9180ebc16eddb983dad73d137d8ec047)) +* **YouTube Music:** Add `Custom branding` patch ([#6007](https://github.com/ReVanced/revanced-patches/issues/6007)) ([4c8b56f](https://github.com/ReVanced/revanced-patches/commit/4c8b56f5466b244737f501654eb7c5d34b6b2f88)) +* **YouTube Music:** Add `Force original audio` patch ([#6036](https://github.com/ReVanced/revanced-patches/issues/6036)) ([d0d53d1](https://github.com/ReVanced/revanced-patches/commit/d0d53d109e451759a029326873adfa36fba12b23)) + +# [5.42.0-dev.19](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.18...v5.42.0-dev.19) (2025-10-07) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Do not allow VR AV1 if "Force AVC" is enabled ([7afeaeb](https://github.com/ReVanced/revanced-patches/commit/7afeaebb5cc22eb4f4512d8aa0cf4e835e7a2daf)) + +# [5.42.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.17...v5.42.0-dev.18) (2025-10-07) + + +### Features + +* **Custom branding:** Add in-app settings to change icon and name ([#6059](https://github.com/ReVanced/revanced-patches/issues/6059)) ([a50f3b5](https://github.com/ReVanced/revanced-patches/commit/a50f3b5177808f07d84041c946caccb5a08ad387)) + +# [5.42.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.16...v5.42.0-dev.17) (2025-10-07) + + +### Bug Fixes + +* **YouTube - Force original audio:** Change patch to default on ([#6070](https://github.com/ReVanced/revanced-patches/issues/6070)) ([bd4ba2d](https://github.com/ReVanced/revanced-patches/commit/bd4ba2dae85ee6fd8d7e6078c3de775ca336e0b6)) + +# [5.42.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.15...v5.42.0-dev.16) (2025-10-07) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add "Allow Android VR AV1" setting ([#6071](https://github.com/ReVanced/revanced-patches/issues/6071)) ([f03256c](https://github.com/ReVanced/revanced-patches/commit/f03256c471e1ee6a12267c1b56b531ca8f89278c)) + +# [5.42.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.14...v5.42.0-dev.15) (2025-10-07) + + +### Features + +* **Instagram:** Add `Enable developer menu` patch ([#6043](https://github.com/ReVanced/revanced-patches/issues/6043)) ([2154d89](https://github.com/ReVanced/revanced-patches/commit/2154d89242fd8d7f7460145d5d35a4f1986944a3)) + +# [5.42.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.13...v5.42.0-dev.14) (2025-10-07) + + +### Features + +* **Instagram:** Add `Custom share domain` patch ([#5998](https://github.com/ReVanced/revanced-patches/issues/5998)) ([20c4131](https://github.com/ReVanced/revanced-patches/commit/20c413120bad97af6121718e76b22a1b5540aa44)) + +# [5.42.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.12...v5.42.0-dev.13) (2025-10-07) + + +### Bug Fixes + +* **Spotify:** Change `Hide Create button` patch to default off ([#6067](https://github.com/ReVanced/revanced-patches/issues/6067)) ([19949e1](https://github.com/ReVanced/revanced-patches/commit/19949e1695cc252ff0f94a33b6e3fb62e967d7fd)) + +# [5.42.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.11...v5.42.0-dev.12) (2025-10-03) + + +### Bug Fixes + +* **Custom branding:** Update ReVanced logo ([#6049](https://github.com/ReVanced/revanced-patches/issues/6049)) ([9441e7a](https://github.com/ReVanced/revanced-patches/commit/9441e7acb4817e12d1443d438ef6c448518bd614)) + + +### Features + +* **Instagram:** Add `Sanitize sharing links` patch ([#5986](https://github.com/ReVanced/revanced-patches/issues/5986)) ([963a4ef](https://github.com/ReVanced/revanced-patches/commit/963a4ef43fd513de7a2d7d019992f06b62fdcc10)) + +# [5.42.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.10...v5.42.0-dev.11) (2025-10-03) + + +### Bug Fixes + +* **YouTube:** Resolve UI components not hiding for some users ([#6054](https://github.com/ReVanced/revanced-patches/issues/6054)) ([6b26346](https://github.com/ReVanced/revanced-patches/commit/6b2634691423f5ce25a28b3f2fbc420977b81748)) + +# [5.42.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.9...v5.42.0-dev.10) (2025-10-02) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Resolve playback dropping frames ([#6051](https://github.com/ReVanced/revanced-patches/issues/6051)) ([a62ee43](https://github.com/ReVanced/revanced-patches/commit/a62ee43441b197f5c8352ae373bb8919ad66f0bd)) + +# [5.42.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.8...v5.42.0-dev.9) (2025-10-01) + + +### Bug Fixes + +* **Custom branding:** Update ReVanced logo sizing ([#6029](https://github.com/ReVanced/revanced-patches/issues/6029)) ([ae4b947](https://github.com/ReVanced/revanced-patches/commit/ae4b9474d3fb62528fc21397c19954d31605e9da)) + +# [5.42.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.7...v5.42.0-dev.8) (2025-10-01) + + +### Bug Fixes + +* **YouTube - Force original language:** Resolve some videos using Swedish audio track ([9d67316](https://github.com/ReVanced/revanced-patches/commit/9d6731660ba0e19b863d05d54aa04f74a879f69b)) + + +### Features + +* **YouTube Music:** Add `Force original audio` patch ([#6036](https://github.com/ReVanced/revanced-patches/issues/6036)) ([d0d53d1](https://github.com/ReVanced/revanced-patches/commit/d0d53d109e451759a029326873adfa36fba12b23)) + +# [5.42.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.6...v5.42.0-dev.7) (2025-10-01) + + +### Features + +* **Instagram:** Add `Open links externally` patch ([#6012](https://github.com/ReVanced/revanced-patches/issues/6012)) ([08e8ead](https://github.com/ReVanced/revanced-patches/commit/08e8ead04ffff47a4608a3db7aadc8d5feccd4ad)) + +# [5.42.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.5...v5.42.0-dev.6) (2025-09-30) + + +### Bug Fixes + +* **X / Twitter:** Remove non functional and obsolete patch `Open links with app chooser` ([#6033](https://github.com/ReVanced/revanced-patches/issues/6033)) ([673609c](https://github.com/ReVanced/revanced-patches/commit/673609c2aa87988cdc138eab101b9750fe6a7b62)) + +# [5.42.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.4...v5.42.0-dev.5) (2025-09-28) + + +### Features + +* **YouTube Music:** Add `Custom branding` patch ([#6007](https://github.com/ReVanced/revanced-patches/issues/6007)) ([4c8b56f](https://github.com/ReVanced/revanced-patches/commit/4c8b56f5466b244737f501654eb7c5d34b6b2f88)) + +# [5.42.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.3...v5.42.0-dev.4) (2025-09-28) + + +### Bug Fixes + +* **YouTube Music - GmsCore support:** Handle sharing links to certain apps such as Instagram ([#6026](https://github.com/ReVanced/revanced-patches/issues/6026)) ([328234f](https://github.com/ReVanced/revanced-patches/commit/328234f39ada81542e596f04e8ce410c787c15c8)) + +# [5.42.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.2...v5.42.0-dev.3) (2025-09-28) + + +### Bug Fixes + +* **YouTube - Hide end screen cards:** Hide new type of end screen card ([#6027](https://github.com/ReVanced/revanced-patches/issues/6027)) ([76b0364](https://github.com/ReVanced/revanced-patches/commit/76b0364c5b5562c6a0d178d2bbe5b220f48aaca9)) + +# [5.42.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.42.0-dev.1...v5.42.0-dev.2) (2025-09-27) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Resolve app startup crash ([080a226](https://github.com/ReVanced/revanced-patches/commit/080a2266146798be71789c939deef2f289697523)) + +# [5.42.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.41.1-dev.2...v5.42.0-dev.1) (2025-09-27) + + +### Features + +* **Viber:** Add `Hide navigation buttons` patch ([#5991](https://github.com/ReVanced/revanced-patches/issues/5991)) ([5cb46c4](https://github.com/ReVanced/revanced-patches/commit/5cb46c4e9180ebc16eddb983dad73d137d8ec047)) + +## [5.41.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.41.1-dev.1...v5.41.1-dev.2) (2025-09-27) + + +### Bug Fixes + +* **YouTube Music - Hide cast button:** Fix patching error ([28799a5](https://github.com/ReVanced/revanced-patches/commit/28799a548a73651134ef304cb6cb542cf8e55abe)) + +## [5.41.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.41.0...v5.41.1-dev.1) (2025-09-27) + + +### Bug Fixes + +* **YouTube Music - Hide cast button:** Resolve button not hiding ([7817885](https://github.com/ReVanced/revanced-patches/commit/7817885cffed66608039ab45881537cbd3069c9d)) + +# [5.41.0](https://github.com/ReVanced/revanced-patches/compare/v5.40.0...v5.41.0) (2025-09-27) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Remove button based on name ([#5971](https://github.com/ReVanced/revanced-patches/issues/5971)) ([6fa4043](https://github.com/ReVanced/revanced-patches/commit/6fa404331b5162682d83fba5f38ed570c31495fc)) +* **Instagram - Limit feed to followed profiles:** Preserve favorites feed ([#5963](https://github.com/ReVanced/revanced-patches/issues/5963)) ([ef51401](https://github.com/ReVanced/revanced-patches/commit/ef514017f46025d9aef6884424caeb0670514e7a)) +* **TikTok:** Show correct dialog restart text, use correct font color for non-dark mode ([d1a1293](https://github.com/ReVanced/revanced-patches/commit/d1a12930c35f630793a0f240d4203c2ff9060158)) +* **Twitch - Settings:** Fix missing style resources ([#5970](https://github.com/ReVanced/revanced-patches/issues/5970)) ([8c22995](https://github.com/ReVanced/revanced-patches/commit/8c229954d7f232a7a472ca49f1b5e7cdc475bbcc)) +* **YouTube - Hide Shorts components:** Fix "Hide preview comment" ([#5990](https://github.com/ReVanced/revanced-patches/issues/5990)) ([dd4e2cd](https://github.com/ReVanced/revanced-patches/commit/dd4e2cd0855ccc51b94593004fdd8150ac3b41cc)) +* **YouTube - Return YouTube Dislike:** Do not show error toast if API returns 401 status ([#5949](https://github.com/ReVanced/revanced-patches/issues/5949)) ([58d088a](https://github.com/ReVanced/revanced-patches/commit/58d088ab307440a6912a867246da799b7dd6499b)) +* **YouTube - Settings:** Handle on screen back swipe gesture ([#6002](https://github.com/ReVanced/revanced-patches/issues/6002)) ([6f92b6c](https://github.com/ReVanced/revanced-patches/commit/6f92b6c50beab091f5f7ef7386579eda38cb4c66)) +* **YouTube - Settings:** Use an overlay to show search results ([#5806](https://github.com/ReVanced/revanced-patches/issues/5806)) ([ece8076](https://github.com/ReVanced/revanced-patches/commit/ece8076f7cefd752b97515014bc50fe4fd80171e)) +* **YouTube - SponsorBlock:** Show category color dot in voting dialog menu ([4be00d0](https://github.com/ReVanced/revanced-patches/commit/4be00d09b7b87dcfac324de8709af06e9f730791)) +* **YouTube - SponsorBlock:** Show category color in create new segment menu ([#5987](https://github.com/ReVanced/revanced-patches/issues/5987)) ([ffd933c](https://github.com/ReVanced/revanced-patches/commit/ffd933c6734274cdde5aaec0159b67f173f9228c)) +* **YouTube - Spoof video streams:** Update client side effects summary text ([a0a62dd](https://github.com/ReVanced/revanced-patches/commit/a0a62ddad26cfab3e04907fae5532e1ba1fdf710)) + + +### Features + +* **Tumblr:** Add `Disable Tumblr TV` patch ([#5959](https://github.com/ReVanced/revanced-patches/issues/5959)) ([212418b](https://github.com/ReVanced/revanced-patches/commit/212418b8db9a730ae9efa89ad2bef24952afbadd)) +* **YouTube - Hide layout components:** Add "Hide Emoji and Timestamp buttons" setting ([#5992](https://github.com/ReVanced/revanced-patches/issues/5992)) ([2b555f6](https://github.com/ReVanced/revanced-patches/commit/2b555f67f07e0de5703c630888ce2fbba3145192)) +* **YouTube - Hide layout components:** Add "Hide view count" and "Hide upload time" settings ([#5983](https://github.com/ReVanced/revanced-patches/issues/5983)) ([7a37d85](https://github.com/ReVanced/revanced-patches/commit/7a37d858fb937c6bdc2219103dac765b62600e6c)) +* **YouTube - Loop video:** Add player button to change loop video state ([#5961](https://github.com/ReVanced/revanced-patches/issues/5961)) ([dfb5407](https://github.com/ReVanced/revanced-patches/commit/dfb5407e67222e80e23c8935e04b6dbf1a43d757)) +* **YouTube - Spoof app version:** Add spoof target `20.05.46` that fixes transcript functionality ([5823f0e](https://github.com/ReVanced/revanced-patches/commit/5823f0e982e87b4a35d30feeca8a7e16edfebc5f)) +* **YouTube Music:** Add `Check watch history domain name resolution` ([#5979](https://github.com/ReVanced/revanced-patches/issues/5979)) ([8af70fe](https://github.com/ReVanced/revanced-patches/commit/8af70fe2d10c0f4da2d7e53bd00f5b3979775d5d)) +* **YouTube Music:** Add `Sanitize sharing links` patch ([#5952](https://github.com/ReVanced/revanced-patches/issues/5952)) ([45c1ee8](https://github.com/ReVanced/revanced-patches/commit/45c1ee8a12dc777a371875d90741a05cf5d8e9dd)) +* **YouTube Music:** Add `Theme` patch ([#5984](https://github.com/ReVanced/revanced-patches/issues/5984)) ([3bd76d6](https://github.com/ReVanced/revanced-patches/commit/3bd76d60d664befff29c24c9de56dac1486a6e67)) +* **YouTube:** Add `Disable video codecs` patch ([#5981](https://github.com/ReVanced/revanced-patches/issues/5981)) ([bfbffbd](https://github.com/ReVanced/revanced-patches/commit/bfbffbd1f5aa867027053e25b343a51a606216a3)) + +# [5.41.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.17...v5.41.0-dev.18) (2025-09-26) + + +### Bug Fixes + +* **YouTube - Settings:** Handle on screen back swipe gesture ([#6002](https://github.com/ReVanced/revanced-patches/issues/6002)) ([6f92b6c](https://github.com/ReVanced/revanced-patches/commit/6f92b6c50beab091f5f7ef7386579eda38cb4c66)) + +# [5.41.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.16...v5.41.0-dev.17) (2025-09-26) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Show category color dot in voting dialog menu ([4be00d0](https://github.com/ReVanced/revanced-patches/commit/4be00d09b7b87dcfac324de8709af06e9f730791)) + +# [5.41.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.15...v5.41.0-dev.16) (2025-09-26) + + +### Features + +* **YouTube Music:** Add `Theme` patch ([#5984](https://github.com/ReVanced/revanced-patches/issues/5984)) ([3bd76d6](https://github.com/ReVanced/revanced-patches/commit/3bd76d60d664befff29c24c9de56dac1486a6e67)) + +# [5.41.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.14...v5.41.0-dev.15) (2025-09-25) + + +### Features + +* **YouTube - Hide layout components:** Add "Hide view count" and "Hide upload time" settings ([#5983](https://github.com/ReVanced/revanced-patches/issues/5983)) ([7a37d85](https://github.com/ReVanced/revanced-patches/commit/7a37d858fb937c6bdc2219103dac765b62600e6c)) + +# [5.41.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.13...v5.41.0-dev.14) (2025-09-24) + + +### Features + +* **YouTube - Hide layout components:** Add "Hide Emoji and Timestamp buttons" setting ([#5992](https://github.com/ReVanced/revanced-patches/issues/5992)) ([2b555f6](https://github.com/ReVanced/revanced-patches/commit/2b555f67f07e0de5703c630888ce2fbba3145192)) + +# [5.41.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.12...v5.41.0-dev.13) (2025-09-24) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Fix "Hide preview comment" ([#5990](https://github.com/ReVanced/revanced-patches/issues/5990)) ([dd4e2cd](https://github.com/ReVanced/revanced-patches/commit/dd4e2cd0855ccc51b94593004fdd8150ac3b41cc)) + +# [5.41.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.11...v5.41.0-dev.12) (2025-09-24) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Show category color in create new segment menu ([#5987](https://github.com/ReVanced/revanced-patches/issues/5987)) ([ffd933c](https://github.com/ReVanced/revanced-patches/commit/ffd933c6734274cdde5aaec0159b67f173f9228c)) + +# [5.41.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.10...v5.41.0-dev.11) (2025-09-23) + + +### Features + +* **YouTube:** Add `Disable video codecs` patch ([#5981](https://github.com/ReVanced/revanced-patches/issues/5981)) ([bfbffbd](https://github.com/ReVanced/revanced-patches/commit/bfbffbd1f5aa867027053e25b343a51a606216a3)) + +# [5.41.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.9...v5.41.0-dev.10) (2025-09-23) + + +### Bug Fixes + +* **TikTok:** Show correct dialog restart text, use correct font color for non-dark mode ([d1a1293](https://github.com/ReVanced/revanced-patches/commit/d1a12930c35f630793a0f240d4203c2ff9060158)) + +# [5.41.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.8...v5.41.0-dev.9) (2025-09-23) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Remove button based on name ([#5971](https://github.com/ReVanced/revanced-patches/issues/5971)) ([6fa4043](https://github.com/ReVanced/revanced-patches/commit/6fa404331b5162682d83fba5f38ed570c31495fc)) + +# [5.41.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.7...v5.41.0-dev.8) (2025-09-23) + + +### Features + +* **YouTube Music:** Add `Check watch history domain name resolution` ([#5979](https://github.com/ReVanced/revanced-patches/issues/5979)) ([8af70fe](https://github.com/ReVanced/revanced-patches/commit/8af70fe2d10c0f4da2d7e53bd00f5b3979775d5d)) + +# [5.41.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.6...v5.41.0-dev.7) (2025-09-23) + + +### Features + +* **Tumblr:** Add `Disable Tumblr TV` patch ([#5959](https://github.com/ReVanced/revanced-patches/issues/5959)) ([212418b](https://github.com/ReVanced/revanced-patches/commit/212418b8db9a730ae9efa89ad2bef24952afbadd)) + +# [5.41.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.5...v5.41.0-dev.6) (2025-09-22) + + +### Features + +* **YouTube - Spoof app version:** Add spoof target `20.05.46` that fixes transcript functionality ([5823f0e](https://github.com/ReVanced/revanced-patches/commit/5823f0e982e87b4a35d30feeca8a7e16edfebc5f)) + +# [5.41.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.4...v5.41.0-dev.5) (2025-09-22) + + +### Bug Fixes + +* **Twitch - Settings:** Fix missing style resources ([#5970](https://github.com/ReVanced/revanced-patches/issues/5970)) ([8c22995](https://github.com/ReVanced/revanced-patches/commit/8c229954d7f232a7a472ca49f1b5e7cdc475bbcc)) + +# [5.41.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.3...v5.41.0-dev.4) (2025-09-22) + + +### Bug Fixes + +* **Instagram - Limit feed to followed profiles:** Preserve favorites feed ([#5963](https://github.com/ReVanced/revanced-patches/issues/5963)) ([ef51401](https://github.com/ReVanced/revanced-patches/commit/ef514017f46025d9aef6884424caeb0670514e7a)) + +# [5.41.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.2...v5.41.0-dev.3) (2025-09-22) + + +### Features + +* **YouTube - Loop video:** Add player button to change loop video state ([#5961](https://github.com/ReVanced/revanced-patches/issues/5961)) ([dfb5407](https://github.com/ReVanced/revanced-patches/commit/dfb5407e67222e80e23c8935e04b6dbf1a43d757)) + +# [5.41.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.41.0-dev.1...v5.41.0-dev.2) (2025-09-21) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Update client side effects summary text ([a0a62dd](https://github.com/ReVanced/revanced-patches/commit/a0a62ddad26cfab3e04907fae5532e1ba1fdf710)) + +# [5.41.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.40.1-dev.1...v5.41.0-dev.1) (2025-09-21) + + +### Features + +* **YouTube Music:** Add `Sanitize sharing links` patch ([#5952](https://github.com/ReVanced/revanced-patches/issues/5952)) ([45c1ee8](https://github.com/ReVanced/revanced-patches/commit/45c1ee8a12dc777a371875d90741a05cf5d8e9dd)) + +## [5.40.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.40.0...v5.40.1-dev.1) (2025-09-21) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Do not show error toast if API returns 401 status ([#5949](https://github.com/ReVanced/revanced-patches/issues/5949)) ([58d088a](https://github.com/ReVanced/revanced-patches/commit/58d088ab307440a6912a867246da799b7dd6499b)) +* **YouTube - Settings:** Use an overlay to show search results ([#5806](https://github.com/ReVanced/revanced-patches/issues/5806)) ([ece8076](https://github.com/ReVanced/revanced-patches/commit/ece8076f7cefd752b97515014bc50fe4fd80171e)) + +# [5.40.0](https://github.com/ReVanced/revanced-patches/compare/v5.39.0...v5.40.0) (2025-09-21) + + +### Bug Fixes + +* **Instagram - Limit feed to followed profiles:** Change patch to default off ([767f1e3](https://github.com/ReVanced/revanced-patches/commit/767f1e3695327bdbc4daea8b50a80d4c0a38456a)) +* **Spoof video streams:** Resolve occasional playback stuttering ([5c7c8b5](https://github.com/ReVanced/revanced-patches/commit/5c7c8b536416ec53cd98f7d59d11850aa1b70f11)) +* **YouTube - Force original audio:** Show UI setting summary if spoofing to Android Studio ([b7026b7](https://github.com/ReVanced/revanced-patches/commit/b7026b70865bc44de07b30f84ba8b8b608930d5b)) +* **YouTube - Spoof video streams:** Add "Force original audio" disclaimer for Android Studio client ([f97d332](https://github.com/ReVanced/revanced-patches/commit/f97d33206b4c97244f0bd0c672c4b91eaf477b0b)) +* **YouTube - Spoof video streams:** Add stream audio selector disclaimer for Android Studio client ([a8a4107](https://github.com/ReVanced/revanced-patches/commit/a8a410708d50f34ac4bd2ca29bbbc3cde00bbf93)) + + +### Features + +* **Instagram:** Add `Limit feed to followed profiles` patch ([#5908](https://github.com/ReVanced/revanced-patches/issues/5908)) ([8ba9a19](https://github.com/ReVanced/revanced-patches/commit/8ba9a19ade24c5fe9bd6d4e49772b7663522780e)) +* **Viber - Hide ads:** Support latest app target ([#5863](https://github.com/ReVanced/revanced-patches/issues/5863)) ([e6cce85](https://github.com/ReVanced/revanced-patches/commit/e6cce8554116df3c0ea6dbb7440c59c9e73d8334)) +* **YouTube - Hide video action buttons:** Add "Hide comments" button ([db796fb](https://github.com/ReVanced/revanced-patches/commit/db796fb8830b813e1ed626d491c4a797171e69e7)) +* **YouTube Music:** Add `Enable debugging` patch ([#5939](https://github.com/ReVanced/revanced-patches/issues/5939)) ([418f594](https://github.com/ReVanced/revanced-patches/commit/418f5945c213313f9a77cac9a5c326d89c754dfd)) +* **YouTube Music:** Add `Hide cast button` and `Navigation bar` patches ([#5934](https://github.com/ReVanced/revanced-patches/issues/5934)) ([651d358](https://github.com/ReVanced/revanced-patches/commit/651d3580967a252b57cbf4afbba02d6a4601ccfe)) +* **YouTube Music:** Support version `8.10.52` ([#5941](https://github.com/ReVanced/revanced-patches/issues/5941)) ([01c0f1b](https://github.com/ReVanced/revanced-patches/commit/01c0f1bd1ac6edb8aea758f88ffffcdea74a29b7)) +* **YouTube:** Support version `20.14.43` ([#5940](https://github.com/ReVanced/revanced-patches/issues/5940)) ([f7f4a1b](https://github.com/ReVanced/revanced-patches/commit/f7f4a1b0f0186598266b41a2c6a781fdee49e440)) + +# [5.40.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.10...v5.40.0-dev.11) (2025-09-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add stream audio selector disclaimer for Android Studio client ([a8a4107](https://github.com/ReVanced/revanced-patches/commit/a8a410708d50f34ac4bd2ca29bbbc3cde00bbf93)) + +# [5.40.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.9...v5.40.0-dev.10) (2025-09-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add "Force original audio" disclaimer for Android Studio client ([f97d332](https://github.com/ReVanced/revanced-patches/commit/f97d33206b4c97244f0bd0c672c4b91eaf477b0b)) + +# [5.40.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.8...v5.40.0-dev.9) (2025-09-20) + + +### Features + +* **YouTube Music:** Support version `8.10.52` ([#5941](https://github.com/ReVanced/revanced-patches/issues/5941)) ([01c0f1b](https://github.com/ReVanced/revanced-patches/commit/01c0f1bd1ac6edb8aea758f88ffffcdea74a29b7)) + +# [5.40.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.7...v5.40.0-dev.8) (2025-09-20) + + +### Features + +* **YouTube:** Support version `20.14.43` ([#5940](https://github.com/ReVanced/revanced-patches/issues/5940)) ([f7f4a1b](https://github.com/ReVanced/revanced-patches/commit/f7f4a1b0f0186598266b41a2c6a781fdee49e440)) + +# [5.40.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.6...v5.40.0-dev.7) (2025-09-20) + + +### Features + +* **YouTube - Hide video action buttons:** Add "Hide comments" button ([db796fb](https://github.com/ReVanced/revanced-patches/commit/db796fb8830b813e1ed626d491c4a797171e69e7)) + +# [5.40.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.5...v5.40.0-dev.6) (2025-09-20) + + +### Features + +* **YouTube Music:** Add `Enable debugging` patch ([#5939](https://github.com/ReVanced/revanced-patches/issues/5939)) ([418f594](https://github.com/ReVanced/revanced-patches/commit/418f5945c213313f9a77cac9a5c326d89c754dfd)) + +# [5.40.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.4...v5.40.0-dev.5) (2025-09-20) + + +### Features + +* **YouTube Music:** Add `Hide cast button` and `Navigation bar` patches ([#5934](https://github.com/ReVanced/revanced-patches/issues/5934)) ([651d358](https://github.com/ReVanced/revanced-patches/commit/651d3580967a252b57cbf4afbba02d6a4601ccfe)) + +# [5.40.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.3...v5.40.0-dev.4) (2025-09-20) + + +### Bug Fixes + +* **Spoof video streams:** Resolve occasional playback stuttering ([5c7c8b5](https://github.com/ReVanced/revanced-patches/commit/5c7c8b536416ec53cd98f7d59d11850aa1b70f11)) + +# [5.40.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.2...v5.40.0-dev.3) (2025-09-19) + + +### Bug Fixes + +* **Instagram - Limit feed to followed profiles:** Change patch to default off ([767f1e3](https://github.com/ReVanced/revanced-patches/commit/767f1e3695327bdbc4daea8b50a80d4c0a38456a)) + +# [5.40.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.40.0-dev.1...v5.40.0-dev.2) (2025-09-18) + + +### Features + +* **Instagram:** Add `Limit feed to followed profiles` patch ([#5908](https://github.com/ReVanced/revanced-patches/issues/5908)) ([8ba9a19](https://github.com/ReVanced/revanced-patches/commit/8ba9a19ade24c5fe9bd6d4e49772b7663522780e)) + +# [5.40.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.39.1-dev.1...v5.40.0-dev.1) (2025-09-17) + + +### Features + +* **Viber - Hide ads:** Support latest app target ([#5863](https://github.com/ReVanced/revanced-patches/issues/5863)) ([e6cce85](https://github.com/ReVanced/revanced-patches/commit/e6cce8554116df3c0ea6dbb7440c59c9e73d8334)) + +## [5.39.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.39.0...v5.39.1-dev.1) (2025-09-17) + + +### Bug Fixes + +* **YouTube - Force original audio:** Show UI setting summary if spoofing to Android Studio ([b7026b7](https://github.com/ReVanced/revanced-patches/commit/b7026b70865bc44de07b30f84ba8b8b608930d5b)) + +# [5.39.0](https://github.com/ReVanced/revanced-patches/compare/v5.38.0...v5.39.0) (2025-09-17) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Do not use Android Creator for livestreams ([cbe576b](https://github.com/ReVanced/revanced-patches/commit/cbe576bc384ef5f5ee2fa341147925ed0dff568b)) +* **YouTube - Spoof video streams:** Show Android Studio in spoof stream menu ([c9f741e](https://github.com/ReVanced/revanced-patches/commit/c9f741e616c7acab0cd4558e02b0c4ec18392c10)) +* **YouTube Music - Spoof video streams:** Remove iPadOS client ([7eeffd3](https://github.com/ReVanced/revanced-patches/commit/7eeffd3392c57555342173103d3a417c038d0970)) + + +### Features + +* **YouTube - Hide video action buttons:** Add "Hide Shop button" setting ([a84db7b](https://github.com/ReVanced/revanced-patches/commit/a84db7be7fde2e9bb3ac41aec709a1681e845fe1)) + +# [5.39.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.39.0-dev.1...v5.39.0-dev.2) (2025-09-17) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Show Android Studio in spoof stream menu ([c9f741e](https://github.com/ReVanced/revanced-patches/commit/c9f741e616c7acab0cd4558e02b0c4ec18392c10)) + +# [5.39.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.38.1-dev.2...v5.39.0-dev.1) (2025-09-17) + + +### Features + +* **YouTube - Hide video action buttons:** Add "Hide Shop button" setting ([a84db7b](https://github.com/ReVanced/revanced-patches/commit/a84db7be7fde2e9bb3ac41aec709a1681e845fe1)) + +## [5.38.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.38.1-dev.1...v5.38.1-dev.2) (2025-09-16) + + +### Bug Fixes + +* **YouTube Music - Spoof video streams:** Remove iPadOS client ([7eeffd3](https://github.com/ReVanced/revanced-patches/commit/7eeffd3392c57555342173103d3a417c038d0970)) + +## [5.38.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.38.0...v5.38.1-dev.1) (2025-09-16) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Do not use Android Creator for livestreams ([cbe576b](https://github.com/ReVanced/revanced-patches/commit/cbe576bc384ef5f5ee2fa341147925ed0dff568b)) + +# [5.38.0](https://github.com/ReVanced/revanced-patches/compare/v5.37.0...v5.38.0) (2025-09-16) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Support v397.1.0.52.81 ([#5855](https://github.com/ReVanced/revanced-patches/issues/5855)) ([f11d1ef](https://github.com/ReVanced/revanced-patches/commit/f11d1ef9907082512f139d4ab0e2e9f707de7e48)) +* **Spoof video streams:** Remove Android TV and iOS TV clients, add experimental VisionOS, add temporary fix for `Force original audio` to work with any spoof client ([#5861](https://github.com/ReVanced/revanced-patches/issues/5861)) ([abe3943](https://github.com/ReVanced/revanced-patches/commit/abe3943f98fd86dcd74c7e07cf65d3c7fc24fef9)) +* **YouTube - Spoof video streams:** Show settings summary if `Force original audio` is enabled ([3776dda](https://github.com/ReVanced/revanced-patches/commit/3776dda710a7780717b7e6f2cdc1333ab67b92fc)) +* **YouTube Music - Spoof video streams:** Fix playback issues when using a cellular network ([fa04c8e](https://github.com/ReVanced/revanced-patches/commit/fa04c8eecfbdd0b6ed082b464ca9032536d71762)) +* **YouTube Music:** Use correct light/dark mode settings UI ([1475643](https://github.com/ReVanced/revanced-patches/commit/1475643f84e9ee4af2ba360a2274001ff1570dad)) + + +### Features + +* **Instagram:** Add `Hide explore feed` patch ([#5856](https://github.com/ReVanced/revanced-patches/issues/5856)) ([1d65887](https://github.com/ReVanced/revanced-patches/commit/1d65887e015a067196f5a84db486fff355c96596)) +* **YouTube - Spoof video streams:** Add iPadOS client ([2726231](https://github.com/ReVanced/revanced-patches/commit/2726231404384d87f101d825e10a17c944e8f1bd)) +* **YouTube Music:** Add `Settings` patch ([#5838](https://github.com/ReVanced/revanced-patches/issues/5838)) ([5e20bd8](https://github.com/ReVanced/revanced-patches/commit/5e20bd80f138d7ca94f18857194c46e489c435dc)) + +# [5.38.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.38.0-dev.4...v5.38.0-dev.5) (2025-09-16) + + +### Bug Fixes + +* **YouTube Music:** Use correct light/dark mode settings UI ([1475643](https://github.com/ReVanced/revanced-patches/commit/1475643f84e9ee4af2ba360a2274001ff1570dad)) + +# [5.38.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.38.0-dev.3...v5.38.0-dev.4) (2025-09-16) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Show settings summary if `Force original audio` is enabled ([3776dda](https://github.com/ReVanced/revanced-patches/commit/3776dda710a7780717b7e6f2cdc1333ab67b92fc)) + +# [5.38.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.38.0-dev.2...v5.38.0-dev.3) (2025-09-16) + + +### Features + +* **YouTube - Spoof video streams:** Add iPadOS client ([2726231](https://github.com/ReVanced/revanced-patches/commit/2726231404384d87f101d825e10a17c944e8f1bd)) + +# [5.38.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.38.0-dev.1...v5.38.0-dev.2) (2025-09-16) + + +### Features + +* **YouTube Music:** Add `Settings` patch ([#5838](https://github.com/ReVanced/revanced-patches/issues/5838)) ([5e20bd8](https://github.com/ReVanced/revanced-patches/commit/5e20bd80f138d7ca94f18857194c46e489c435dc)) + +# [5.38.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.37.1-dev.3...v5.38.0-dev.1) (2025-09-15) + + +### Features + +* **Instagram:** Add `Hide explore feed` patch ([#5856](https://github.com/ReVanced/revanced-patches/issues/5856)) ([1d65887](https://github.com/ReVanced/revanced-patches/commit/1d65887e015a067196f5a84db486fff355c96596)) + +## [5.37.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.37.1-dev.2...v5.37.1-dev.3) (2025-09-15) + + +### Bug Fixes + +* **Spoof video streams:** Remove Android TV and iOS TV clients, add experimental VisionOS, add temporary fix for `Force original audio` to work with any spoof client ([#5861](https://github.com/ReVanced/revanced-patches/issues/5861)) ([abe3943](https://github.com/ReVanced/revanced-patches/commit/abe3943f98fd86dcd74c7e07cf65d3c7fc24fef9)) + +## [5.37.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.37.1-dev.1...v5.37.1-dev.2) (2025-09-15) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Support v397.1.0.52.81 ([#5855](https://github.com/ReVanced/revanced-patches/issues/5855)) ([f11d1ef](https://github.com/ReVanced/revanced-patches/commit/f11d1ef9907082512f139d4ab0e2e9f707de7e48)) + +## [5.37.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.37.0...v5.37.1-dev.1) (2025-09-15) + + +### Bug Fixes + +* **YouTube Music - Spoof video streams:** Fix playback issues when using a cellular network ([fa04c8e](https://github.com/ReVanced/revanced-patches/commit/fa04c8eecfbdd0b6ed082b464ca9032536d71762)) + +# [5.37.0](https://github.com/ReVanced/revanced-patches/compare/v5.36.0...v5.37.0) (2025-09-15) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Add constrain to known working version ([e6c79f1](https://github.com/ReVanced/revanced-patches/commit/e6c79f13834c83fef04e4dee5e628cb0b9a27765)) +* Resolve patching with dev branch ([09b941a](https://github.com/ReVanced/revanced-patches/commit/09b941abf0e8029999565082b02a88b5de507ec4)) +* **Spotify:** Remove broken `Spoof client` patch ([#5833](https://github.com/ReVanced/revanced-patches/issues/5833)) ([dcd4245](https://github.com/ReVanced/revanced-patches/commit/dcd42454bd5f87dddd720534f6120c4ef90063a3)) +* **Viber - Hide ads:** Add constrain to known working version ([2db0948](https://github.com/ReVanced/revanced-patches/commit/2db0948beaf2b68391a1fe7f21e92d31c7df61e7)) +* **YouTube Music - Spoof streaming data:** Fix audio playback stuttering ([#5839](https://github.com/ReVanced/revanced-patches/issues/5839)) ([2a85a3b](https://github.com/ReVanced/revanced-patches/commit/2a85a3b29092729ae16d1fd93803634ce5f08e95)) + + +### Features + +* **Viber:** Add `Hide ads` patch ([#5826](https://github.com/ReVanced/revanced-patches/issues/5826)) ([0abfab7](https://github.com/ReVanced/revanced-patches/commit/0abfab79d7cda15bf17c53679fbfffb021662649)) + +# [5.37.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.37.0-dev.5...v5.37.0-dev.6) (2025-09-15) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Add constrain to known working version ([e6c79f1](https://github.com/ReVanced/revanced-patches/commit/e6c79f13834c83fef04e4dee5e628cb0b9a27765)) + +# [5.37.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.37.0-dev.4...v5.37.0-dev.5) (2025-09-15) + + +### Bug Fixes + +* **Viber - Hide ads:** Add constrain to known working version ([2db0948](https://github.com/ReVanced/revanced-patches/commit/2db0948beaf2b68391a1fe7f21e92d31c7df61e7)) + +# [5.37.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.37.0-dev.3...v5.37.0-dev.4) (2025-09-14) + + +### Bug Fixes + +* **YouTube Music - Spoof streaming data:** Fix audio playback stuttering ([#5839](https://github.com/ReVanced/revanced-patches/issues/5839)) ([2a85a3b](https://github.com/ReVanced/revanced-patches/commit/2a85a3b29092729ae16d1fd93803634ce5f08e95)) + +# [5.37.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.37.0-dev.2...v5.37.0-dev.3) (2025-09-14) + + +### Bug Fixes + +* **Spotify:** Remove broken `Spoof client` patch ([#5833](https://github.com/ReVanced/revanced-patches/issues/5833)) ([dcd4245](https://github.com/ReVanced/revanced-patches/commit/dcd42454bd5f87dddd720534f6120c4ef90063a3)) + +# [5.37.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.37.0-dev.1...v5.37.0-dev.2) (2025-09-14) + + +### Bug Fixes + +* Resolve patching with dev branch ([09b941a](https://github.com/ReVanced/revanced-patches/commit/09b941abf0e8029999565082b02a88b5de507ec4)) + +# [5.37.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.36.0...v5.37.0-dev.1) (2025-09-14) + + +### Features + +* **Viber:** Add `Hide ads` patch ([#5826](https://github.com/ReVanced/revanced-patches/issues/5826)) ([0abfab7](https://github.com/ReVanced/revanced-patches/commit/0abfab79d7cda15bf17c53679fbfffb021662649)) + +# [5.36.0](https://github.com/ReVanced/revanced-patches/compare/v5.35.0...v5.36.0) (2025-09-14) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Support latest app target ([#5782](https://github.com/ReVanced/revanced-patches/issues/5782)) ([88b47ef](https://github.com/ReVanced/revanced-patches/commit/88b47ef414cd073ec3800258b32aceb6f383a411)) +* **YouTube - Hide layout components:** Hide new type of Playable shelf ([8cd8e59](https://github.com/ReVanced/revanced-patches/commit/8cd8e59bbc3a878269276b8ae5f627b044d157f0)) +* **YouTube Music:** Resolve playback issues, change recommended app target to `7.29.52` ([#5813](https://github.com/ReVanced/revanced-patches/issues/5813)) ([a53b00d](https://github.com/ReVanced/revanced-patches/commit/a53b00dd514dbe2b3406f3c1013a4f58a7f481c5)) + + +### Features + +* **YouTube - SponsorBlock:** Add 'Hook' segment category ([#5783](https://github.com/ReVanced/revanced-patches/issues/5783)) ([9d4aa5c](https://github.com/ReVanced/revanced-patches/commit/9d4aa5cd16a6f9e95cf7c626351b46b86ca80efe)) + +# [5.36.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.35.0...v5.36.0-dev.1) (2025-09-13) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Support latest app target ([#5782](https://github.com/ReVanced/revanced-patches/issues/5782)) ([88b47ef](https://github.com/ReVanced/revanced-patches/commit/88b47ef414cd073ec3800258b32aceb6f383a411)) +* **YouTube - Hide layout components:** Hide new type of Playable shelf ([8cd8e59](https://github.com/ReVanced/revanced-patches/commit/8cd8e59bbc3a878269276b8ae5f627b044d157f0)) +* **YouTube Music:** Resolve playback issues, change recommended app target to `7.29.52` ([#5813](https://github.com/ReVanced/revanced-patches/issues/5813)) ([a53b00d](https://github.com/ReVanced/revanced-patches/commit/a53b00dd514dbe2b3406f3c1013a4f58a7f481c5)) + + +### Features + +* **YouTube - SponsorBlock:** Add 'Hook' segment category ([#5783](https://github.com/ReVanced/revanced-patches/issues/5783)) ([9d4aa5c](https://github.com/ReVanced/revanced-patches/commit/9d4aa5cd16a6f9e95cf7c626351b46b86ca80efe)) + +# [5.36.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.35.0...v5.36.0-dev.1) (2025-09-13) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Support latest app target ([#5782](https://github.com/ReVanced/revanced-patches/issues/5782)) ([88b47ef](https://github.com/ReVanced/revanced-patches/commit/88b47ef414cd073ec3800258b32aceb6f383a411)) +* **YouTube - Hide layout components:** Hide new type of Playable shelf ([8cd8e59](https://github.com/ReVanced/revanced-patches/commit/8cd8e59bbc3a878269276b8ae5f627b044d157f0)) +* **YouTube Music:** Resolve playback issues, change recommended app target to `7.29.52` ([#5813](https://github.com/ReVanced/revanced-patches/issues/5813)) ([a53b00d](https://github.com/ReVanced/revanced-patches/commit/a53b00dd514dbe2b3406f3c1013a4f58a7f481c5)) + + +### Features + +* **YouTube - SponsorBlock:** Add 'Hook' segment category ([#5783](https://github.com/ReVanced/revanced-patches/issues/5783)) ([9d4aa5c](https://github.com/ReVanced/revanced-patches/commit/9d4aa5cd16a6f9e95cf7c626351b46b86ca80efe)) + +# [5.36.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.35.0...v5.36.0-dev.1) (2025-09-13) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Support latest app target ([#5782](https://github.com/ReVanced/revanced-patches/issues/5782)) ([88b47ef](https://github.com/ReVanced/revanced-patches/commit/88b47ef414cd073ec3800258b32aceb6f383a411)) +* **YouTube - Hide layout components:** Hide new type of Playable shelf ([8cd8e59](https://github.com/ReVanced/revanced-patches/commit/8cd8e59bbc3a878269276b8ae5f627b044d157f0)) + + +### Features + +* **YouTube - SponsorBlock:** Add 'Hook' segment category ([#5783](https://github.com/ReVanced/revanced-patches/issues/5783)) ([9d4aa5c](https://github.com/ReVanced/revanced-patches/commit/9d4aa5cd16a6f9e95cf7c626351b46b86ca80efe)) + +# [5.36.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.35.0...v5.36.0-dev.1) (2025-09-12) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Support latest app target ([#5782](https://github.com/ReVanced/revanced-patches/issues/5782)) ([88b47ef](https://github.com/ReVanced/revanced-patches/commit/88b47ef414cd073ec3800258b32aceb6f383a411)) +* **YouTube - Hide layout components:** Hide new type of Playable shelf ([8cd8e59](https://github.com/ReVanced/revanced-patches/commit/8cd8e59bbc3a878269276b8ae5f627b044d157f0)) + + +### Features + +* **YouTube - SponsorBlock:** Add 'Hook' segment category ([#5783](https://github.com/ReVanced/revanced-patches/issues/5783)) ([9d4aa5c](https://github.com/ReVanced/revanced-patches/commit/9d4aa5cd16a6f9e95cf7c626351b46b86ca80efe)) + +# [5.36.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.35.1-dev.1...v5.36.0-dev.1) (2025-09-12) + + +### Features + +* **YouTube - SponsorBlock:** Add 'Hook' segment category ([#5783](https://github.com/ReVanced/revanced-patches/issues/5783)) ([2e042c4](https://github.com/ReVanced/revanced-patches/commit/2e042c4b3366fa3daf991d5560fcae991d00ad12)) + +## [5.35.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.35.0...v5.35.1-dev.1) (2025-09-11) + + +### Bug Fixes + +* **Duolingo - Disable ads:** Support latest app target ([#5782](https://github.com/ReVanced/revanced-patches/issues/5782)) ([8491516](https://github.com/ReVanced/revanced-patches/commit/849151637389b8f399356d0d331bb74482f3f05d)) +* **YouTube - Hide layout components:** Hide new type of Playable shelf ([3af4126](https://github.com/ReVanced/revanced-patches/commit/3af41265338ddaab52d009f53370c57abddd4599)) + +# [5.35.0](https://github.com/ReVanced/revanced-patches/compare/v5.34.0...v5.35.0) (2025-09-09) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Fix Manager patching error ([0d10e94](https://github.com/ReVanced/revanced-patches/commit/0d10e94663283fac09f3efc57c9b9805c38c4e13)) +* **Proton mail:** Constrain patches to last working app target ([21c34b9](https://github.com/ReVanced/revanced-patches/commit/21c34b908e07a97de8c31c7c828b44a8cc4739b6)) +* Revert dependency updates to fix Manager pre-release patching ([4c7a1a8](https://github.com/ReVanced/revanced-patches/commit/4c7a1a8554c67797bf663e5230f566c5a9b229af)) +* **Spotify - Unlock Premium:** Make compatible with latest versions again by fixing fingerprint ([#5684](https://github.com/ReVanced/revanced-patches/issues/5684)) ([30dcff1](https://github.com/ReVanced/revanced-patches/commit/30dcff13a56883efc499b71faadb403877cd1c67)) +* **YouTube - Hide layout components:** Hide Playable shelf header ([fbb5046](https://github.com/ReVanced/revanced-patches/commit/fbb50463f0e3f533a278c5251cfbce59f09ce641)) + + +### Features + +* **BaconReader:** Add `Fix Redgifs API` patch ([#5761](https://github.com/ReVanced/revanced-patches/issues/5761)) ([08868c0](https://github.com/ReVanced/revanced-patches/commit/08868c00d3c4f1f37f4a77f333a03ca5a3259b59)) +* **Boost/Sync for Reddit:** Add `Fix Redgifs` patch ([#5725](https://github.com/ReVanced/revanced-patches/issues/5725)) ([c5e8079](https://github.com/ReVanced/revanced-patches/commit/c5e8079eab08075a72078cd0fa79f3beb1f75d98)) +* **Instagram:** Add `Hide navigation buttons` patch ([#5678](https://github.com/ReVanced/revanced-patches/issues/5678)) ([415cf0f](https://github.com/ReVanced/revanced-patches/commit/415cf0fb5b9b3dcaf4592943a69eea1c10447b07)) +* **Instagram:** Add `Hide Stories from Home` patch ([#5756](https://github.com/ReVanced/revanced-patches/issues/5756)) ([3ae3251](https://github.com/ReVanced/revanced-patches/commit/3ae3251dc0317b6ced136fe9aa14be369642f203)) + +# [5.35.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.35.0-dev.4...v5.35.0-dev.5) (2025-09-06) + + +### Features + +* **BaconReader:** Add `Fix Redgifs API` patch ([#5761](https://github.com/ReVanced/revanced-patches/issues/5761)) ([08868c0](https://github.com/ReVanced/revanced-patches/commit/08868c00d3c4f1f37f4a77f333a03ca5a3259b59)) +* **Instagram:** Add `Hide Stories from Home` patch ([#5756](https://github.com/ReVanced/revanced-patches/issues/5756)) ([3ae3251](https://github.com/ReVanced/revanced-patches/commit/3ae3251dc0317b6ced136fe9aa14be369642f203)) + +# [5.35.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.35.0-dev.3...v5.35.0-dev.4) (2025-09-04) + + +### Features + +* **Boost/Sync for Reddit:** Add `Fix Redgifs` patch ([#5725](https://github.com/ReVanced/revanced-patches/issues/5725)) ([c5e8079](https://github.com/ReVanced/revanced-patches/commit/c5e8079eab08075a72078cd0fa79f3beb1f75d98)) + +# [5.35.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.35.0-dev.2...v5.35.0-dev.3) (2025-09-04) + + +### Bug Fixes + +* **Instagram - Hide navigation buttons:** Fix Manager patching error ([0d10e94](https://github.com/ReVanced/revanced-patches/commit/0d10e94663283fac09f3efc57c9b9805c38c4e13)) + +# [5.35.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.35.0-dev.1...v5.35.0-dev.2) (2025-09-04) + + +### Bug Fixes + +* Revert dependency updates to fix Manager pre-release patching ([4c7a1a8](https://github.com/ReVanced/revanced-patches/commit/4c7a1a8554c67797bf663e5230f566c5a9b229af)) + +# [5.35.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.34.1-dev.3...v5.35.0-dev.1) (2025-09-03) + + +### Features + +* **Instagram:** Add `Hide navigation buttons` patch ([#5678](https://github.com/ReVanced/revanced-patches/issues/5678)) ([415cf0f](https://github.com/ReVanced/revanced-patches/commit/415cf0fb5b9b3dcaf4592943a69eea1c10447b07)) + +## [5.34.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.34.1-dev.2...v5.34.1-dev.3) (2025-08-24) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide Playable shelf header ([fbb5046](https://github.com/ReVanced/revanced-patches/commit/fbb50463f0e3f533a278c5251cfbce59f09ce641)) + +## [5.34.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.34.1-dev.1...v5.34.1-dev.2) (2025-08-22) + + +### Bug Fixes + +* **Proton mail:** Constrain patches to last working app target ([21c34b9](https://github.com/ReVanced/revanced-patches/commit/21c34b908e07a97de8c31c7c828b44a8cc4739b6)) + +## [5.34.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.34.0...v5.34.1-dev.1) (2025-08-21) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Make compatible with latest versions again by fixing fingerprint ([#5684](https://github.com/ReVanced/revanced-patches/issues/5684)) ([30dcff1](https://github.com/ReVanced/revanced-patches/commit/30dcff13a56883efc499b71faadb403877cd1c67)) + +# [5.34.0](https://github.com/ReVanced/revanced-patches/compare/v5.33.0...v5.34.0) (2025-08-19) + + +### Bug Fixes + +* **Backdrops:** Remove broken patch that is no longer supported ([#5627](https://github.com/ReVanced/revanced-patches/issues/5627)) ([ebb8332](https://github.com/ReVanced/revanced-patches/commit/ebb83320838aa99dd4417d45a50333dd42c1218a)) +* **pixiv - Hide ads:** Constrain patch to last working app target ([d8ea56c](https://github.com/ReVanced/revanced-patches/commit/d8ea56ca4be47df1c43f96ec41b91c800f1d9daf)) +* **Twitch:** Constrain patches to last working app targets ([#5373](https://github.com/ReVanced/revanced-patches/issues/5373)) ([29a4748](https://github.com/ReVanced/revanced-patches/commit/29a47481c4efa209a3a53df60613b59a73adbe07)) +* **YouTube - Hide layout components:** Do not hide community posts on channel profiles ([#5634](https://github.com/ReVanced/revanced-patches/issues/5634)) ([9e3d5a2](https://github.com/ReVanced/revanced-patches/commit/9e3d5a2b36106479470f3f69920518b57e8c4dca)) +* **YouTube - Player Controls:** Fix chapter title overlapping the bottom buttons ([#5673](https://github.com/ReVanced/revanced-patches/issues/5673)) ([09ccee7](https://github.com/ReVanced/revanced-patches/commit/09ccee71384df338bbf8acc1097f619a372c4868)) +* **YouTube - SponsorBlock:** Do not hide voting or create button when the video ends ([6aba4e2](https://github.com/ReVanced/revanced-patches/commit/6aba4e284de9bb94b49eea8be2baf2870eecbbcf)) +* **YouTube - Video playback:** Disable HDR video does not disable Dolby Vision HDR ([#5661](https://github.com/ReVanced/revanced-patches/issues/5661)) ([6dab988](https://github.com/ReVanced/revanced-patches/commit/6dab98810645b96bd0387ba7d607e5d8ffb1b5bb)) +* **YouTube - Video quality:** Fix additional incorrect quality resolutions used by YouTube ([a2a1fbe](https://github.com/ReVanced/revanced-patches/commit/a2a1fbe2959be8334c54cfc3426c24a960c55c8f)) +* **YouTube - Video quality:** Show FHD+ icon for 1080p 60fps enhanced bitrate ([76bed37](https://github.com/ReVanced/revanced-patches/commit/76bed3734093713af24ef065d5ffc5b1cd83f29a)) +* **YouTube:** Use correct fade out animation when tapping to dismiss the video overlay ([#5670](https://github.com/ReVanced/revanced-patches/issues/5670)) ([cce6737](https://github.com/ReVanced/revanced-patches/commit/cce6737f627fc7621bbde50a5653b6af14c6f31a)) + + +### Features + +* **Instagram:** Support latest app version ([#5611](https://github.com/ReVanced/revanced-patches/issues/5611)) ([26fe690](https://github.com/ReVanced/revanced-patches/commit/26fe690dfbefe6c412c5f81f208a3b1d2fbd7a0a)) +* **NU.nl:** Support latest app version ([#5643](https://github.com/ReVanced/revanced-patches/issues/5643)) ([7338e4a](https://github.com/ReVanced/revanced-patches/commit/7338e4a5a99f913256120d0d58fede3aa4ee8922)) +* **YouTube - Hide player flyout menu items:** Add option to hide quality flyout menu ([eb55068](https://github.com/ReVanced/revanced-patches/commit/eb5506856a2eaf2a8585e598868ddba3e1429159)) +* **YouTube - Hide video action buttons:** Add "Hide Hype button" setting ([f13f377](https://github.com/ReVanced/revanced-patches/commit/f13f3770e7c4fd5bff8f3e224fb1b1ead50a3c18)) +* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b)) +* **YouTube - Playback speed:** Show current playback speed on player speed dialog button ([#5607](https://github.com/ReVanced/revanced-patches/issues/5607)) ([279436a](https://github.com/ReVanced/revanced-patches/commit/279436a3657b50f98bb4cc64dc88dc14e422f204)) +* **YouTube:** Add `Disable sign in to TV popup` patch ([#5639](https://github.com/ReVanced/revanced-patches/issues/5639)) ([d0e5bd0](https://github.com/ReVanced/revanced-patches/commit/d0e5bd0479a8910b081c483ed2a6ab4d7134e3c3)) + +# [5.34.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.12...v5.34.0-dev.13) (2025-08-19) + + +### Bug Fixes + +* **YouTube - Player Controls:** Fix chapter title overlapping the bottom buttons ([#5673](https://github.com/ReVanced/revanced-patches/issues/5673)) ([09ccee7](https://github.com/ReVanced/revanced-patches/commit/09ccee71384df338bbf8acc1097f619a372c4868)) + +# [5.34.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.12...v5.34.0-dev.13) (2025-08-18) + + +### Bug Fixes + +* **YouTube - Player Controls:** Fix chapter title overlapping the bottom buttons ([#5673](https://github.com/ReVanced/revanced-patches/issues/5673)) ([09ccee7](https://github.com/ReVanced/revanced-patches/commit/09ccee71384df338bbf8acc1097f619a372c4868)) + +# [5.34.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.11...v5.34.0-dev.12) (2025-08-18) + + +### Bug Fixes + +* **YouTube:** Use correct fade out animation when tapping to dismiss the video overlay ([#5670](https://github.com/ReVanced/revanced-patches/issues/5670)) ([cce6737](https://github.com/ReVanced/revanced-patches/commit/cce6737f627fc7621bbde50a5653b6af14c6f31a)) + +# [5.34.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.10...v5.34.0-dev.11) (2025-08-16) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Do not hide voting or create button when the video ends ([6aba4e2](https://github.com/ReVanced/revanced-patches/commit/6aba4e284de9bb94b49eea8be2baf2870eecbbcf)) + +# [5.34.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.9...v5.34.0-dev.10) (2025-08-16) + + +### Bug Fixes + +* **YouTube - Video playback:** Disable HDR video does not disable Dolby Vision HDR ([#5661](https://github.com/ReVanced/revanced-patches/issues/5661)) ([6dab988](https://github.com/ReVanced/revanced-patches/commit/6dab98810645b96bd0387ba7d607e5d8ffb1b5bb)) + + +### Features + +* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b)) + +# [5.34.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.9...v5.34.0-dev.10) (2025-08-16) + + +### Features + +* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b)) + +# [5.34.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.9...v5.34.0-dev.10) (2025-08-16) + + +### Features + +* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b)) + +# [5.34.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.8...v5.34.0-dev.9) (2025-08-16) + + +### Features + +* **YouTube - Hide video action buttons:** Add "Hide Hype button" setting ([f13f377](https://github.com/ReVanced/revanced-patches/commit/f13f3770e7c4fd5bff8f3e224fb1b1ead50a3c18)) + +# [5.34.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.7...v5.34.0-dev.8) (2025-08-15) + + +### Features + +* **NU.nl:** Support latest app version ([#5643](https://github.com/ReVanced/revanced-patches/issues/5643)) ([7338e4a](https://github.com/ReVanced/revanced-patches/commit/7338e4a5a99f913256120d0d58fede3aa4ee8922)) +* **YouTube:** Add `Disable sign in to TV popup` patch ([#5639](https://github.com/ReVanced/revanced-patches/issues/5639)) ([d0e5bd0](https://github.com/ReVanced/revanced-patches/commit/d0e5bd0479a8910b081c483ed2a6ab4d7134e3c3)) + +# [5.34.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.6...v5.34.0-dev.7) (2025-08-13) + + +### Bug Fixes + +* **YouTube - Video quality:** Fix additional incorrect quality resolutions used by YouTube ([a2a1fbe](https://github.com/ReVanced/revanced-patches/commit/a2a1fbe2959be8334c54cfc3426c24a960c55c8f)) + +# [5.34.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.5...v5.34.0-dev.6) (2025-08-11) + + +### Bug Fixes + +* **YouTube - Video quality:** Show FHD+ icon for 1080p 60fps enhanced bitrate ([76bed37](https://github.com/ReVanced/revanced-patches/commit/76bed3734093713af24ef065d5ffc5b1cd83f29a)) + +# [5.34.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.4...v5.34.0-dev.5) (2025-08-10) + + +### Features + +* **YouTube - Hide player flyout menu items:** Add option to hide quality flyout menu ([eb55068](https://github.com/ReVanced/revanced-patches/commit/eb5506856a2eaf2a8585e598868ddba3e1429159)) + +# [5.34.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.3...v5.34.0-dev.4) (2025-08-10) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Do not hide community posts on channel profiles ([#5634](https://github.com/ReVanced/revanced-patches/issues/5634)) ([9e3d5a2](https://github.com/ReVanced/revanced-patches/commit/9e3d5a2b36106479470f3f69920518b57e8c4dca)) + +# [5.34.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.2...v5.34.0-dev.3) (2025-08-09) + + +### Bug Fixes + +* **pixiv - Hide ads:** Constrain patch to last working app target ([d8ea56c](https://github.com/ReVanced/revanced-patches/commit/d8ea56ca4be47df1c43f96ec41b91c800f1d9daf)) + +# [5.34.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.1...v5.34.0-dev.2) (2025-08-09) + + +### Bug Fixes + +* **Backdrops:** Remove broken patch that is no longer supported ([#5627](https://github.com/ReVanced/revanced-patches/issues/5627)) ([ebb8332](https://github.com/ReVanced/revanced-patches/commit/ebb83320838aa99dd4417d45a50333dd42c1218a)) + + +### Features + +* **YouTube - Playback speed:** Show current playback speed on player speed dialog button ([#5607](https://github.com/ReVanced/revanced-patches/issues/5607)) ([279436a](https://github.com/ReVanced/revanced-patches/commit/279436a3657b50f98bb4cc64dc88dc14e422f204)) + +# [5.34.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.33.0...v5.34.0-dev.1) (2025-08-08) + + +### Bug Fixes + +* **Twitch:** Constrain patches to last working app targets ([#5373](https://github.com/ReVanced/revanced-patches/issues/5373)) ([29a4748](https://github.com/ReVanced/revanced-patches/commit/29a47481c4efa209a3a53df60613b59a73adbe07)) + + +### Features + +* **Instagram:** Support latest app version ([#5611](https://github.com/ReVanced/revanced-patches/issues/5611)) ([26fe690](https://github.com/ReVanced/revanced-patches/commit/26fe690dfbefe6c412c5f81f208a3b1d2fbd7a0a)) + +# [5.33.0](https://github.com/ReVanced/revanced-patches/compare/v5.32.0...v5.33.0) (2025-08-05) + + +### Bug Fixes + +* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([0cab98d](https://github.com/ReVanced/revanced-patches/commit/0cab98df1689dbf7a042f18f4a961d47da1430ad)) +* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([cd3a6be](https://github.com/ReVanced/revanced-patches/commit/cd3a6be75c6bd3cc33c0b17a044bd6147f27b5ce)) +* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([9fe13ee](https://github.com/ReVanced/revanced-patches/commit/9fe13ee1af104c009efd19b826adef375e48e191)) +* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([bf29d69](https://github.com/ReVanced/revanced-patches/commit/bf29d6909e389819bad878ad3b94bbc90d823cc9)) +* **YouTube - Playback speed:** Use old speed menu for player button if enabled ([1e8f436](https://github.com/ReVanced/revanced-patches/commit/1e8f4368e117f4b278c24709231cb32546e46dc0)) +* **YouTube - Video quality:** Fix 144p default not always used ([2f7483a](https://github.com/ReVanced/revanced-patches/commit/2f7483a2d789c28a243b58bb7a252c0d590858ee)) +* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([295f0f2](https://github.com/ReVanced/revanced-patches/commit/295f0f216b5e8aa9d68457862e73e312b7342703)) +* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([7378ae3](https://github.com/ReVanced/revanced-patches/commit/7378ae3c5fc88f91bf5cd6db47c6cd170a8c5a4f)) +* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([bd3ace0](https://github.com/ReVanced/revanced-patches/commit/bd3ace0bd04ccd0369adb49d63aa0cf986402346)) + + +### Features + +* **ORF ON:** Add `Remove root detection` patch ([#5551](https://github.com/ReVanced/revanced-patches/issues/5551)) ([6c6aa35](https://github.com/ReVanced/revanced-patches/commit/6c6aa35411a139dddc3a15dd757fbeded5d1a0a3)) +* **YouTube - Playback speed:** Add "Restore old playback speed menu" option ([#5552](https://github.com/ReVanced/revanced-patches/issues/5552)) ([b01f15b](https://github.com/ReVanced/revanced-patches/commit/b01f15b9acb0427aed99b0141ae271831b7936bf)) +* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([d5f51bf](https://github.com/ReVanced/revanced-patches/commit/d5f51bf400dd22626ff65d7563b6fde70d53fb25)) + + +### Performance Improvements + +* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([ccac46e](https://github.com/ReVanced/revanced-patches/commit/ccac46eebc2e14b094454e37ef4461d48a62c53f)) + +# [5.33.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.12...v5.33.0-dev.13) (2025-08-05) + + +### Bug Fixes + +* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([0cab98d](https://github.com/ReVanced/revanced-patches/commit/0cab98df1689dbf7a042f18f4a961d47da1430ad)) + +# [5.33.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.11...v5.33.0-dev.12) (2025-08-04) + + +### Bug Fixes + +* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([295f0f2](https://github.com/ReVanced/revanced-patches/commit/295f0f216b5e8aa9d68457862e73e312b7342703)) + +# [5.33.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.10...v5.33.0-dev.11) (2025-08-04) + + +### Bug Fixes + +* **YouTube - Video quality:** Fix 144p default not always used ([2f7483a](https://github.com/ReVanced/revanced-patches/commit/2f7483a2d789c28a243b58bb7a252c0d590858ee)) + +# [5.33.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.9...v5.33.0-dev.10) (2025-08-04) + + +### Bug Fixes + +* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([7378ae3](https://github.com/ReVanced/revanced-patches/commit/7378ae3c5fc88f91bf5cd6db47c6cd170a8c5a4f)) + +# [5.33.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.8...v5.33.0-dev.9) (2025-08-04) + + +### Bug Fixes + +* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([9fe13ee](https://github.com/ReVanced/revanced-patches/commit/9fe13ee1af104c009efd19b826adef375e48e191)) + +# [5.33.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.7...v5.33.0-dev.8) (2025-08-03) + + +### Bug Fixes + +* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([cd3a6be](https://github.com/ReVanced/revanced-patches/commit/cd3a6be75c6bd3cc33c0b17a044bd6147f27b5ce)) + +# [5.33.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.6...v5.33.0-dev.7) (2025-08-03) + + +### Features + +* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([d5f51bf](https://github.com/ReVanced/revanced-patches/commit/d5f51bf400dd22626ff65d7563b6fde70d53fb25)) + +# [5.33.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.5...v5.33.0-dev.6) (2025-07-31) + + +### Bug Fixes + +* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([bd3ace0](https://github.com/ReVanced/revanced-patches/commit/bd3ace0bd04ccd0369adb49d63aa0cf986402346)) + +# [5.33.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.4...v5.33.0-dev.5) (2025-07-31) + + +### Bug Fixes + +* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([bf29d69](https://github.com/ReVanced/revanced-patches/commit/bf29d6909e389819bad878ad3b94bbc90d823cc9)) + +# [5.33.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.3...v5.33.0-dev.4) (2025-07-30) + + +### Performance Improvements + +* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([ccac46e](https://github.com/ReVanced/revanced-patches/commit/ccac46eebc2e14b094454e37ef4461d48a62c53f)) + +# [5.33.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.2...v5.33.0-dev.3) (2025-07-30) + + +### Bug Fixes + +* **YouTube - Playback speed:** Use old speed menu for player button if enabled ([1e8f436](https://github.com/ReVanced/revanced-patches/commit/1e8f4368e117f4b278c24709231cb32546e46dc0)) + +# [5.33.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.1...v5.33.0-dev.2) (2025-07-29) + + +### Features + +* **ORF ON:** Add `Remove root detection` patch ([#5551](https://github.com/ReVanced/revanced-patches/issues/5551)) ([6c6aa35](https://github.com/ReVanced/revanced-patches/commit/6c6aa35411a139dddc3a15dd757fbeded5d1a0a3)) + +# [5.33.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.32.0...v5.33.0-dev.1) (2025-07-28) + + +### Features + +* **YouTube - Playback speed:** Add "Restore old playback speed menu" option ([#5552](https://github.com/ReVanced/revanced-patches/issues/5552)) ([b01f15b](https://github.com/ReVanced/revanced-patches/commit/b01f15b9acb0427aed99b0141ae271831b7936bf)) + +# [5.32.0](https://github.com/ReVanced/revanced-patches/compare/v5.31.2...v5.32.0) (2025-07-27) + + +### Bug Fixes + +* **Messenger - Hide inbox ads:** Support the latest app version ([2959c02](https://github.com/ReVanced/revanced-patches/commit/2959c0214dfa703ee623ef1f89bded7f78c9d252)) +* **YouTube - Hide layout components:** Fix "Hide ticket shelf" ([#5516](https://github.com/ReVanced/revanced-patches/issues/5516)) ([3b85c71](https://github.com/ReVanced/revanced-patches/commit/3b85c71433325fff49e01c77c7b9ff8ddd0a7068)) +* **YouTube - GmsCore support:** Fix search suggestions when logged out by using correct search provider ([#5483](https://github.com/ReVanced/revanced-patches/issues/5483)) ([e86fdc8](https://github.com/ReVanced/revanced-patches/commit/e86fdc86b161a6077960b85149e83bacbac664e7)) + + +### Features + +* **Prime Video:** Add `Playback speed` patch ([#5444](https://github.com/ReVanced/revanced-patches/issues/5444)) ([22cf313](https://github.com/ReVanced/revanced-patches/commit/22cf313a7b99b69e17b9d488c514802043a5dc10)) +* **YouTube - External downloads:** Improve the selection of the external downloader package ([#5504](https://github.com/ReVanced/revanced-patches/issues/5504)) ([5de9aa9](https://github.com/ReVanced/revanced-patches/commit/5de9aa9fad4f24186da045fb188f8718d6f63d7a)) +* **YT Music:** Support latest versions ([#5524](https://github.com/ReVanced/revanced-patches/issues/5524)) ([551dcf0](https://github.com/ReVanced/revanced-patches/commit/551dcf01ca9c489a779196b49c8744727d79d6bc)) + +# [5.32.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.4...v5.32.0-dev.5) (2025-07-26) + + +### Features + +* **YT Music:** Support latest versions ([#5524](https://github.com/ReVanced/revanced-patches/issues/5524)) ([551dcf0](https://github.com/ReVanced/revanced-patches/commit/551dcf01ca9c489a779196b49c8744727d79d6bc)) + +# [5.32.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.3...v5.32.0-dev.4) (2025-07-25) + + +### Bug Fixes + +* **Messenger - Hide inbox ads:** Support the latest app version ([2959c02](https://github.com/ReVanced/revanced-patches/commit/2959c0214dfa703ee623ef1f89bded7f78c9d252)) + +# [5.32.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.2...v5.32.0-dev.3) (2025-07-24) + + +### Features + +* **YouTube - External downloads:** Improve the selection of the external downloader package ([#5504](https://github.com/ReVanced/revanced-patches/issues/5504)) ([5de9aa9](https://github.com/ReVanced/revanced-patches/commit/5de9aa9fad4f24186da045fb188f8718d6f63d7a)) + +# [5.32.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.32.0-dev.1...v5.32.0-dev.2) (2025-07-23) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix "Hide ticket shelf" ([#5516](https://github.com/ReVanced/revanced-patches/issues/5516)) ([3b85c71](https://github.com/ReVanced/revanced-patches/commit/3b85c71433325fff49e01c77c7b9ff8ddd0a7068)) + +# [5.32.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.3-dev.1...v5.32.0-dev.1) (2025-07-16) + + +### Features + +* **Prime Video:** Add `Playback speed` patch ([#5444](https://github.com/ReVanced/revanced-patches/issues/5444)) ([22cf313](https://github.com/ReVanced/revanced-patches/commit/22cf313a7b99b69e17b9d488c514802043a5dc10)) + +## [5.31.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.2...v5.31.3-dev.1) (2025-07-16) + + +### Bug Fixes + +* **YouTube - GmsCore support:** Fix search suggestions when logged out by using correct search provider ([#5483](https://github.com/ReVanced/revanced-patches/issues/5483)) ([e86fdc8](https://github.com/ReVanced/revanced-patches/commit/e86fdc86b161a6077960b85149e83bacbac664e7)) + +## [5.31.2](https://github.com/ReVanced/revanced-patches/compare/v5.31.1...v5.31.2) (2025-07-14) + + +### Bug Fixes + +* **Spotify - Spoof client:** Fix login failing by spoofing login request in addition ([#5448](https://github.com/ReVanced/revanced-patches/issues/5448)) ([4e59ddc](https://github.com/ReVanced/revanced-patches/commit/4e59ddc62388d09f71b89593fc8b76933d9facea)) +* **YouTube - Disable double tap actions:** Remove old incompatible targets ([857053e](https://github.com/ReVanced/revanced-patches/commit/857053e29b72ded10a84b0ac693fa107705342d9)) +* **YouTube - Hide layout components:** Hide quick actions does not work ([#5423](https://github.com/ReVanced/revanced-patches/issues/5423)) ([9c66729](https://github.com/ReVanced/revanced-patches/commit/9c6672946d44001e106bdac9041e2d79ef3f6ab2)) +* **YouTube - Hide layout components:** Show correct custom header logo if 'Hide YouTube Doodles' is enabled ([#5431](https://github.com/ReVanced/revanced-patches/issues/5431)) ([20cc141](https://github.com/ReVanced/revanced-patches/commit/20cc141e61f75de1a1749247c4f4aed167dee8ea)) +* **YouTube - Settings:** Back button/gesture closes search instead of exiting ([#5418](https://github.com/ReVanced/revanced-patches/issues/5418)) ([134b278](https://github.com/ReVanced/revanced-patches/commit/134b278baa7b90d2c4b06200cabacabf55ebc055)) + +## [5.31.2-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.4...v5.31.2-dev.5) (2025-07-14) + + +### Bug Fixes + +* **Spotify - Spoof client:** Fix login failing by spoofing login request in addition ([#5448](https://github.com/ReVanced/revanced-patches/issues/5448)) ([4e59ddc](https://github.com/ReVanced/revanced-patches/commit/4e59ddc62388d09f71b89593fc8b76933d9facea)) + +## [5.31.2-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.3...v5.31.2-dev.4) (2025-07-13) + + +### Bug Fixes + +* **YouTube - Settings:** Back button/gesture closes search instead of exiting ([#5418](https://github.com/ReVanced/revanced-patches/issues/5418)) ([134b278](https://github.com/ReVanced/revanced-patches/commit/134b278baa7b90d2c4b06200cabacabf55ebc055)) + +## [5.31.2-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.2...v5.31.2-dev.3) (2025-07-13) + + +### Bug Fixes + +* **YouTube - Disable double tap actions:** Remove old incompatible targets ([857053e](https://github.com/ReVanced/revanced-patches/commit/857053e29b72ded10a84b0ac693fa107705342d9)) + +## [5.31.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.31.2-dev.1...v5.31.2-dev.2) (2025-07-12) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Show correct custom header logo if 'Hide YouTube Doodles' is enabled ([#5431](https://github.com/ReVanced/revanced-patches/issues/5431)) ([20cc141](https://github.com/ReVanced/revanced-patches/commit/20cc141e61f75de1a1749247c4f4aed167dee8ea)) + +## [5.31.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.1...v5.31.2-dev.1) (2025-07-12) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide quick actions does not work ([#5423](https://github.com/ReVanced/revanced-patches/issues/5423)) ([9c66729](https://github.com/ReVanced/revanced-patches/commit/9c6672946d44001e106bdac9041e2d79ef3f6ab2)) + +## [5.31.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.0...v5.31.1) (2025-07-11) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Fix hiding context menu ads for latest version ([#5415](https://github.com/ReVanced/revanced-patches/issues/5415)) ([dcde393](https://github.com/ReVanced/revanced-patches/commit/dcde3935bde3172576d0f9f5ff9eb62ecfff7dfe)) + +## [5.31.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.31.0...v5.31.1-dev.1) (2025-07-11) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Fix hiding context menu ads for latest version ([#5415](https://github.com/ReVanced/revanced-patches/issues/5415)) ([dcde393](https://github.com/ReVanced/revanced-patches/commit/dcde3935bde3172576d0f9f5ff9eb62ecfff7dfe)) + +# [5.31.0](https://github.com/ReVanced/revanced-patches/compare/v5.30.0...v5.31.0) (2025-07-11) + + +### Bug Fixes + +* **Bacon Reader - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5402](https://github.com/ReVanced/revanced-patches/issues/5402)) ([72459bb](https://github.com/ReVanced/revanced-patches/commit/72459bb2eaf4691e32822dfdd1db3240e2fe98dd)) +* Correctly name `Enable ROM signature spoofing` patch ([d85881a](https://github.com/ReVanced/revanced-patches/commit/d85881a6768232a999534677bebb248e640fe5ab)) +* Fix accidental changes ([e2ac841](https://github.com/ReVanced/revanced-patches/commit/e2ac8419756e3c7d62e2c0430a2918a3c1c63666)) +* Fix refactoring typo ([ec0ae42](https://github.com/ReVanced/revanced-patches/commit/ec0ae42496628cdeb2a639020fce94316b41b751)) +* Handle empty list of announcements ([de9d720](https://github.com/ReVanced/revanced-patches/commit/de9d7209f4e818a618a7fd9000013ae8ebd728f2)) +* **SoundCloud:** Constrain patches to last working app target ([e8ea89f](https://github.com/ReVanced/revanced-patches/commit/e8ea89fc1a3f0531a0af7529663f13328aca4fe7)) +* **Spotify - Unlock Premium:** Remove wrongfully hidden non ad browse sections ([#5403](https://github.com/ReVanced/revanced-patches/issues/5403)) ([8633544](https://github.com/ReVanced/revanced-patches/commit/8633544decc0814d7a548fbc5576b4bdd1d7eee0)) +* **Spotify:** Remove other ads type from the browse screen ([#5333](https://github.com/ReVanced/revanced-patches/issues/5333)) ([c68533a](https://github.com/ReVanced/revanced-patches/commit/c68533a33a399ca813380b5c9ccddce434ceadf8)) +* **Sync for Reddit - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5392](https://github.com/ReVanced/revanced-patches/issues/5392)) ([47e6b62](https://github.com/ReVanced/revanced-patches/commit/47e6b62f3d8b07960cfb2963f441222d3e67df92)) +* **YouTube - Hide ads:** Hide new type of general ad ([#5345](https://github.com/ReVanced/revanced-patches/issues/5345)) ([f23716b](https://github.com/ReVanced/revanced-patches/commit/f23716bc52c03d8d0271bfe38b19247e6de7021d)) +* **YouTube - Hide layout components:** Do not hide playlist sort button if 'Hide AI comments summary' is on ([5f3e48e](https://github.com/ReVanced/revanced-patches/commit/5f3e48ec5853f6439800ef58239291c34bcab5f6)) +* **YouTube - Playback speed:** Allow custom speeds with 0.01x precision ([#5360](https://github.com/ReVanced/revanced-patches/issues/5360)) ([0eecef0](https://github.com/ReVanced/revanced-patches/commit/0eecef00fc93d2a217944978e29dce82e3134e35)) +* **YouTube - Slide to seek:** Show tap and hold 2x speed overlay when active ([#5398](https://github.com/ReVanced/revanced-patches/issues/5398)) ([dbc9c5f](https://github.com/ReVanced/revanced-patches/commit/dbc9c5f00c1f5bbb95f8822667cc1ac3c613fa00)) + + +### Features + +* **Cricbuzz - Hide ads:** Hide Cricbuzz11 UI elements ([#5381](https://github.com/ReVanced/revanced-patches/issues/5381)) ([a42c98f](https://github.com/ReVanced/revanced-patches/commit/a42c98f8b51fd37d815fd38b75a2b7ccc4fb049b)) +* **Lightroom:** Constrain patches to last working version ([#5335](https://github.com/ReVanced/revanced-patches/issues/5335)) ([32ce70e](https://github.com/ReVanced/revanced-patches/commit/32ce70e994f354b9a569376bb89eb38b3190e6f9)) +* **Spotify - Spoof client:** Fix issues like songs skipping by spoofing to iOS ([#5388](https://github.com/ReVanced/revanced-patches/issues/5388)) ([e36d4c1](https://github.com/ReVanced/revanced-patches/commit/e36d4c1986b58815c7659e6ef44011166873f9c8)) +* **Spotify:** Remove support for old versions ([#5404](https://github.com/ReVanced/revanced-patches/issues/5404)) ([9d31238](https://github.com/ReVanced/revanced-patches/commit/9d31238803a45e957472760fc40c3862da2cf3f0)) +* **YouTube - Change header:** Add in-app setting to change the app header ([#5346](https://github.com/ReVanced/revanced-patches/issues/5346)) ([9ba45b6](https://github.com/ReVanced/revanced-patches/commit/9ba45b6680595d732b47e8fa54bee98b7c7af179)) +* **YouTube - Hide layout components:** Add `Hide channel links preview` and `Hide 'Visit Community' button` in channel page ([#5320](https://github.com/ReVanced/revanced-patches/issues/5320)) ([9d9cce3](https://github.com/ReVanced/revanced-patches/commit/9d9cce3ec5550b2fea88df745f1700bb2f17eb9e)) +* **YouTube:** Disable two-finger tap gesture for skipping chapters ([#5374](https://github.com/ReVanced/revanced-patches/issues/5374)) ([71db0a2](https://github.com/ReVanced/revanced-patches/commit/71db0a2661b5f76eb5048cdeed83f26fbfdf4fee)) + +# [5.31.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.16...v5.31.0-dev.17) (2025-07-11) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Remove wrongfully hidden non ad browse sections ([#5403](https://github.com/ReVanced/revanced-patches/issues/5403)) ([8633544](https://github.com/ReVanced/revanced-patches/commit/8633544decc0814d7a548fbc5576b4bdd1d7eee0)) + + +### Features + +* **Spotify:** Remove support for old versions ([#5404](https://github.com/ReVanced/revanced-patches/issues/5404)) ([9d31238](https://github.com/ReVanced/revanced-patches/commit/9d31238803a45e957472760fc40c3862da2cf3f0)) + +# [5.31.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.15...v5.31.0-dev.16) (2025-07-11) + + +### Features + +* **Spotify - Spoof client:** Fix issues like songs skipping by spoofing to iOS ([#5388](https://github.com/ReVanced/revanced-patches/issues/5388)) ([e36d4c1](https://github.com/ReVanced/revanced-patches/commit/e36d4c1986b58815c7659e6ef44011166873f9c8)) +* **YouTube:** Disable two-finger tap gesture for skipping chapters ([#5374](https://github.com/ReVanced/revanced-patches/issues/5374)) ([71db0a2](https://github.com/ReVanced/revanced-patches/commit/71db0a2661b5f76eb5048cdeed83f26fbfdf4fee)) + +# [5.31.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.14...v5.31.0-dev.15) (2025-07-11) + + +### Bug Fixes + +* Handle empty list of announcements ([de9d720](https://github.com/ReVanced/revanced-patches/commit/de9d7209f4e818a618a7fd9000013ae8ebd728f2)) + +# [5.31.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.13...v5.31.0-dev.14) (2025-07-10) + + +### Bug Fixes + +* **Bacon Reader - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5402](https://github.com/ReVanced/revanced-patches/issues/5402)) ([72459bb](https://github.com/ReVanced/revanced-patches/commit/72459bb2eaf4691e32822dfdd1db3240e2fe98dd)) + +# [5.31.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.12...v5.31.0-dev.13) (2025-07-10) + + +### Bug Fixes + +* **YouTube - Slide to seek:** Show tap and hold 2x speed overlay when active ([#5398](https://github.com/ReVanced/revanced-patches/issues/5398)) ([dbc9c5f](https://github.com/ReVanced/revanced-patches/commit/dbc9c5f00c1f5bbb95f8822667cc1ac3c613fa00)) + +# [5.31.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.11...v5.31.0-dev.12) (2025-07-09) + + +### Bug Fixes + +* **Sync for Reddit - Spoof client:** Use www instead of ssl API to fix auth related issues ([#5392](https://github.com/ReVanced/revanced-patches/issues/5392)) ([47e6b62](https://github.com/ReVanced/revanced-patches/commit/47e6b62f3d8b07960cfb2963f441222d3e67df92)) + +# [5.31.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.10...v5.31.0-dev.11) (2025-07-09) + + +### Features + +* **Cricbuzz - Hide ads:** Hide Cricbuzz11 UI elements ([#5381](https://github.com/ReVanced/revanced-patches/issues/5381)) ([a42c98f](https://github.com/ReVanced/revanced-patches/commit/a42c98f8b51fd37d815fd38b75a2b7ccc4fb049b)) + +# [5.31.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.9...v5.31.0-dev.10) (2025-07-09) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Do not hide playlist sort button if 'Hide AI comments summary' is on ([5f3e48e](https://github.com/ReVanced/revanced-patches/commit/5f3e48ec5853f6439800ef58239291c34bcab5f6)) + +# [5.31.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.8...v5.31.0-dev.9) (2025-07-07) + + +### Bug Fixes + +* Fix accidental changes ([e2ac841](https://github.com/ReVanced/revanced-patches/commit/e2ac8419756e3c7d62e2c0430a2918a3c1c63666)) + +# [5.31.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.7...v5.31.0-dev.8) (2025-07-07) + + +### Bug Fixes + +* Correctly name `Enable ROM signature spoofing` patch ([d85881a](https://github.com/ReVanced/revanced-patches/commit/d85881a6768232a999534677bebb248e640fe5ab)) + +# [5.31.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.6...v5.31.0-dev.7) (2025-07-06) + + +### Bug Fixes + +* Fix refactoring typo ([ec0ae42](https://github.com/ReVanced/revanced-patches/commit/ec0ae42496628cdeb2a639020fce94316b41b751)) + +# [5.31.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.5...v5.31.0-dev.6) (2025-07-06) + + +### Bug Fixes + +* **YouTube - Playback speed:** Allow custom speeds with 0.01x precision ([#5360](https://github.com/ReVanced/revanced-patches/issues/5360)) ([0eecef0](https://github.com/ReVanced/revanced-patches/commit/0eecef00fc93d2a217944978e29dce82e3134e35)) + +# [5.31.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.4...v5.31.0-dev.5) (2025-07-05) + + +### Features + +* **YouTube - Change header:** Add in-app setting to change the app header ([#5346](https://github.com/ReVanced/revanced-patches/issues/5346)) ([9ba45b6](https://github.com/ReVanced/revanced-patches/commit/9ba45b6680595d732b47e8fa54bee98b7c7af179)) + +# [5.31.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.3...v5.31.0-dev.4) (2025-07-04) + + +### Bug Fixes + +* **YouTube - Hide ads:** Hide new type of general ad ([#5345](https://github.com/ReVanced/revanced-patches/issues/5345)) ([f23716b](https://github.com/ReVanced/revanced-patches/commit/f23716bc52c03d8d0271bfe38b19247e6de7021d)) + +# [5.31.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.2...v5.31.0-dev.3) (2025-07-04) + + +### Bug Fixes + +* **Spotify:** Remove other ads type from the browse screen ([#5333](https://github.com/ReVanced/revanced-patches/issues/5333)) ([c68533a](https://github.com/ReVanced/revanced-patches/commit/c68533a33a399ca813380b5c9ccddce434ceadf8)) + +# [5.31.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.31.0-dev.1...v5.31.0-dev.2) (2025-07-04) + + +### Features + +* **YouTube - Hide layout components:** Add `Hide channel links preview` and `Hide 'Visit Community' button` in channel page ([#5320](https://github.com/ReVanced/revanced-patches/issues/5320)) ([9d9cce3](https://github.com/ReVanced/revanced-patches/commit/9d9cce3ec5550b2fea88df745f1700bb2f17eb9e)) + +# [5.31.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.30.0...v5.31.0-dev.1) (2025-07-04) + + +### Bug Fixes + +* **SoundCloud:** Constrain patches to last working app target ([e8ea89f](https://github.com/ReVanced/revanced-patches/commit/e8ea89fc1a3f0531a0af7529663f13328aca4fe7)) + + +### Features + +* **Lightroom:** Constrain patches to last working version ([#5335](https://github.com/ReVanced/revanced-patches/issues/5335)) ([32ce70e](https://github.com/ReVanced/revanced-patches/commit/32ce70e994f354b9a569376bb89eb38b3190e6f9)) + +# [5.30.0](https://github.com/ReVanced/revanced-patches/compare/v5.29.0...v5.30.0) (2025-07-02) + + +### Bug Fixes + +* **Spotify - Spoof client patch:** Block sending bad integrity verdicts to potentially fix account suspensions ([#5274](https://github.com/ReVanced/revanced-patches/issues/5274)) ([f7b574c](https://github.com/ReVanced/revanced-patches/commit/f7b574ca79c5a616cfe33a3fc75bd8cf68571f7d)) +* **Spotify - Spoof client:** Handle remaining edge cases to obtain a session ([#5285](https://github.com/ReVanced/revanced-patches/issues/5285)) ([2bb2d59](https://github.com/ReVanced/revanced-patches/commit/2bb2d594936093774e232ad8b274c81e805c5bf6)) +* **Spotify - Spoof client:** Skip native login screens ([#5228](https://github.com/ReVanced/revanced-patches/issues/5228)) ([c5ebc63](https://github.com/ReVanced/revanced-patches/commit/c5ebc6336ed17cc9cc7f1348282a2aa3c173fb95)) +* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([73fd832](https://github.com/ReVanced/revanced-patches/commit/73fd83222e089a5fd6e1526e5c12f5a1e9893a35)) +* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions by simplifying fingerprint ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([dad0ff4](https://github.com/ReVanced/revanced-patches/commit/dad0ff4fba74c2b020fbde6c6d5eb66e10e6f1f7)) +* **Spotify:** Add `Spoof client` patch to fix various issues by using a web platform access token ([#5173](https://github.com/ReVanced/revanced-patches/issues/5173)) ([b7b75bb](https://github.com/ReVanced/revanced-patches/commit/b7b75bb9d8d5fd505121e752b8a20e61ff28d1b2)) +* **YouTube - Hide ads:** Fix "Hide shopping links" ([#5267](https://github.com/ReVanced/revanced-patches/issues/5267)) ([2fe4607](https://github.com/ReVanced/revanced-patches/commit/2fe46079d78ab98076d3a4cdf01c8bfdbdea45c0)) +* **YouTube - Hide layout components:** Fix "Hide AI Comments summary" in Comments ([#5284](https://github.com/ReVanced/revanced-patches/issues/5284)) ([d42370e](https://github.com/ReVanced/revanced-patches/commit/d42370ef71f4608abc64b6ef4a3fb0c5bd5e3eb6)) +* **YouTube - Hide layout components:** Fix "Hide AI-generated video summary" in video description ([#5269](https://github.com/ReVanced/revanced-patches/issues/5269)) ([5203da0](https://github.com/ReVanced/revanced-patches/commit/5203da0ae58e467657bc915ab0af5b9904c4f492)) +* **YouTube - Hide layout components:** Fix "Hide ticket shelf" hiding unwanted components ([#5292](https://github.com/ReVanced/revanced-patches/issues/5292)) ([d6b1f7a](https://github.com/ReVanced/revanced-patches/commit/d6b1f7a6e18b1c0eb4374c5e22a1c746dcb3a522)) +* **YouTube - Hide Shorts components:** Fix hiding of untoggled components ([#5266](https://github.com/ReVanced/revanced-patches/issues/5266)) ([008e192](https://github.com/ReVanced/revanced-patches/commit/008e192779a8658e894d5718baa732717bf96e40)) +* **YouTube - SponsorBlock:** Do not show undo skip if PiP is active ([#5314](https://github.com/ReVanced/revanced-patches/issues/5314)) ([18af8de](https://github.com/ReVanced/revanced-patches/commit/18af8dead2c6c7f0d99cd75b69948240e0bcd12c)) + + +### Features + +* **Spotify:** Remove ads section from browse ([#5193](https://github.com/ReVanced/revanced-patches/issues/5193)) ([ebd4dcc](https://github.com/ReVanced/revanced-patches/commit/ebd4dccf12a5fbd31d2d53c19a792c389a4641d7)) +* **YouTube - Hide layout components:** Add `Hide in history` option to filter bar ([#5271](https://github.com/ReVanced/revanced-patches/issues/5271)) ([ba242a3](https://github.com/ReVanced/revanced-patches/commit/ba242a36b040b82e84870e5e240734637125a472)) +* **YouTube - SponsorBlock:** Add "Undo automatic skip toast" ([#5277](https://github.com/ReVanced/revanced-patches/issues/5277)) ([7fa169a](https://github.com/ReVanced/revanced-patches/commit/7fa169ae262c880019c5a069a2d6bdc7f94885f1)) + +# [5.30.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.9...v5.30.0-dev.10) (2025-07-02) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions by simplifying fingerprint ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([dad0ff4](https://github.com/ReVanced/revanced-patches/commit/dad0ff4fba74c2b020fbde6c6d5eb66e10e6f1f7)) + +# [5.30.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.8...v5.30.0-dev.9) (2025-07-02) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Fix hiding context menu ads on newest versions ([#5318](https://github.com/ReVanced/revanced-patches/issues/5318)) ([73fd832](https://github.com/ReVanced/revanced-patches/commit/73fd83222e089a5fd6e1526e5c12f5a1e9893a35)) + +# [5.30.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.7...v5.30.0-dev.8) (2025-07-02) + + +### Bug Fixes + +* **Spotify - Spoof client:** Skip native login screens ([#5228](https://github.com/ReVanced/revanced-patches/issues/5228)) ([c5ebc63](https://github.com/ReVanced/revanced-patches/commit/c5ebc6336ed17cc9cc7f1348282a2aa3c173fb95)) + +# [5.30.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.6...v5.30.0-dev.7) (2025-07-01) + + +### Bug Fixes + +* **Spotify - Spoof client:** Handle remaining edge cases to obtain a session ([#5285](https://github.com/ReVanced/revanced-patches/issues/5285)) ([2bb2d59](https://github.com/ReVanced/revanced-patches/commit/2bb2d594936093774e232ad8b274c81e805c5bf6)) + +# [5.30.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.5...v5.30.0-dev.6) (2025-07-01) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Do not show undo skip if PiP is active ([#5314](https://github.com/ReVanced/revanced-patches/issues/5314)) ([18af8de](https://github.com/ReVanced/revanced-patches/commit/18af8dead2c6c7f0d99cd75b69948240e0bcd12c)) + +# [5.30.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.4...v5.30.0-dev.5) (2025-06-30) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix "Hide ticket shelf" hiding unwanted components ([#5292](https://github.com/ReVanced/revanced-patches/issues/5292)) ([d6b1f7a](https://github.com/ReVanced/revanced-patches/commit/d6b1f7a6e18b1c0eb4374c5e22a1c746dcb3a522)) + +# [5.30.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.3...v5.30.0-dev.4) (2025-06-30) + + +### Features + +* **YouTube - SponsorBlock:** Add "Undo automatic skip toast" ([#5277](https://github.com/ReVanced/revanced-patches/issues/5277)) ([7fa169a](https://github.com/ReVanced/revanced-patches/commit/7fa169ae262c880019c5a069a2d6bdc7f94885f1)) + +# [5.30.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.2...v5.30.0-dev.3) (2025-06-28) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix "Hide AI Comments summary" in Comments ([#5284](https://github.com/ReVanced/revanced-patches/issues/5284)) ([d42370e](https://github.com/ReVanced/revanced-patches/commit/d42370ef71f4608abc64b6ef4a3fb0c5bd5e3eb6)) + +# [5.30.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.30.0-dev.1...v5.30.0-dev.2) (2025-06-27) + + +### Bug Fixes + +* **Spotify - Spoof client patch:** Block sending bad integrity verdicts to potentially fix account suspensions ([#5274](https://github.com/ReVanced/revanced-patches/issues/5274)) ([f7b574c](https://github.com/ReVanced/revanced-patches/commit/f7b574ca79c5a616cfe33a3fc75bd8cf68571f7d)) + +# [5.30.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.1-dev.1...v5.30.0-dev.1) (2025-06-27) + + +### Bug Fixes + +* **YouTube - Hide ads:** Fix "Hide shopping links" ([#5267](https://github.com/ReVanced/revanced-patches/issues/5267)) ([2fe4607](https://github.com/ReVanced/revanced-patches/commit/2fe46079d78ab98076d3a4cdf01c8bfdbdea45c0)) +* **YouTube - Hide layout components:** Fix "Hide AI-generated video summary" in video description ([#5269](https://github.com/ReVanced/revanced-patches/issues/5269)) ([5203da0](https://github.com/ReVanced/revanced-patches/commit/5203da0ae58e467657bc915ab0af5b9904c4f492)) +* **YouTube - Hide Shorts components:** Fix hiding of untoggled components ([#5266](https://github.com/ReVanced/revanced-patches/issues/5266)) ([008e192](https://github.com/ReVanced/revanced-patches/commit/008e192779a8658e894d5718baa732717bf96e40)) + + +### Features + +* **Spotify:** Remove ads section from browse ([#5193](https://github.com/ReVanced/revanced-patches/issues/5193)) ([ebd4dcc](https://github.com/ReVanced/revanced-patches/commit/ebd4dccf12a5fbd31d2d53c19a792c389a4641d7)) +* **YouTube - Hide layout components:** Add `Hide in history` option to filter bar ([#5271](https://github.com/ReVanced/revanced-patches/issues/5271)) ([ba242a3](https://github.com/ReVanced/revanced-patches/commit/ba242a36b040b82e84870e5e240734637125a472)) + +## [5.29.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.29.0...v5.29.1-dev.1) (2025-06-26) + + +### Bug Fixes + +* **Spotify:** Add `Spoof client` patch to fix various issues by using a web platform access token ([#5173](https://github.com/ReVanced/revanced-patches/issues/5173)) ([b7b75bb](https://github.com/ReVanced/revanced-patches/commit/b7b75bb9d8d5fd505121e752b8a20e61ff28d1b2)) + +# [5.29.0](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.29.0) (2025-06-26) + + +### Bug Fixes + +* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([e6876d5](https://github.com/ReVanced/revanced-patches/commit/e6876d510d28f6a3a41ec1722a033b3e30a22c65)) +* **Google Photos:** Resolve startup crash for Android 5.0 devices ([0294533](https://github.com/ReVanced/revanced-patches/commit/0294533c4d9a321aea086eedb4e46385ae9a026e)) +* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([1e2efad](https://github.com/ReVanced/revanced-patches/commit/1e2efad7b2714c395ed6b0a77cbbf8a2265df520)) +* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([2f22d45](https://github.com/ReVanced/revanced-patches/commit/2f22d45eb80745ac64fbea44c8055ebe7925a586)) +* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([5d6ec9e](https://github.com/ReVanced/revanced-patches/commit/5d6ec9e94a6221a0f32762d5bede893e9e7457fc)) +* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([b399ecb](https://github.com/ReVanced/revanced-patches/commit/b399ecbb6a222d82dd5e4b3417c9f7eff4324adb)) +* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([1f539b1](https://github.com/ReVanced/revanced-patches/commit/1f539b1396526b2c767d77a804bd0308ee4a42ec)) +* **YouTube:** Fix refactoring app startup exception ([1b00c90](https://github.com/ReVanced/revanced-patches/commit/1b00c907f4b90f4659afb4a54ba61ac2835b460d)) + + +### Features + +* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([78b25aa](https://github.com/ReVanced/revanced-patches/commit/78b25aa4e87ec3f9df1d57831b48a39029969416)) +* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([83ccfa8](https://github.com/ReVanced/revanced-patches/commit/83ccfa8e1b5d5a44c55ef659484acf3cc08d3346)) +* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([46b4398](https://github.com/ReVanced/revanced-patches/commit/46b4398fd6ca223391ed8f497a8347c2313421b7)) +* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([240897a](https://github.com/ReVanced/revanced-patches/commit/240897a94008ce9a148c87bb41b978d553d5a6f5)) +* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([274dcc6](https://github.com/ReVanced/revanced-patches/commit/274dcc676e009be63eb6970de33abacd34dc6560)) +* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([c68cde3](https://github.com/ReVanced/revanced-patches/commit/c68cde3a896450874cc571be5c4723387db96032)) +* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([d284c3d](https://github.com/ReVanced/revanced-patches/commit/d284c3dd3277430b6885e7c27ee02d062dcefc85)) + +# [5.29.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.10...v5.29.0-dev.11) (2025-06-26) + + +### Features + +* **Cricbuzz:** Add `Hide ads` patch ([#4998](https://github.com/ReVanced/revanced-patches/issues/4998)) ([83ccfa8](https://github.com/ReVanced/revanced-patches/commit/83ccfa8e1b5d5a44c55ef659484acf3cc08d3346)) + +# [5.29.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.9...v5.29.0-dev.10) (2025-06-25) + + +### Features + +* **YouTube - Hide Shorts components:** Add `Hide Effects button` ([#5255](https://github.com/ReVanced/revanced-patches/issues/5255)) ([240897a](https://github.com/ReVanced/revanced-patches/commit/240897a94008ce9a148c87bb41b978d553d5a6f5)) + +# [5.29.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.8...v5.29.0-dev.9) (2025-06-25) + + +### Features + +* Add `Spoof app signature` patch ([#5158](https://github.com/ReVanced/revanced-patches/issues/5158)) ([78b25aa](https://github.com/ReVanced/revanced-patches/commit/78b25aa4e87ec3f9df1d57831b48a39029969416)) + +# [5.29.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.7...v5.29.0-dev.8) (2025-06-25) + + +### Features + +* **YouTube:** Support version `20.13.41` ([#5253](https://github.com/ReVanced/revanced-patches/issues/5253)) ([d284c3d](https://github.com/ReVanced/revanced-patches/commit/d284c3dd3277430b6885e7c27ee02d062dcefc85)) + +# [5.29.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.6...v5.29.0-dev.7) (2025-06-24) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix "Hide video description attributes" ([#5250](https://github.com/ReVanced/revanced-patches/issues/5250)) ([2f22d45](https://github.com/ReVanced/revanced-patches/commit/2f22d45eb80745ac64fbea44c8055ebe7925a586)) +* **YouTube - Hide Shorts components:** Fix "Hide Use this template button" ([#5249](https://github.com/ReVanced/revanced-patches/issues/5249)) ([b399ecb](https://github.com/ReVanced/revanced-patches/commit/b399ecbb6a222d82dd5e4b3417c9f7eff4324adb)) + +# [5.29.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.5...v5.29.0-dev.6) (2025-06-24) + + +### Features + +* **YouTube - Hide video action buttons:** Add `Hide Stop ads` ([#5245](https://github.com/ReVanced/revanced-patches/issues/5245)) ([274dcc6](https://github.com/ReVanced/revanced-patches/commit/274dcc676e009be63eb6970de33abacd34dc6560)) + +# [5.29.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.4...v5.29.0-dev.5) (2025-06-23) + + +### Bug Fixes + +* **Google Photos:** Resolve startup crash for Android 5.0 devices ([0294533](https://github.com/ReVanced/revanced-patches/commit/0294533c4d9a321aea086eedb4e46385ae9a026e)) + +# [5.29.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.3...v5.29.0-dev.4) (2025-06-23) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([5d6ec9e](https://github.com/ReVanced/revanced-patches/commit/5d6ec9e94a6221a0f32762d5bede893e9e7457fc)) + +# [5.29.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.2...v5.29.0-dev.3) (2025-06-23) + + +### Bug Fixes + +* **YouTube:** Fix refactoring app startup exception ([1b00c90](https://github.com/ReVanced/revanced-patches/commit/1b00c907f4b90f4659afb4a54ba61ac2835b460d)) + +# [5.29.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.1...v5.29.0-dev.2) (2025-06-23) + + +### Features + +* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([46b4398](https://github.com/ReVanced/revanced-patches/commit/46b4398fd6ca223391ed8f497a8347c2313421b7)) + +# [5.29.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.2...v5.29.0-dev.1) (2025-06-23) + + +### Bug Fixes + +* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([1f539b1](https://github.com/ReVanced/revanced-patches/commit/1f539b1396526b2c767d77a804bd0308ee4a42ec)) + + +### Features + +* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([c68cde3](https://github.com/ReVanced/revanced-patches/commit/c68cde3a896450874cc571be5c4723387db96032)) + +## [5.28.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.1...v5.28.1-dev.2) (2025-06-23) + + +### Bug Fixes + +* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([1e2efad](https://github.com/ReVanced/revanced-patches/commit/1e2efad7b2714c395ed6b0a77cbbf8a2265df520)) + +## [5.28.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.28.1-dev.1) (2025-06-22) + + +### Bug Fixes + +* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([e6876d5](https://github.com/ReVanced/revanced-patches/commit/e6876d510d28f6a3a41ec1722a033b3e30a22c65)) + +# [5.28.0](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0) (2025-06-20) + + +### Bug Fixes + +* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59)) +* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([4ad4887](https://github.com/ReVanced/revanced-patches/commit/4ad488744d87543c31e453dc7b6d8182b3a7f440)) +* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([ca83047](https://github.com/ReVanced/revanced-patches/commit/ca83047f5c4acbb267d5b98db80ad111999086e0)) +* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2)) +* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([3558c44](https://github.com/ReVanced/revanced-patches/commit/3558c44a05c13f19fefdbbf14b364181a79f17c0)) +* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([c9e54e1](https://github.com/ReVanced/revanced-patches/commit/c9e54e1d36243945ac1ec3108fe38edf0e15d772)) + + +### Features + +* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd)) +* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([312b6dc](https://github.com/ReVanced/revanced-patches/commit/312b6dc04e01c2758cd304ca8606306027aa2f01)) + +# [5.28.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.7...v5.28.0-dev.8) (2025-06-19) + + +### Bug Fixes + +* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([4ad4887](https://github.com/ReVanced/revanced-patches/commit/4ad488744d87543c31e453dc7b6d8182b3a7f440)) + +# [5.28.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.6...v5.28.0-dev.7) (2025-06-18) + + +### Bug Fixes + +* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([c9e54e1](https://github.com/ReVanced/revanced-patches/commit/c9e54e1d36243945ac1ec3108fe38edf0e15d772)) + +# [5.28.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.5...v5.28.0-dev.6) (2025-06-17) + + +### Bug Fixes + +* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([3558c44](https://github.com/ReVanced/revanced-patches/commit/3558c44a05c13f19fefdbbf14b364181a79f17c0)) + +# [5.28.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.4...v5.28.0-dev.5) (2025-06-17) + + +### Bug Fixes + +* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([ca83047](https://github.com/ReVanced/revanced-patches/commit/ca83047f5c4acbb267d5b98db80ad111999086e0)) + +# [5.28.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.3...v5.28.0-dev.4) (2025-06-13) + + +### Features + +* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([312b6dc](https://github.com/ReVanced/revanced-patches/commit/312b6dc04e01c2758cd304ca8606306027aa2f01)) + +# [5.28.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.2...v5.28.0-dev.3) (2025-06-11) + + +### Bug Fixes + +* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2)) + +# [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11) + + +### Bug Fixes + +* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59)) + +# [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11) + + +### Features + +* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd)) + +# [5.27.0](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.27.0) (2025-06-09) + + +### Bug Fixes + +* **Bandcamp - Remove play limits:** Support latest app version ([#5124](https://github.com/ReVanced/revanced-patches/issues/5124)) ([863e92b](https://github.com/ReVanced/revanced-patches/commit/863e92b20ad6682f10524e475ed18f879048ecae)) +* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([0923600](https://github.com/ReVanced/revanced-patches/commit/0923600739a126329fc62100b500216860d7005e)) +* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([8afbef0](https://github.com/ReVanced/revanced-patches/commit/8afbef01343c1e3e6e7e4a4cec6319aebfa4b11c)) +* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([9a6a639](https://github.com/ReVanced/revanced-patches/commit/9a6a639c4905b00d6dffb0923c839c8e3ae54d0c)) +* **YouTube - Hide Shorts components:** Disable A/B player flags that prevents hiding buttons ([bef0dac](https://github.com/ReVanced/revanced-patches/commit/bef0dacac54caf1ca9511d7bc19b19140ccb4eaf)) +* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([3113cd6](https://github.com/ReVanced/revanced-patches/commit/3113cd6d092952c8657454452f34c0ae85358ec9)) + + +### Features + +* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([92311b8](https://github.com/ReVanced/revanced-patches/commit/92311b8e5675f3d4b80ed690d34b699fb847e3cd)) +* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([328d232](https://github.com/ReVanced/revanced-patches/commit/328d232fe77406fa93a14768fc66e7b998506fba)) +* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([9175b23](https://github.com/ReVanced/revanced-patches/commit/9175b23e8360d13c8c1c9c8602ca0b5931d13627)) +* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([dd8afa2](https://github.com/ReVanced/revanced-patches/commit/dd8afa2b07b50be24d764c0f6ddc9e1bbdb91bf1)) +* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([ac6b916](https://github.com/ReVanced/revanced-patches/commit/ac6b916c0c212167c4645e2110500dc811b3e54a)) +* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([42db0c2](https://github.com/ReVanced/revanced-patches/commit/42db0c2e36fefccdbeaa072edcec48b1e05b6270)) + +# [5.27.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.8...v5.27.0-dev.9) (2025-06-09) + + +### Features + +* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([9175b23](https://github.com/ReVanced/revanced-patches/commit/9175b23e8360d13c8c1c9c8602ca0b5931d13627)) + +# [5.27.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.7...v5.27.0-dev.8) (2025-06-09) + + +### Features + +* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([92311b8](https://github.com/ReVanced/revanced-patches/commit/92311b8e5675f3d4b80ed690d34b699fb847e3cd)) + +# [5.27.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.6...v5.27.0-dev.7) (2025-06-08) + + +### Features + +* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([dd8afa2](https://github.com/ReVanced/revanced-patches/commit/dd8afa2b07b50be24d764c0f6ddc9e1bbdb91bf1)) + +# [5.27.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.5...v5.27.0-dev.6) (2025-06-08) + + +### Features + +* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([ac6b916](https://github.com/ReVanced/revanced-patches/commit/ac6b916c0c212167c4645e2110500dc811b3e54a)) + +# [5.27.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.4...v5.27.0-dev.5) (2025-06-08) + + +### Features + +* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([328d232](https://github.com/ReVanced/revanced-patches/commit/328d232fe77406fa93a14768fc66e7b998506fba)) + +# [5.27.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.3...v5.27.0-dev.4) (2025-06-06) + + +### Bug Fixes + +* **Bandcamp - Remove play limits:** Support latest app version ([#5124](https://github.com/ReVanced/revanced-patches/issues/5124)) ([863e92b](https://github.com/ReVanced/revanced-patches/commit/863e92b20ad6682f10524e475ed18f879048ecae)) + +# [5.27.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.2...v5.27.0-dev.3) (2025-06-06) + + +### Bug Fixes + +* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([0923600](https://github.com/ReVanced/revanced-patches/commit/0923600739a126329fc62100b500216860d7005e)) + +# [5.27.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.1...v5.27.0-dev.2) (2025-06-06) + + +### Bug Fixes + +* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([3113cd6](https://github.com/ReVanced/revanced-patches/commit/3113cd6d092952c8657454452f34c0ae85358ec9)) + +# [5.27.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.3...v5.27.0-dev.1) (2025-06-05) + + +### Features + +* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([42db0c2](https://github.com/ReVanced/revanced-patches/commit/42db0c2e36fefccdbeaa072edcec48b1e05b6270)) + +## [5.26.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.2...v5.26.1-dev.3) (2025-06-05) + + +### Bug Fixes + +* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([8afbef0](https://github.com/ReVanced/revanced-patches/commit/8afbef01343c1e3e6e7e4a4cec6319aebfa4b11c)) + +## [5.26.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.1...v5.26.1-dev.2) (2025-06-05) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([9a6a639](https://github.com/ReVanced/revanced-patches/commit/9a6a639c4905b00d6dffb0923c839c8e3ae54d0c)) + +## [5.26.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.26.1-dev.1) (2025-06-05) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Disable A/B player flags that prevents hiding buttons ([bef0dac](https://github.com/ReVanced/revanced-patches/commit/bef0dacac54caf1ca9511d7bc19b19140ccb4eaf)) + +# [5.26.0](https://github.com/ReVanced/revanced-patches/compare/v5.25.0...v5.26.0) (2025-06-04) + + +### Bug Fixes + +* **Spotify - Custom theme:** Apply accent color in more places ([#5039](https://github.com/ReVanced/revanced-patches/issues/5039)) ([9357887](https://github.com/ReVanced/revanced-patches/commit/9357887b6fca7aaf34dfb0163645b6a998e1db76)) +* **YouTube - Hide Shorts components:** Disable A/B player that prevents hiding buttons ([#5104](https://github.com/ReVanced/revanced-patches/issues/5104)) ([835b7bd](https://github.com/ReVanced/revanced-patches/commit/835b7bd7bd667abd632822c98898972e5124dbb6)) +* **YouTube:** Support A/B Shorts layout for RYD and component hiding ([#5081](https://github.com/ReVanced/revanced-patches/issues/5081)) ([8ecacaa](https://github.com/ReVanced/revanced-patches/commit/8ecacaad27162d9380014a9a13ac9220b12257b2)) + + +### Features + +* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([b0440ad](https://github.com/ReVanced/revanced-patches/commit/b0440ad6af0e190e516974ce896dcc54c8d2e122)) +* **Spotify:** Add `Hide Create button` patch ([#5062](https://github.com/ReVanced/revanced-patches/issues/5062)) ([3201681](https://github.com/ReVanced/revanced-patches/commit/32016819d2adbdfdd5e028941d56feda36d20b00)) +* **Sync for Reddit:** Add `Fix post thumbnails` patch ([e1ec30c](https://github.com/ReVanced/revanced-patches/commit/e1ec30c5b07560a39d7b8ab293b0c1f39fd59ef2)) +* **YouTube - Hide Shorts components:** Add option to hide comment panel ([#5102](https://github.com/ReVanced/revanced-patches/issues/5102)) ([22b9bee](https://github.com/ReVanced/revanced-patches/commit/22b9beedd3243a8d6a5635f591b91cdcf307be37)) +* **YouTube - Playback Speed:** Use modern custom speed dialog ([#5069](https://github.com/ReVanced/revanced-patches/issues/5069)) ([9a1e6ca](https://github.com/ReVanced/revanced-patches/commit/9a1e6ca178d9833ee2c681fb130b9290a4e89cd8)) + +# [5.26.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.7...v5.26.0-dev.8) (2025-06-04) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Disable A/B player that prevents hiding buttons ([#5104](https://github.com/ReVanced/revanced-patches/issues/5104)) ([835b7bd](https://github.com/ReVanced/revanced-patches/commit/835b7bd7bd667abd632822c98898972e5124dbb6)) + +# [5.26.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.6...v5.26.0-dev.7) (2025-06-04) + + +### Features + +* **YouTube - Hide Shorts components:** Add option to hide comment panel ([#5102](https://github.com/ReVanced/revanced-patches/issues/5102)) ([22b9bee](https://github.com/ReVanced/revanced-patches/commit/22b9beedd3243a8d6a5635f591b91cdcf307be37)) + +# [5.26.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.5...v5.26.0-dev.6) (2025-06-03) + + +### Features + +* **Sync for Reddit:** Add `Fix post thumbnails` patch ([e1ec30c](https://github.com/ReVanced/revanced-patches/commit/e1ec30c5b07560a39d7b8ab293b0c1f39fd59ef2)) + +# [5.26.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.4...v5.26.0-dev.5) (2025-06-03) + + +### Bug Fixes + +* **Spotify - Custom theme:** Apply accent color in more places ([#5039](https://github.com/ReVanced/revanced-patches/issues/5039)) ([9357887](https://github.com/ReVanced/revanced-patches/commit/9357887b6fca7aaf34dfb0163645b6a998e1db76)) + +# [5.26.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.3...v5.26.0-dev.4) (2025-06-03) + + +### Features + +* **Spotify:** Add `Hide Create button` patch ([#5062](https://github.com/ReVanced/revanced-patches/issues/5062)) ([3201681](https://github.com/ReVanced/revanced-patches/commit/32016819d2adbdfdd5e028941d56feda36d20b00)) + +# [5.26.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.2...v5.26.0-dev.3) (2025-06-01) + + +### Features + +* **YouTube - Playback Speed:** Use modern custom speed dialog ([#5069](https://github.com/ReVanced/revanced-patches/issues/5069)) ([9a1e6ca](https://github.com/ReVanced/revanced-patches/commit/9a1e6ca178d9833ee2c681fb130b9290a4e89cd8)) + +# [5.26.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.1...v5.26.0-dev.2) (2025-06-01) + + +### Bug Fixes + +* **YouTube:** Support A/B Shorts layout for RYD and component hiding ([#5081](https://github.com/ReVanced/revanced-patches/issues/5081)) ([8ecacaa](https://github.com/ReVanced/revanced-patches/commit/8ecacaad27162d9380014a9a13ac9220b12257b2)) + +# [5.26.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.25.0...v5.26.0-dev.1) (2025-05-30) + + +### Features + +* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([b0440ad](https://github.com/ReVanced/revanced-patches/commit/b0440ad6af0e190e516974ce896dcc54c8d2e122)) + +# [5.25.0](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0) (2025-05-29) + + +### Bug Fixes + +* **Disable Pairip license check:** Change patch to default off ([74b6a94](https://github.com/ReVanced/revanced-patches/commit/74b6a94577ac3f73b04bd0cce98fb7011a6607fd)) +* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([1abebd5](https://github.com/ReVanced/revanced-patches/commit/1abebd5f3b73250c6638d2d8a274b92ea8268924)) +* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8)) +* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47)) +* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([7686bbe](https://github.com/ReVanced/revanced-patches/commit/7686bbe975644e1e582fa52f166879da5694ed93)) +* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([37e59d2](https://github.com/ReVanced/revanced-patches/commit/37e59d2771528c631dc13e73dac095fec95c6485)) +* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([3384f8d](https://github.com/ReVanced/revanced-patches/commit/3384f8dd0ff2a345f2e387f4ed1570079a83ccb6)) +* **YouTube:** Better handle incorrect duplicate translations ([20abac5](https://github.com/ReVanced/revanced-patches/commit/20abac52121fbecb65d87d0982f3380e1cf4e20e)) +* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([fac6e59](https://github.com/ReVanced/revanced-patches/commit/fac6e59d281e21e57abdcfc899cd1aeb18e5c2b8)) + + +### Features + +* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([42d2c27](https://github.com/ReVanced/revanced-patches/commit/42d2c277982ef63e6ad42d85e46f13c3ab50243c)) +* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([012dff7](https://github.com/ReVanced/revanced-patches/commit/012dff7b6511b9e519ccac96f6713cf1a1b327b4)) +* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf)) +* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5)) +* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e)) +* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a)) +* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16)) +* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a)) +* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7)) + +# [5.25.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.13...v5.25.0-dev.14) (2025-05-29) + + +### Features + +* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e)) + +# [5.25.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.12...v5.25.0-dev.13) (2025-05-28) + + +### Features + +* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf)) + +# [5.25.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.11...v5.25.0-dev.12) (2025-05-28) + + +### Features + +* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a)) + +# [5.25.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.10...v5.25.0-dev.11) (2025-05-27) + + +### Features + +* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a)) +* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7)) + +# [5.25.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.9...v5.25.0-dev.10) (2025-05-27) + + +### Bug Fixes + +* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8)) +* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47)) + +# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26) + + +### Features + +* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5)) +* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16)) + +# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26) + + +### Features + +* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5)) +* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16)) + +# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26) + + +### Features + +* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5)) +* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16)) + +# [5.25.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.7...v5.25.0-dev.8) (2025-05-25) + + +### Bug Fixes + +* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([1abebd5](https://github.com/ReVanced/revanced-patches/commit/1abebd5f3b73250c6638d2d8a274b92ea8268924)) + +# [5.25.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.6...v5.25.0-dev.7) (2025-05-24) + + +### Bug Fixes + +* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([3384f8d](https://github.com/ReVanced/revanced-patches/commit/3384f8dd0ff2a345f2e387f4ed1570079a83ccb6)) + +# [5.25.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.5...v5.25.0-dev.6) (2025-05-23) + + +### Bug Fixes + +* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([fac6e59](https://github.com/ReVanced/revanced-patches/commit/fac6e59d281e21e57abdcfc899cd1aeb18e5c2b8)) + +# [5.25.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.4...v5.25.0-dev.5) (2025-05-22) + + +### Bug Fixes + +* **YouTube:** Better handle incorrect duplicate translations ([20abac5](https://github.com/ReVanced/revanced-patches/commit/20abac52121fbecb65d87d0982f3380e1cf4e20e)) + +# [5.25.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.3...v5.25.0-dev.4) (2025-05-22) + + +### Bug Fixes + +* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([7686bbe](https://github.com/ReVanced/revanced-patches/commit/7686bbe975644e1e582fa52f166879da5694ed93)) + +# [5.25.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.2...v5.25.0-dev.3) (2025-05-22) + + +### Bug Fixes + +* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([37e59d2](https://github.com/ReVanced/revanced-patches/commit/37e59d2771528c631dc13e73dac095fec95c6485)) + +# [5.25.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.1...v5.25.0-dev.2) (2025-05-22) + + +### Bug Fixes + +* **Disable Pairip license check:** Change patch to default off ([74b6a94](https://github.com/ReVanced/revanced-patches/commit/74b6a94577ac3f73b04bd0cce98fb7011a6607fd)) + +# [5.25.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0-dev.1) (2025-05-22) + + +### Features + +* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([42d2c27](https://github.com/ReVanced/revanced-patches/commit/42d2c277982ef63e6ad42d85e46f13c3ab50243c)) +* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([012dff7](https://github.com/ReVanced/revanced-patches/commit/012dff7b6511b9e519ccac96f6713cf1a1b327b4)) + +# [5.24.0](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0) (2025-05-19) + + +### Bug Fixes + +* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5)) +* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d)) +* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf)) +* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b)) + + +### Features + +* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560)) +* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a)) +* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2)) +* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c)) +* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be)) +* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5)) +* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058)) + +# [5.24.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.8...v5.24.0-dev.9) (2025-05-18) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b)) + +# [5.24.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.7...v5.24.0-dev.8) (2025-05-17) + + +### Bug Fixes + +* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf)) + +# [5.24.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.6...v5.24.0-dev.7) (2025-05-17) + + +### Features + +* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be)) + +# [5.24.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.5...v5.24.0-dev.6) (2025-05-17) + + +### Features + +* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c)) + +# [5.24.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.4...v5.24.0-dev.5) (2025-05-17) + + +### Bug Fixes + +* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5)) + + +### Features + +* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058)) + +# [5.24.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.3...v5.24.0-dev.4) (2025-05-16) + + +### Features + +* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2)) + +# [5.24.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.2...v5.24.0-dev.3) (2025-05-14) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d)) + +# [5.24.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.1...v5.24.0-dev.2) (2025-05-14) + + +### Features + +* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560)) +* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5)) + +# [5.24.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0-dev.1) (2025-05-12) + + +### Features + +* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a)) + +# [5.23.0](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0) (2025-05-10) + + +### Bug Fixes + +* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa)) +* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e)) +* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0)) +* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([7b43986](https://github.com/ReVanced/revanced-patches/commit/7b43986871a68e5cb43331d2fb2fdb9ef67438ad)) +* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([bd53955](https://github.com/ReVanced/revanced-patches/commit/bd53955df738bb7b819eb91a3e776e9d2ca5c74a)) + + +### Features + +* **Lightroom:** Constrain patches to last working version ([efef03b](https://github.com/ReVanced/revanced-patches/commit/efef03b80da21552d0d8be6913faba64e4fb5ed1)) +* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575)) +* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10)) +* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([2e3511d](https://github.com/ReVanced/revanced-patches/commit/2e3511d03c8198bbdb9336888df038a33fb3ab8c)) + +# [5.23.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.6...v5.23.0-dev.7) (2025-05-06) + + +### Bug Fixes + +* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e)) + +# [5.23.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.5...v5.23.0-dev.6) (2025-05-06) + + +### Bug Fixes + +* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa)) + +# [5.23.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.4...v5.23.0-dev.5) (2025-05-06) + + +### Bug Fixes + +* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0)) + + +### Features + +* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575)) +* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10)) + +# [5.23.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.3...v5.23.0-dev.4) (2025-05-06) + + +### Features + +* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([2e3511d](https://github.com/ReVanced/revanced-patches/commit/2e3511d03c8198bbdb9336888df038a33fb3ab8c)) + +# [5.23.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.2...v5.23.0-dev.3) (2025-05-05) + + +### Bug Fixes + +* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([bd53955](https://github.com/ReVanced/revanced-patches/commit/bd53955df738bb7b819eb91a3e776e9d2ca5c74a)) + +# [5.23.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.1...v5.23.0-dev.2) (2025-05-04) + + +### Bug Fixes + +* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([7b43986](https://github.com/ReVanced/revanced-patches/commit/7b43986871a68e5cb43331d2fb2fdb9ef67438ad)) + +# [5.23.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0-dev.1) (2025-05-02) + + +### Features + +* **Lightroom:** Constrain patches to last working version ([efef03b](https://github.com/ReVanced/revanced-patches/commit/efef03b80da21552d0d8be6913faba64e4fb5ed1)) + +# [5.22.0](https://github.com/ReVanced/revanced-patches/compare/v5.21.0...v5.22.0) (2025-05-01) + + +### Bug Fixes + +* **TikTok - Feed filter:** Hide ads in following feed ([#4844](https://github.com/ReVanced/revanced-patches/issues/4844)) ([c255ac1](https://github.com/ReVanced/revanced-patches/commit/c255ac18e0b2dcf917bd0559876be5a2a81023db)) +* **YouTube - Hide layout components:** Hide new type of community posts ([#4888](https://github.com/ReVanced/revanced-patches/issues/4888)) ([f0c9c35](https://github.com/ReVanced/revanced-patches/commit/f0c9c35778ab43a99149ee5ad0ccfd8aeb09f638)) +* **YouTube - Hide Shorts components:** Hide action buttons A/B button layout ([#4889](https://github.com/ReVanced/revanced-patches/issues/4889)) ([9dcd3d3](https://github.com/ReVanced/revanced-patches/commit/9dcd3d35dddf019547ab6ce431bac7a5a8a4c291)) +* **YouTube - Shorts autoplay:** Fix autoplay with YT 20.12 ([06b35b2](https://github.com/ReVanced/revanced-patches/commit/06b35b2a7d7371915881e8f430c32ce15fa224de)) +* **YouTube - Spoof app version:** Do not hide spoof version in general settings menu ([#4861](https://github.com/ReVanced/revanced-patches/issues/4861)) ([f459c3c](https://github.com/ReVanced/revanced-patches/commit/f459c3c7fae3a1b8addf3354488dcef9f95255cc)) + + +### Features + +* **TikTok - Feed Filter:** Remove TikTok Shop from feed. ([#4851](https://github.com/ReVanced/revanced-patches/issues/4851)) ([f198bec](https://github.com/ReVanced/revanced-patches/commit/f198bece653e3e1adf083129dedb77c1d1a633d7)) +* **YouTube - GmsCore support:** Show troubleshooting in app text if the user recently changed their account details ([#4879](https://github.com/ReVanced/revanced-patches/issues/4879)) ([ab4bdc8](https://github.com/ReVanced/revanced-patches/commit/ab4bdc8a2519cee15f79bf95d89e7ea56ea464ee)) + +# [5.22.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.22.0-dev.3...v5.22.0-dev.4) (2025-04-30) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new type of community posts ([#4888](https://github.com/ReVanced/revanced-patches/issues/4888)) ([f0c9c35](https://github.com/ReVanced/revanced-patches/commit/f0c9c35778ab43a99149ee5ad0ccfd8aeb09f638)) +* **YouTube - Hide Shorts components:** Hide action buttons A/B button layout ([#4889](https://github.com/ReVanced/revanced-patches/issues/4889)) ([9dcd3d3](https://github.com/ReVanced/revanced-patches/commit/9dcd3d35dddf019547ab6ce431bac7a5a8a4c291)) + +# [5.22.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.22.0-dev.2...v5.22.0-dev.3) (2025-04-29) + + +### Features + +* **YouTube - GmsCore support:** Show troubleshooting in app text if the user recently changed their account details ([#4879](https://github.com/ReVanced/revanced-patches/issues/4879)) ([ab4bdc8](https://github.com/ReVanced/revanced-patches/commit/ab4bdc8a2519cee15f79bf95d89e7ea56ea464ee)) + +# [5.22.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.22.0-dev.1...v5.22.0-dev.2) (2025-04-27) + + +### Bug Fixes + +* **YouTube - Shorts autoplay:** Fix autoplay with YT 20.12 ([06b35b2](https://github.com/ReVanced/revanced-patches/commit/06b35b2a7d7371915881e8f430c32ce15fa224de)) + +# [5.22.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.21.0...v5.22.0-dev.1) (2025-04-26) + + +### Bug Fixes + +* **TikTok - Feed filter:** Hide ads in following feed ([#4844](https://github.com/ReVanced/revanced-patches/issues/4844)) ([c255ac1](https://github.com/ReVanced/revanced-patches/commit/c255ac18e0b2dcf917bd0559876be5a2a81023db)) +* **YouTube - Spoof app version:** Do not hide spoof version in general settings menu ([#4861](https://github.com/ReVanced/revanced-patches/issues/4861)) ([f459c3c](https://github.com/ReVanced/revanced-patches/commit/f459c3c7fae3a1b8addf3354488dcef9f95255cc)) + + +### Features + +* **TikTok - Feed Filter:** Remove TikTok Shop from feed. ([#4851](https://github.com/ReVanced/revanced-patches/issues/4851)) ([f198bec](https://github.com/ReVanced/revanced-patches/commit/f198bece653e3e1adf083129dedb77c1d1a633d7)) + +# [5.21.0](https://github.com/ReVanced/revanced-patches/compare/v5.20.1...v5.21.0) (2025-04-25) + + +### Bug Fixes + +* `Hide ADB status` patch ([#4814](https://github.com/ReVanced/revanced-patches/issues/4814)) ([dc89be0](https://github.com/ReVanced/revanced-patches/commit/dc89be0e94880733f862b250d95d4848f02c594d)) +* **GmsCore Support:** Correct the description to refer to the app being patched ([2bbcf9d](https://github.com/ReVanced/revanced-patches/commit/2bbcf9d82ca2f442572a6aa886cc611b0d56ff0a)) +* **Wide search bar:** Fix patching `19.16.39` ([433dbc3](https://github.com/ReVanced/revanced-patches/commit/433dbc3bf81823369e146035c954281e84d3a436)) +* **YouTube - Change start page:** Add option to always override start page on app launch ([#4832](https://github.com/ReVanced/revanced-patches/issues/4832)) ([5062e24](https://github.com/ReVanced/revanced-patches/commit/5062e24433ba38eba397438e8fde32099109d3c3)) +* **YouTube - Disable auto captions:** Correctly hide captions with YT 20.12 ([5ecbe82](https://github.com/ReVanced/revanced-patches/commit/5ecbe823ed5197533328cc37f1de5cd1f048a217)) +* **YouTube - Hide video action buttons:** Add option to hide 'Ask' button ([#4852](https://github.com/ReVanced/revanced-patches/issues/4852)) ([43bcf5a](https://github.com/ReVanced/revanced-patches/commit/43bcf5a098c9008cc11dc7df9680437d5effbb32)) +* **YouTube - Hide video action buttons:** Hide A/B layout buttons ([4db5d3c](https://github.com/ReVanced/revanced-patches/commit/4db5d3c3d5ac04faf70cc07fb309b324d752e7e3)) +* **YouTube - Wide search bar:** Do not force phone layout for tablet devices ([#4827](https://github.com/ReVanced/revanced-patches/issues/4827)) ([0cb38f9](https://github.com/ReVanced/revanced-patches/commit/0cb38f9f367a7fe742d8ca336150049181d637b6)) + + +### Features + +* Add `Hide ADB status` patch ([#4585](https://github.com/ReVanced/revanced-patches/issues/4585)) ([1ea8047](https://github.com/ReVanced/revanced-patches/commit/1ea8047aefdaa358e9af8038923ac54d68a39176)) +* **X / Twitter:** Support version `10.86.0-release.0` ([#4805](https://github.com/ReVanced/revanced-patches/issues/4805)) ([655b390](https://github.com/ReVanced/revanced-patches/commit/655b39043ad77efcb4380de67c3f603666e7bc49)) +* **YouTube - Swipe controls:** Add option for vertical progress bar ([#4811](https://github.com/ReVanced/revanced-patches/issues/4811)) ([ebee07e](https://github.com/ReVanced/revanced-patches/commit/ebee07ec3aba6fd3adbd8e0af30390e197879d89)) +* **YouTube:** Support version `20.12.46` ([#4779](https://github.com/ReVanced/revanced-patches/issues/4779)) ([703359f](https://github.com/ReVanced/revanced-patches/commit/703359f0c16b613c204cf16cf42227b628f664fa)) + +# [5.21.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.11...v5.21.0-dev.12) (2025-04-24) + + +### Bug Fixes + +* **YouTube - Hide video action buttons:** Add option to hide 'Ask' button ([#4852](https://github.com/ReVanced/revanced-patches/issues/4852)) ([43bcf5a](https://github.com/ReVanced/revanced-patches/commit/43bcf5a098c9008cc11dc7df9680437d5effbb32)) + +# [5.21.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.10...v5.21.0-dev.11) (2025-04-24) + + +### Bug Fixes + +* **GmsCore Support:** Correct the description to refer to the app being patched ([2bbcf9d](https://github.com/ReVanced/revanced-patches/commit/2bbcf9d82ca2f442572a6aa886cc611b0d56ff0a)) + +# [5.21.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.9...v5.21.0-dev.10) (2025-04-23) + + +### Features + +* **YouTube - Swipe controls:** Add option for vertical progress bar ([#4811](https://github.com/ReVanced/revanced-patches/issues/4811)) ([ebee07e](https://github.com/ReVanced/revanced-patches/commit/ebee07ec3aba6fd3adbd8e0af30390e197879d89)) + +# [5.21.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.8...v5.21.0-dev.9) (2025-04-21) + + +### Bug Fixes + +* **YouTube - Hide video action buttons:** Hide A/B layout buttons ([4db5d3c](https://github.com/ReVanced/revanced-patches/commit/4db5d3c3d5ac04faf70cc07fb309b324d752e7e3)) + +# [5.21.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.7...v5.21.0-dev.8) (2025-04-20) + + +### Bug Fixes + +* **Wide search bar:** Fix patching `19.16.39` ([433dbc3](https://github.com/ReVanced/revanced-patches/commit/433dbc3bf81823369e146035c954281e84d3a436)) + +# [5.21.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.6...v5.21.0-dev.7) (2025-04-20) + + +### Bug Fixes + +* **YouTube - Change start page:** Add option to always override start page on app launch ([#4832](https://github.com/ReVanced/revanced-patches/issues/4832)) ([5062e24](https://github.com/ReVanced/revanced-patches/commit/5062e24433ba38eba397438e8fde32099109d3c3)) + +# [5.21.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.5...v5.21.0-dev.6) (2025-04-19) + + +### Bug Fixes + +* **YouTube - Wide search bar:** Do not force phone layout for tablet devices ([#4827](https://github.com/ReVanced/revanced-patches/issues/4827)) ([0cb38f9](https://github.com/ReVanced/revanced-patches/commit/0cb38f9f367a7fe742d8ca336150049181d637b6)) + +# [5.21.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.4...v5.21.0-dev.5) (2025-04-18) + + +### Bug Fixes + +* `Hide ADB status` patch ([#4814](https://github.com/ReVanced/revanced-patches/issues/4814)) ([dc89be0](https://github.com/ReVanced/revanced-patches/commit/dc89be0e94880733f862b250d95d4848f02c594d)) + +# [5.21.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.3...v5.21.0-dev.4) (2025-04-17) + + +### Bug Fixes + +* **YouTube - Disable auto captions:** Correctly hide captions with YT 20.12 ([5ecbe82](https://github.com/ReVanced/revanced-patches/commit/5ecbe823ed5197533328cc37f1de5cd1f048a217)) + +# [5.21.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.2...v5.21.0-dev.3) (2025-04-16) + + +### Features + +* **X / Twitter:** Support version `10.86.0-release.0` ([#4805](https://github.com/ReVanced/revanced-patches/issues/4805)) ([655b390](https://github.com/ReVanced/revanced-patches/commit/655b39043ad77efcb4380de67c3f603666e7bc49)) + +# [5.21.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.21.0-dev.1...v5.21.0-dev.2) (2025-04-16) + + +### Features + +* Add `Hide ADB status` patch ([#4585](https://github.com/ReVanced/revanced-patches/issues/4585)) ([1ea8047](https://github.com/ReVanced/revanced-patches/commit/1ea8047aefdaa358e9af8038923ac54d68a39176)) + +# [5.21.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.20.1...v5.21.0-dev.1) (2025-04-16) + + +### Features + +* **YouTube:** Support version `20.12.46` ([#4779](https://github.com/ReVanced/revanced-patches/issues/4779)) ([703359f](https://github.com/ReVanced/revanced-patches/commit/703359f0c16b613c204cf16cf42227b628f664fa)) + +## [5.20.1](https://github.com/ReVanced/revanced-patches/compare/v5.20.0...v5.20.1) (2025-04-15) + + +### Bug Fixes + +* **Spotify - Custom theme:** Support latest app target ([#4800](https://github.com/ReVanced/revanced-patches/issues/4800)) ([03d0eb2](https://github.com/ReVanced/revanced-patches/commit/03d0eb2f8c0f3e48d53bdab38d34057f2020bb65)) + +## [5.20.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.20.0...v5.20.1-dev.1) (2025-04-15) + + +### Bug Fixes + +* **Spotify - Custom theme:** Support latest app target ([#4800](https://github.com/ReVanced/revanced-patches/issues/4800)) ([03d0eb2](https://github.com/ReVanced/revanced-patches/commit/03d0eb2f8c0f3e48d53bdab38d34057f2020bb65)) + +# [5.20.0](https://github.com/ReVanced/revanced-patches/compare/v5.19.1...v5.20.0) (2025-04-15) + + +### Bug Fixes + +* **Duolingo - Hide ads:** Support lastest app release ([#4790](https://github.com/ReVanced/revanced-patches/issues/4790)) ([215fccb](https://github.com/ReVanced/revanced-patches/commit/215fccbaf2fdd54251c46cbda106029eb304996b)) +* **Spotify - Unlock Spotify Premium:** Remove premium restriction for 'Spotify Connect' ([#4782](https://github.com/ReVanced/revanced-patches/issues/4782)) ([50f5b1a](https://github.com/ReVanced/revanced-patches/commit/50f5b1ac54372542d76e87626f00ddefb54da125)) +* **Spotify:** Fix login by replacing `Spoof signature` patch with new `Spoof package info` patch ([#4794](https://github.com/ReVanced/revanced-patches/issues/4794)) ([d639151](https://github.com/ReVanced/revanced-patches/commit/d639151641352ce651037b17fb65bd58953cd51c)) +* **YouTube - Remove background playback restrictions:** Restore PiP button functionality after screen is unlocked ([6837348](https://github.com/ReVanced/revanced-patches/commit/6837348c45156d6743a63fef8b6e045087afbda8)) + + +### Features + +* Add `Set target SDK version 34` patch (Disable edge-to-edge display) ([#4780](https://github.com/ReVanced/revanced-patches/issues/4780)) ([dcf6178](https://github.com/ReVanced/revanced-patches/commit/dcf6178f19f86dd1b57d54c855b8c47b086dd33a)) +* **Spotify - Custom theme:** Add option to use unmodified player background gradient ([#4741](https://github.com/ReVanced/revanced-patches/issues/4741)) ([0ee3693](https://github.com/ReVanced/revanced-patches/commit/0ee36939f43f325afca37119db1cf1af3b63be27)) +* **YouTube - Swipe controls:** Add option to change volume swipe sensitivity (step size) ([#4557](https://github.com/ReVanced/revanced-patches/issues/4557)) ([8957325](https://github.com/ReVanced/revanced-patches/commit/8957325d78eb42e087c4c1ff0abedb2146aa4423)) + +# [5.20.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.20.0-dev.6...v5.20.0-dev.7) (2025-04-15) + + +### Bug Fixes + +* **Spotify:** Fix login by replacing `Spoof signature` patch with new `Spoof package info` patch ([#4794](https://github.com/ReVanced/revanced-patches/issues/4794)) ([d639151](https://github.com/ReVanced/revanced-patches/commit/d639151641352ce651037b17fb65bd58953cd51c)) + +# [5.20.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.20.0-dev.5...v5.20.0-dev.6) (2025-04-15) + + +### Bug Fixes + +* **Duolingo - Hide ads:** Support lastest app release ([#4790](https://github.com/ReVanced/revanced-patches/issues/4790)) ([215fccb](https://github.com/ReVanced/revanced-patches/commit/215fccbaf2fdd54251c46cbda106029eb304996b)) + +# [5.20.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.20.0-dev.4...v5.20.0-dev.5) (2025-04-14) + + +### Features + +* **YouTube - Swipe controls:** Add option to change volume swipe sensitivity (step size) ([#4557](https://github.com/ReVanced/revanced-patches/issues/4557)) ([8957325](https://github.com/ReVanced/revanced-patches/commit/8957325d78eb42e087c4c1ff0abedb2146aa4423)) + +# [5.20.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.20.0-dev.3...v5.20.0-dev.4) (2025-04-14) + + +### Bug Fixes + +* **Spotify - Unlock Spotify Premium:** Remove premium restriction for 'Spotify Connect' ([#4782](https://github.com/ReVanced/revanced-patches/issues/4782)) ([50f5b1a](https://github.com/ReVanced/revanced-patches/commit/50f5b1ac54372542d76e87626f00ddefb54da125)) + +# [5.20.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.20.0-dev.2...v5.20.0-dev.3) (2025-04-13) + + +### Bug Fixes + +* **YouTube - Remove background playback restrictions:** Restore PiP button functionality after screen is unlocked ([6837348](https://github.com/ReVanced/revanced-patches/commit/6837348c45156d6743a63fef8b6e045087afbda8)) + +# [5.20.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.20.0-dev.1...v5.20.0-dev.2) (2025-04-13) + + +### Features + +* **Spotify - Custom theme:** Add option to use unmodified player background gradient ([#4741](https://github.com/ReVanced/revanced-patches/issues/4741)) ([0ee3693](https://github.com/ReVanced/revanced-patches/commit/0ee36939f43f325afca37119db1cf1af3b63be27)) + +# [5.20.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.19.1...v5.20.0-dev.1) (2025-04-13) + + +### Features + +* Add `Set target SDK version 34` patch (Disable edge-to-edge display) ([#4780](https://github.com/ReVanced/revanced-patches/issues/4780)) ([dcf6178](https://github.com/ReVanced/revanced-patches/commit/dcf6178f19f86dd1b57d54c855b8c47b086dd33a)) + +## [5.19.1](https://github.com/ReVanced/revanced-patches/compare/v5.19.0...v5.19.1) (2025-04-12) + + +### Bug Fixes + +* **Google Photos:** Restore patching with ReVanced Manager ([#4773](https://github.com/ReVanced/revanced-patches/issues/4773)) ([3e18e86](https://github.com/ReVanced/revanced-patches/commit/3e18e868bbd9fd0600fe81a7fe8767b4bd89a00e)) +* **Spotify:** Restore patching with ReVanced Manager ([#4769](https://github.com/ReVanced/revanced-patches/issues/4769)) ([89d44da](https://github.com/ReVanced/revanced-patches/commit/89d44da171c3f56f13112d1d82bc4ea4a56c7c06)) + +## [5.19.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.19.1-dev.1...v5.19.1-dev.2) (2025-04-12) + + +### Bug Fixes + +* **Google Photos:** Restore patching with ReVanced Manager ([#4773](https://github.com/ReVanced/revanced-patches/issues/4773)) ([3e18e86](https://github.com/ReVanced/revanced-patches/commit/3e18e868bbd9fd0600fe81a7fe8767b4bd89a00e)) + +## [5.19.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.19.0...v5.19.1-dev.1) (2025-04-12) + + +### Bug Fixes + +* **Spotify:** Restore patching with ReVanced Manager ([#4769](https://github.com/ReVanced/revanced-patches/issues/4769)) ([89d44da](https://github.com/ReVanced/revanced-patches/commit/89d44da171c3f56f13112d1d82bc4ea4a56c7c06)) + +# [5.19.0](https://github.com/ReVanced/revanced-patches/compare/v5.18.0...v5.19.0) (2025-04-12) + + +### Bug Fixes + +* **Google Photos - Restore hidden 'Back up while charging' toggle:** Constrain to last working app target ([#4761](https://github.com/ReVanced/revanced-patches/issues/4761)) ([152bb7c](https://github.com/ReVanced/revanced-patches/commit/152bb7c3ee7cf36bc07460e7a3444631ec540441)) +* **Google Photos:** Remove obsolete non functional patch `Restore hidden 'Back up while charging' toggle` ([#4764](https://github.com/ReVanced/revanced-patches/issues/4764)) ([56e48f4](https://github.com/ReVanced/revanced-patches/commit/56e48f4c89da51f81ff11a79a164eaa5b440690e)) +* **Spotify - Custom theme:** Override more color resources ([#4690](https://github.com/ReVanced/revanced-patches/issues/4690)) ([d7a7a0b](https://github.com/ReVanced/revanced-patches/commit/d7a7a0b982dbafa181b04f984a5f7618fb067c2a)) +* **Spotify - Unlock Spotify Premium:** Remove restrictions for Google voice assistant ([#4702](https://github.com/ReVanced/revanced-patches/issues/4702)) ([106202f](https://github.com/ReVanced/revanced-patches/commit/106202f9ebb7699c4ba4ae46b82133e35f1ac6b9)) +* **Spotify:** Remove ads sections from home ([#4722](https://github.com/ReVanced/revanced-patches/issues/4722)) ([0b9a5e7](https://github.com/ReVanced/revanced-patches/commit/0b9a5e7f89a89d971762b3539166d4f145111481)) +* **Twitter - Hide recommended users:** Make hiding work again by filtering for new entryId prefix ([#4456](https://github.com/ReVanced/revanced-patches/issues/4456)) ([ff846b0](https://github.com/ReVanced/revanced-patches/commit/ff846b0b7ef5060caaffedb08c1f901172f5b2d1)) +* **YouTube - Hide layout components:** Do not hide video description music/game links if hide horizontal shelves is enabled ([3864f35](https://github.com/ReVanced/revanced-patches/commit/3864f3550153617e23ad9979fb543d8a7fb4dc0a)) +* **YouTube - Hide player flyout menu items:** Show more detailed summary text for 'Hide Audio track' if using Android spoof client ([#4756](https://github.com/ReVanced/revanced-patches/issues/4756)) ([b67bbb2](https://github.com/ReVanced/revanced-patches/commit/b67bbb299669336addb68cf52a8ce5b39c68cec0)) +* **YouTube - Remove background playback restrictions:** Do not show media controls when playing Shorts from the feed ([2ed675c](https://github.com/ReVanced/revanced-patches/commit/2ed675cdd058fb5876381a9d30dee5263f6b2e26)) +* **YouTube - Return YouTube Dislike:** Correctly update label after disliking a Short with 20.07 ([0bb3e32](https://github.com/ReVanced/revanced-patches/commit/0bb3e32244fa10809aee5c4e549f77ed4054537e)) +* **YouTube - Return YouTube Dislike:** Fix inconsistent label after disliking a Short ([ea92a2e](https://github.com/ReVanced/revanced-patches/commit/ea92a2e36c7aab3bd115f7d0ec40467179485b32)) +* **YouTube - Seekbar:** Correctly hide the feed seekbar with target 20.07 ([ddc6e4c](https://github.com/ReVanced/revanced-patches/commit/ddc6e4c34fe35fa34bd859bf34e25645a23dbdc9)) +* **YouTube:** Combine multiple seekbar patches into a single patch ([#4705](https://github.com/ReVanced/revanced-patches/issues/4705)) ([503b7eb](https://github.com/ReVanced/revanced-patches/commit/503b7eb8d413ef7f248394f128f3b2a6f3192ba6)) + + +### Features + +* **Angulus:** Add `Hide ads` patch ([#4604](https://github.com/ReVanced/revanced-patches/issues/4604)) ([87c86b5](https://github.com/ReVanced/revanced-patches/commit/87c86b53a91b0054ac892a3f02bbe7bf83bbf813)) +* **Messenger:** Add `Remove Meta AI tab` patch ([#4726](https://github.com/ReVanced/revanced-patches/issues/4726)) ([e3fad97](https://github.com/ReVanced/revanced-patches/commit/e3fad97484d7eb962aeb53d44a0047b34a881071)) +* **Photomath:** Support latest version ([#4672](https://github.com/ReVanced/revanced-patches/issues/4672)) ([8e16483](https://github.com/ReVanced/revanced-patches/commit/8e1648322948151e4565fb0d86e0f37d0a02d73f)) +* **Proton Mail:** Add `Remove 'Sent from' signature` patch ([#4514](https://github.com/ReVanced/revanced-patches/issues/4514)) ([34c14c9](https://github.com/ReVanced/revanced-patches/commit/34c14c9b443092824d035afd77adb678c6f89e3e)) +* **Spotify:** Add `Check environment` patch ([#4765](https://github.com/ReVanced/revanced-patches/issues/4765)) ([6d7101c](https://github.com/ReVanced/revanced-patches/commit/6d7101cb2e546e01a934eff9cad1264367aeafe3)) +* **Spotify:** Add limited support for version `8.6.98.900` (last version that supports Kenwood and Pioneer car stereos) ([#4750](https://github.com/ReVanced/revanced-patches/issues/4750)) ([a3fde87](https://github.com/ReVanced/revanced-patches/commit/a3fde874af993125ba7a741820e7bd48e3641b84)) +* **Strava - Disable subscription suggestions:** Make compatible with latest version ([#4739](https://github.com/ReVanced/revanced-patches/issues/4739)) ([649a2c0](https://github.com/ReVanced/revanced-patches/commit/649a2c06161c72a2040b179dbed5b415847d7527)) +* **YouTube - Settings:** Add icons to the ReVanced settings ([#4496](https://github.com/ReVanced/revanced-patches/issues/4496)) ([d0c85f0](https://github.com/ReVanced/revanced-patches/commit/d0c85f044083d720c63a8ea4ff15d42eefeb9db7)) + +# [5.19.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.16...v5.19.0-dev.17) (2025-04-12) + + +### Features + +* **Spotify:** Add `Check environment` patch ([#4765](https://github.com/ReVanced/revanced-patches/issues/4765)) ([6d7101c](https://github.com/ReVanced/revanced-patches/commit/6d7101cb2e546e01a934eff9cad1264367aeafe3)) + +# [5.19.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.15...v5.19.0-dev.16) (2025-04-11) + + +### Bug Fixes + +* **Google Photos:** Remove obsolete non functional patch `Restore hidden 'Back up while charging' toggle` ([#4764](https://github.com/ReVanced/revanced-patches/issues/4764)) ([56e48f4](https://github.com/ReVanced/revanced-patches/commit/56e48f4c89da51f81ff11a79a164eaa5b440690e)) + +# [5.19.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.14...v5.19.0-dev.15) (2025-04-11) + + +### Bug Fixes + +* **Google Photos - Restore hidden 'Back up while charging' toggle:** Constrain to last working app target ([#4761](https://github.com/ReVanced/revanced-patches/issues/4761)) ([152bb7c](https://github.com/ReVanced/revanced-patches/commit/152bb7c3ee7cf36bc07460e7a3444631ec540441)) + +# [5.19.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.13...v5.19.0-dev.14) (2025-04-11) + + +### Bug Fixes + +* **Spotify - Unlock Spotify Premium:** Remove restrictions for Google voice assistant ([#4702](https://github.com/ReVanced/revanced-patches/issues/4702)) ([106202f](https://github.com/ReVanced/revanced-patches/commit/106202f9ebb7699c4ba4ae46b82133e35f1ac6b9)) + +# [5.19.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.12...v5.19.0-dev.13) (2025-04-11) + + +### Features + +* **Spotify:** Add limited support for version `8.6.98.900` (last version that supports Kenwood and Pioneer car stereos) ([#4750](https://github.com/ReVanced/revanced-patches/issues/4750)) ([a3fde87](https://github.com/ReVanced/revanced-patches/commit/a3fde874af993125ba7a741820e7bd48e3641b84)) + +# [5.19.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.11...v5.19.0-dev.12) (2025-04-11) + + +### Features + +* **Strava - Disable subscription suggestions:** Make compatible with latest version ([#4739](https://github.com/ReVanced/revanced-patches/issues/4739)) ([649a2c0](https://github.com/ReVanced/revanced-patches/commit/649a2c06161c72a2040b179dbed5b415847d7527)) + +# [5.19.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.10...v5.19.0-dev.11) (2025-04-10) + + +### Features + +* **Messenger:** Add `Remove Meta AI tab` patch ([#4726](https://github.com/ReVanced/revanced-patches/issues/4726)) ([e3fad97](https://github.com/ReVanced/revanced-patches/commit/e3fad97484d7eb962aeb53d44a0047b34a881071)) + +# [5.19.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.9...v5.19.0-dev.10) (2025-04-10) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Do not hide video description music/game links if hide horizontal shelves is enabled ([3864f35](https://github.com/ReVanced/revanced-patches/commit/3864f3550153617e23ad9979fb543d8a7fb4dc0a)) + +# [5.19.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.8...v5.19.0-dev.9) (2025-04-10) + + +### Bug Fixes + +* **YouTube - Hide player flyout menu items:** Show more detailed summary text for 'Hide Audio track' if using Android spoof client ([#4756](https://github.com/ReVanced/revanced-patches/issues/4756)) ([b67bbb2](https://github.com/ReVanced/revanced-patches/commit/b67bbb299669336addb68cf52a8ce5b39c68cec0)) + +# [5.19.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.7...v5.19.0-dev.8) (2025-04-09) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Fix inconsistent label after disliking a Short ([ea92a2e](https://github.com/ReVanced/revanced-patches/commit/ea92a2e36c7aab3bd115f7d0ec40467179485b32)) + +# [5.19.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.6...v5.19.0-dev.7) (2025-04-07) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Correctly update label after disliking a Short with 20.07 ([0bb3e32](https://github.com/ReVanced/revanced-patches/commit/0bb3e32244fa10809aee5c4e549f77ed4054537e)) + +# [5.19.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.5...v5.19.0-dev.6) (2025-04-04) + + +### Bug Fixes + +* **Spotify:** Remove ads sections from home ([#4722](https://github.com/ReVanced/revanced-patches/issues/4722)) ([0b9a5e7](https://github.com/ReVanced/revanced-patches/commit/0b9a5e7f89a89d971762b3539166d4f145111481)) + +# [5.19.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.4...v5.19.0-dev.5) (2025-04-02) + + +### Bug Fixes + +* **Spotify - Custom theme:** Override more color resources ([#4690](https://github.com/ReVanced/revanced-patches/issues/4690)) ([d7a7a0b](https://github.com/ReVanced/revanced-patches/commit/d7a7a0b982dbafa181b04f984a5f7618fb067c2a)) + +# [5.19.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.3...v5.19.0-dev.4) (2025-04-02) + + +### Bug Fixes + +* **YouTube - Seekbar:** Correctly hide the feed seekbar with target 20.07 ([ddc6e4c](https://github.com/ReVanced/revanced-patches/commit/ddc6e4c34fe35fa34bd859bf34e25645a23dbdc9)) + +# [5.19.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.2...v5.19.0-dev.3) (2025-04-02) + + +### Features + +* **Proton Mail:** Add `Remove 'Sent from' signature` patch ([#4514](https://github.com/ReVanced/revanced-patches/issues/4514)) ([34c14c9](https://github.com/ReVanced/revanced-patches/commit/34c14c9b443092824d035afd77adb678c6f89e3e)) + +# [5.19.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.19.0-dev.1...v5.19.0-dev.2) (2025-04-02) + + +### Features + +* **YouTube - Settings:** Add icons to the ReVanced settings ([#4496](https://github.com/ReVanced/revanced-patches/issues/4496)) ([d0c85f0](https://github.com/ReVanced/revanced-patches/commit/d0c85f044083d720c63a8ea4ff15d42eefeb9db7)) + +# [5.19.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.18.1-dev.2...v5.19.0-dev.1) (2025-04-01) + + +### Bug Fixes + +* **Twitter - Hide recommended users:** Make hiding work again by filtering for new entryId prefix ([#4456](https://github.com/ReVanced/revanced-patches/issues/4456)) ([ff846b0](https://github.com/ReVanced/revanced-patches/commit/ff846b0b7ef5060caaffedb08c1f901172f5b2d1)) + + +### Features + +* **Angulus:** Add `Hide ads` patch ([#4604](https://github.com/ReVanced/revanced-patches/issues/4604)) ([87c86b5](https://github.com/ReVanced/revanced-patches/commit/87c86b53a91b0054ac892a3f02bbe7bf83bbf813)) +* **Photomath:** Support latest version ([#4672](https://github.com/ReVanced/revanced-patches/issues/4672)) ([8e16483](https://github.com/ReVanced/revanced-patches/commit/8e1648322948151e4565fb0d86e0f37d0a02d73f)) + +## [5.18.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.18.1-dev.1...v5.18.1-dev.2) (2025-04-01) + + +### Bug Fixes + +* **YouTube:** Combine multiple seekbar patches into a single patch ([#4705](https://github.com/ReVanced/revanced-patches/issues/4705)) ([503b7eb](https://github.com/ReVanced/revanced-patches/commit/503b7eb8d413ef7f248394f128f3b2a6f3192ba6)) + +## [5.18.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.18.0...v5.18.1-dev.1) (2025-03-31) + + +### Bug Fixes + +* **YouTube - Remove background playback restrictions:** Do not show media controls when playing Shorts from the feed ([2ed675c](https://github.com/ReVanced/revanced-patches/commit/2ed675cdd058fb5876381a9d30dee5263f6b2e26)) + +# [5.18.0](https://github.com/ReVanced/revanced-patches/compare/v5.17.0...v5.18.0) (2025-03-28) + + +### Bug Fixes + +* **Spotify:** Ignore optional attributes if not present ([#4688](https://github.com/ReVanced/revanced-patches/issues/4688)) ([84f5854](https://github.com/ReVanced/revanced-patches/commit/84f585492e4be3604c6c7680ffb3bebcea5a675f)) + + +### Features + +* **YouTube:** Support version `20.07.39` ([#4677](https://github.com/ReVanced/revanced-patches/issues/4677)) ([c1379f6](https://github.com/ReVanced/revanced-patches/commit/c1379f6e520c683d2c9d6a490a69ca542168b3b3)) + +# [5.18.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.18.0-dev.1...v5.18.0-dev.2) (2025-03-28) + + +### Bug Fixes + +* **Spotify:** Ignore optional attributes if not present ([#4688](https://github.com/ReVanced/revanced-patches/issues/4688)) ([84f5854](https://github.com/ReVanced/revanced-patches/commit/84f585492e4be3604c6c7680ffb3bebcea5a675f)) + +# [5.18.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.17.0...v5.18.0-dev.1) (2025-03-28) + + +### Features + +* **YouTube:** Support version `20.07.39` ([#4677](https://github.com/ReVanced/revanced-patches/issues/4677)) ([c1379f6](https://github.com/ReVanced/revanced-patches/commit/c1379f6e520c683d2c9d6a490a69ca542168b3b3)) + +# [5.17.0](https://github.com/ReVanced/revanced-patches/compare/v5.16.1...v5.17.0) (2025-03-28) + + +### Bug Fixes + +* **Facebook - Hide 'Sponsored Stories':** Constrain patch to latest compatible version ([#4657](https://github.com/ReVanced/revanced-patches/issues/4657)) ([46bd1c8](https://github.com/ReVanced/revanced-patches/commit/46bd1c829acd5f83600025e0ceb7d482ae80be69)) +* **Spotify - Unlock Premium:** Override additional attributes ([#4651](https://github.com/ReVanced/revanced-patches/issues/4651)) ([568b40d](https://github.com/ReVanced/revanced-patches/commit/568b40da9692eae9039bbb3cec513a61ca627c24)) +* **Spotify - Unlock Premium:** Use correct patch description convention ([a486522](https://github.com/ReVanced/revanced-patches/commit/a4865228f8481d2efc8fbf4e90902a03289d9a3f)) +* **X / Twitter:** Constrain patches to latest compatible versions ([#4683](https://github.com/ReVanced/revanced-patches/issues/4683)) ([f579728](https://github.com/ReVanced/revanced-patches/commit/f5797289f45186052537982c7f5db6f2b0769aee)) +* **YouTube - Navigation buttons:** Add user dialog message to 'Disable translucent status bar' ([a4a0e68](https://github.com/ReVanced/revanced-patches/commit/a4a0e6869e23d15ee09262460f4e290c90629eeb)) + + +### Features + +* **Spotify - Unlock Premium:** Disable the "Spotify Premium" upsell experiment in context menus ([9a10ee4](https://github.com/ReVanced/revanced-patches/commit/9a10ee4d22fb53da2012a182e038749d3ad72377)) + +# [5.17.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.17.0-dev.3...v5.17.0-dev.4) (2025-03-28) + + +### Bug Fixes + +* **X / Twitter:** Constrain patches to latest compatible versions ([#4683](https://github.com/ReVanced/revanced-patches/issues/4683)) ([f579728](https://github.com/ReVanced/revanced-patches/commit/f5797289f45186052537982c7f5db6f2b0769aee)) + +# [5.17.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.17.0-dev.2...v5.17.0-dev.3) (2025-03-28) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Override additional attributes ([#4651](https://github.com/ReVanced/revanced-patches/issues/4651)) ([568b40d](https://github.com/ReVanced/revanced-patches/commit/568b40da9692eae9039bbb3cec513a61ca627c24)) + +# [5.17.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.17.0-dev.1...v5.17.0-dev.2) (2025-03-27) + + +### Bug Fixes + +* **YouTube - Navigation buttons:** Add user dialog message to 'Disable translucent status bar' ([a4a0e68](https://github.com/ReVanced/revanced-patches/commit/a4a0e6869e23d15ee09262460f4e290c90629eeb)) + +# [5.17.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.16.2-dev.1...v5.17.0-dev.1) (2025-03-27) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Use correct patch description convention ([a486522](https://github.com/ReVanced/revanced-patches/commit/a4865228f8481d2efc8fbf4e90902a03289d9a3f)) + + +### Features + +* **Spotify - Unlock Premium:** Disable the "Spotify Premium" upsell experiment in context menus ([9a10ee4](https://github.com/ReVanced/revanced-patches/commit/9a10ee4d22fb53da2012a182e038749d3ad72377)) + +## [5.16.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.16.1...v5.16.2-dev.1) (2025-03-26) + + +### Bug Fixes + +* **Facebook - Hide 'Sponsored Stories':** Constrain patch to latest compatible version ([#4657](https://github.com/ReVanced/revanced-patches/issues/4657)) ([46bd1c8](https://github.com/ReVanced/revanced-patches/commit/46bd1c829acd5f83600025e0ceb7d482ae80be69)) + +## [5.16.1](https://github.com/ReVanced/revanced-patches/compare/v5.16.0...v5.16.1) (2025-03-26) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Override streaming attribute attempting to fix streaming issues ([06be36c](https://github.com/ReVanced/revanced-patches/commit/06be36cddf3430b4179dff696b3d15718cd6963b)) + +## [5.16.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.16.0...v5.16.1-dev.1) (2025-03-26) + + +### Bug Fixes + +* **Spotify - Unlock Premium:** Override streaming attribute attempting to fix streaming issues ([06be36c](https://github.com/ReVanced/revanced-patches/commit/06be36cddf3430b4179dff696b3d15718cd6963b)) + +# [5.16.0](https://github.com/ReVanced/revanced-patches/compare/v5.15.0...v5.16.0) (2025-03-26) + + +### Bug Fixes + +* **YouTube - Settings:** System navigation bar is located above the settings ui on Android 15+ ([f7497be](https://github.com/ReVanced/revanced-patches/commit/f7497be2c5e4abcde6eb55b84955124a28f55cae)) + + +### Features + +* **Spotify:** Add `Unlock premium` patch ([#4644](https://github.com/ReVanced/revanced-patches/issues/4644)) ([f048c50](https://github.com/ReVanced/revanced-patches/commit/f048c50e56fc1f5a5c607860be4206ef83b528fe)) +* **YouTube - Comments:** Add `Hide AI Comments summary` ([#4634](https://github.com/ReVanced/revanced-patches/issues/4634)) ([e9b7f26](https://github.com/ReVanced/revanced-patches/commit/e9b7f263f739bd130f6ea79913851a52355977c5)) +* **YouTube - Video description:** Add `Hide AI-generated video summary` ([#4636](https://github.com/ReVanced/revanced-patches/issues/4636)) ([521fd48](https://github.com/ReVanced/revanced-patches/commit/521fd48602432ab436d8711c19d7130b2b05af12)) + +# [5.16.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.16.0-dev.1...v5.16.0-dev.2) (2025-03-26) + + +### Features + +* **Spotify:** Add `Unlock premium` patch ([#4644](https://github.com/ReVanced/revanced-patches/issues/4644)) ([f048c50](https://github.com/ReVanced/revanced-patches/commit/f048c50e56fc1f5a5c607860be4206ef83b528fe)) + +# [5.16.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.15.0...v5.16.0-dev.1) (2025-03-24) + + +### Bug Fixes + +* **YouTube - Settings:** System navigation bar is located above the settings ui on Android 15+ ([f7497be](https://github.com/ReVanced/revanced-patches/commit/f7497be2c5e4abcde6eb55b84955124a28f55cae)) + + +### Features + +* **YouTube - Comments:** Add `Hide AI Comments summary` ([#4634](https://github.com/ReVanced/revanced-patches/issues/4634)) ([e9b7f26](https://github.com/ReVanced/revanced-patches/commit/e9b7f263f739bd130f6ea79913851a52355977c5)) +* **YouTube - Video description:** Add `Hide AI-generated video summary` ([#4636](https://github.com/ReVanced/revanced-patches/issues/4636)) ([521fd48](https://github.com/ReVanced/revanced-patches/commit/521fd48602432ab436d8711c19d7130b2b05af12)) + +# [5.15.0](https://github.com/ReVanced/revanced-patches/compare/v5.14.0...v5.15.0) (2025-03-21) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Change oldest spoof target to 19.01.34 ([5012439](https://github.com/ReVanced/revanced-patches/commit/5012439a8e53b2a4ab5e85c47976e1ab28a51208)) +* **YouTube - Spoof app version:** Remove broken spoof targets that YouTube no longer supports ([#4610](https://github.com/ReVanced/revanced-patches/issues/4610)) ([883fbe7](https://github.com/ReVanced/revanced-patches/commit/883fbe71233c57cb1241e57c122b43f40722acc7)) +* **YouTube:** Do not show restart prompt more than once if setting change is canceled ([49797fe](https://github.com/ReVanced/revanced-patches/commit/49797fe8d0c4a0981ef621a31356c4315ae3777b)) + + +### Features + +* **YouTube - SponsorBlock:** Add opacity setting to category segment colors ([#4582](https://github.com/ReVanced/revanced-patches/issues/4582)) ([6e8ffba](https://github.com/ReVanced/revanced-patches/commit/6e8ffbade9e03658f725622631e44dabf2995861)) + +# [5.15.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.15.0-dev.3...v5.15.0-dev.4) (2025-03-21) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Change oldest spoof target to 19.01.34 ([5012439](https://github.com/ReVanced/revanced-patches/commit/5012439a8e53b2a4ab5e85c47976e1ab28a51208)) + +# [5.15.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.15.0-dev.2...v5.15.0-dev.3) (2025-03-20) + + +### Bug Fixes + +* **YouTube:** Do not show restart prompt more than once if setting change is canceled ([49797fe](https://github.com/ReVanced/revanced-patches/commit/49797fe8d0c4a0981ef621a31356c4315ae3777b)) + +# [5.15.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.15.0-dev.1...v5.15.0-dev.2) (2025-03-19) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Remove broken spoof targets that YouTube no longer supports ([#4610](https://github.com/ReVanced/revanced-patches/issues/4610)) ([883fbe7](https://github.com/ReVanced/revanced-patches/commit/883fbe71233c57cb1241e57c122b43f40722acc7)) + +# [5.15.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.14.0...v5.15.0-dev.1) (2025-03-19) + + +### Features + +* **YouTube - SponsorBlock:** Add opacity setting to category segment colors ([#4582](https://github.com/ReVanced/revanced-patches/issues/4582)) ([6e8ffba](https://github.com/ReVanced/revanced-patches/commit/6e8ffbade9e03658f725622631e44dabf2995861)) + +# [5.14.0](https://github.com/ReVanced/revanced-patches/compare/v5.13.0...v5.14.0) (2025-03-09) + + +### Bug Fixes + +* **Boost for reddit - Client spoof:** Use a different user agent to combat Reddit's API issues ([5d3c817](https://github.com/ReVanced/revanced-patches/commit/5d3c8175b34a3f6ae2732b25db0851773a8c000d)) +* **YouTube - Change form factor:** Restore Automotive form factor watch history menu, channel pages, and community posts ([#4541](https://github.com/ReVanced/revanced-patches/issues/4541)) ([aa5c001](https://github.com/ReVanced/revanced-patches/commit/aa5c001968446e5270c756256724e917009612cd)) +* **YouTube - Hide ads:** Hide new type of buttoned ad ([#4528](https://github.com/ReVanced/revanced-patches/issues/4528)) ([4387a7b](https://github.com/ReVanced/revanced-patches/commit/4387a7b131f49729e902e008bb4cec073635c040)) +* **YouTube - Hide layout components:** Do not hide Movie/Courses start page content if 'Hide horizontal shelves' is enabled ([62a6164](https://github.com/ReVanced/revanced-patches/commit/62a6164b88b64200b517a5ba6b800d8214dbbad8)) +* **YouTube - Theme:** Resolve dark mode startup crash with Android 9.0 ([741c2d5](https://github.com/ReVanced/revanced-patches/commit/741c2d59406f5d602554bb3a3c0b8982f42848b4)) +* **YouTube:** Change language settings menu to use native language names ([#4568](https://github.com/ReVanced/revanced-patches/issues/4568)) ([6f3f8fd](https://github.com/ReVanced/revanced-patches/commit/6f3f8fdce05501e4fa4423c2170a916fbea3b199)) +* **YouTube:** Combine `Restore old video quality menu` and `Remember video quality` into `Video quality` patch ([#4552](https://github.com/ReVanced/revanced-patches/issues/4552)) ([ee67b76](https://github.com/ReVanced/revanced-patches/commit/ee67b763d5c5947a5b1ef4420b1efa820ed6af83)) + + +### Features + +* **Infinity for Reddit:** Add support for package name on IzzyOnDroid ([#4554](https://github.com/ReVanced/revanced-patches/issues/4554)) ([cf9f959](https://github.com/ReVanced/revanced-patches/commit/cf9f959923076c10a7f0a29f6ba277f5a055ec07)) +* **Spotify:** Add `Spoof signature` patch ([#4576](https://github.com/ReVanced/revanced-patches/issues/4576)) ([3646c70](https://github.com/ReVanced/revanced-patches/commit/3646c70556b67a6b7ecf9b86869ebf03c3611333)) +* **YouTube - Remember video quality:** Add separate Shorts default quality settings ([#4543](https://github.com/ReVanced/revanced-patches/issues/4543)) ([88142ab](https://github.com/ReVanced/revanced-patches/commit/88142ab464192b564b1b8d56a6b45663f77f5e00)) + +# [5.14.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.8...v5.14.0-dev.9) (2025-03-09) + + +### Features + +* **Spotify:** Add `Spoof signature` patch ([#4576](https://github.com/ReVanced/revanced-patches/issues/4576)) ([3646c70](https://github.com/ReVanced/revanced-patches/commit/3646c70556b67a6b7ecf9b86869ebf03c3611333)) + +# [5.14.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.7...v5.14.0-dev.8) (2025-03-09) + + +### Bug Fixes + +* **YouTube - Theme:** Resolve dark mode startup crash with Android 9.0 ([741c2d5](https://github.com/ReVanced/revanced-patches/commit/741c2d59406f5d602554bb3a3c0b8982f42848b4)) + +# [5.14.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.6...v5.14.0-dev.7) (2025-03-08) + + +### Bug Fixes + +* **YouTube:** Change language settings menu to use native language names ([#4568](https://github.com/ReVanced/revanced-patches/issues/4568)) ([6f3f8fd](https://github.com/ReVanced/revanced-patches/commit/6f3f8fdce05501e4fa4423c2170a916fbea3b199)) + +# [5.14.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.5...v5.14.0-dev.6) (2025-03-07) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Do not hide Movie/Courses start page content if 'Hide horizontal shelves' is enabled ([62a6164](https://github.com/ReVanced/revanced-patches/commit/62a6164b88b64200b517a5ba6b800d8214dbbad8)) + +# [5.14.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.4...v5.14.0-dev.5) (2025-03-06) + + +### Features + +* **Infinity for Reddit:** Add support for package name on IzzyOnDroid ([#4554](https://github.com/ReVanced/revanced-patches/issues/4554)) ([cf9f959](https://github.com/ReVanced/revanced-patches/commit/cf9f959923076c10a7f0a29f6ba277f5a055ec07)) + +# [5.14.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.3...v5.14.0-dev.4) (2025-03-06) + + +### Bug Fixes + +* **YouTube:** Combine `Restore old video quality menu` and `Remember video quality` into `Video quality` patch ([#4552](https://github.com/ReVanced/revanced-patches/issues/4552)) ([ee67b76](https://github.com/ReVanced/revanced-patches/commit/ee67b763d5c5947a5b1ef4420b1efa820ed6af83)) + +# [5.14.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.2...v5.14.0-dev.3) (2025-03-06) + + +### Bug Fixes + +* **Boost for reddit - Client spoof:** Use a different user agent to combat Reddit's API issues ([5d3c817](https://github.com/ReVanced/revanced-patches/commit/5d3c8175b34a3f6ae2732b25db0851773a8c000d)) + +# [5.14.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.14.0-dev.1...v5.14.0-dev.2) (2025-03-06) + + +### Bug Fixes + +* **YouTube - Hide ads:** Hide new type of buttoned ad ([#4528](https://github.com/ReVanced/revanced-patches/issues/4528)) ([4387a7b](https://github.com/ReVanced/revanced-patches/commit/4387a7b131f49729e902e008bb4cec073635c040)) + +# [5.14.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.13.1-dev.1...v5.14.0-dev.1) (2025-03-06) + + +### Features + +* **YouTube - Remember video quality:** Add separate Shorts default quality settings ([#4543](https://github.com/ReVanced/revanced-patches/issues/4543)) ([88142ab](https://github.com/ReVanced/revanced-patches/commit/88142ab464192b564b1b8d56a6b45663f77f5e00)) + +## [5.13.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.13.0...v5.13.1-dev.1) (2025-03-06) + + +### Bug Fixes + +* **YouTube - Change form factor:** Restore Automotive form factor watch history menu, channel pages, and community posts ([#4541](https://github.com/ReVanced/revanced-patches/issues/4541)) ([aa5c001](https://github.com/ReVanced/revanced-patches/commit/aa5c001968446e5270c756256724e917009612cd)) + +# [5.13.0](https://github.com/ReVanced/revanced-patches/compare/v5.12.0...v5.13.0) (2025-03-03) + + +### Bug Fixes + +* **TikTok:** Resolve startup app crash ([18c0fc2](https://github.com/ReVanced/revanced-patches/commit/18c0fc2a7f186f50a904fd25dbaa739abdd24993)) +* **TikTok:** Resolve startup app crash ([6466398](https://github.com/ReVanced/revanced-patches/commit/64663983b84de1f28636205f61bf0a24c83968d1)) +* **TikTok:** Resolve startup app crash ([c14bc24](https://github.com/ReVanced/revanced-patches/commit/c14bc244550de30eca975ca7c09e8eb0c47534b5)) +* **TikTok:** Resolve startup app crash ([d700076](https://github.com/ReVanced/revanced-patches/commit/d7000768a5e5a688c9f4e48858ac34e352222c1e)) +* **YouTube - Copy video URL:** Use correct button ordering ([5e622cc](https://github.com/ReVanced/revanced-patches/commit/5e622ccf66d34af31c6026fa7f4d332460c6ecb0)) +* **YouTube - Hide filter bar:** Fix `Hide in feed` not working in subscriptions feed ([#4512](https://github.com/ReVanced/revanced-patches/issues/4512)) ([634d0ee](https://github.com/ReVanced/revanced-patches/commit/634d0ee12e31491c7ee1d4ceb002daf8366a3c15)) +* **YouTube - Hide layout components:** Do not hide 'Show anyway' button in search results ([4ac8854](https://github.com/ReVanced/revanced-patches/commit/4ac8854b99808a8957f3b0b7438e1e0cdedffbaf)) +* **YouTube - Hide player components:** Show correct end video thumbnail if `Hide end screen suggested video` is enabled ([#4502](https://github.com/ReVanced/revanced-patches/issues/4502)) ([6c4885a](https://github.com/ReVanced/revanced-patches/commit/6c4885a1d5dfff50100b01840b5552d92e83ee4a)) +* **YouTube - Hide video action buttons:** Move 'Disable Like and Subscribe glow' to action buttons settings menu ([29b265d](https://github.com/ReVanced/revanced-patches/commit/29b265d8fdaa48502650be9623bfc518a57a0bb1)) +* **YouTube - Return YouTube Dislike:** Use correct number formatting if using a different ReVanced language ([edf66f4](https://github.com/ReVanced/revanced-patches/commit/edf66f4e16d46156cb8b8e31d18cb8dbcb87737e)) +* **YouTube - Spoof app version:** Force old settings menus if spoofing to older app targets ([#4490](https://github.com/ReVanced/revanced-patches/issues/4490)) ([45e7c46](https://github.com/ReVanced/revanced-patches/commit/45e7c46dd9c70c926b8b1a97ada668f90f5f6f8c)) +* **YouTube - Spoof video streams:** Resolve playback issues with dynamic player config ([#4521](https://github.com/ReVanced/revanced-patches/issues/4521)) ([647e764](https://github.com/ReVanced/revanced-patches/commit/647e7642efc0c00db17ccb6a620d1c96ccf4afed)) +* **YouTube - Swipe controls:** Adjust the overlay text size ([#4503](https://github.com/ReVanced/revanced-patches/issues/4503)) ([6dc4bf7](https://github.com/ReVanced/revanced-patches/commit/6dc4bf75e09ed6f05534919d7b769b720043abce)) +* **YouTube:** Do not hide player controls when using double tap to skip forward ([#4487](https://github.com/ReVanced/revanced-patches/issues/4487)) ([63fe870](https://github.com/ReVanced/revanced-patches/commit/63fe870d48ca2217327b952bde241b7f16ced850)) +* **YouTube:** Fix player button fade out animations ([#4469](https://github.com/ReVanced/revanced-patches/issues/4469)) ([bf8e775](https://github.com/ReVanced/revanced-patches/commit/bf8e7759f9bdbdfef419a879fb3dd7cf0dff0098)) +* **YouTube:** Resolve button flickering when taping seekbar ([#4500](https://github.com/ReVanced/revanced-patches/issues/4500)) ([1f08047](https://github.com/ReVanced/revanced-patches/commit/1f08047b48cc9555a4887d16ec7219a55a77251f)) + + +### Features + +* **Infinity for Reddit:** Add support for Infinity for Reddit Plus ([#4511](https://github.com/ReVanced/revanced-patches/issues/4511)) ([d74732b](https://github.com/ReVanced/revanced-patches/commit/d74732b7596104321bde263201d95649e4bd0eee)) +* **NU.nl:** Add `Hide ads` and `Spoof Certificate` patch ([#4368](https://github.com/ReVanced/revanced-patches/issues/4368)) ([f3268fb](https://github.com/ReVanced/revanced-patches/commit/f3268fb03ca25fb5465e36015b6c9dec2c84a655)) +* **YouTube - Navigation buttons:** Add 'Hide notifications' setting ([#4485](https://github.com/ReVanced/revanced-patches/issues/4485)) ([506d241](https://github.com/ReVanced/revanced-patches/commit/506d2414bbc760e764e5a514b32926083d6ecb6b)) +* **YouTube - Swipe controls:** Swipe controls UI improvements ([#4422](https://github.com/ReVanced/revanced-patches/issues/4422)) ([198e4d2](https://github.com/ReVanced/revanced-patches/commit/198e4d2a2315c24a09eb9ecfefbd131a75384d2c)) + +# [5.13.0-dev.19](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.18...v5.13.0-dev.19) (2025-03-02) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Resolve playback issues with dynamic player config ([#4521](https://github.com/ReVanced/revanced-patches/issues/4521)) ([647e764](https://github.com/ReVanced/revanced-patches/commit/647e7642efc0c00db17ccb6a620d1c96ccf4afed)) + +# [5.13.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.17...v5.13.0-dev.18) (2025-02-28) + + +### Features + +* **Infinity for Reddit:** Add support for Infinity for Reddit Plus ([#4511](https://github.com/ReVanced/revanced-patches/issues/4511)) ([d74732b](https://github.com/ReVanced/revanced-patches/commit/d74732b7596104321bde263201d95649e4bd0eee)) + +# [5.13.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.16...v5.13.0-dev.17) (2025-02-27) + + +### Bug Fixes + +* **YouTube - Hide filter bar:** Fix `Hide in feed` not working in subscriptions feed ([#4512](https://github.com/ReVanced/revanced-patches/issues/4512)) ([634d0ee](https://github.com/ReVanced/revanced-patches/commit/634d0ee12e31491c7ee1d4ceb002daf8366a3c15)) + +# [5.13.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.15...v5.13.0-dev.16) (2025-02-27) + + +### Features + +* **NU.nl:** Add `Hide ads` and `Spoof Certificate` patch ([#4368](https://github.com/ReVanced/revanced-patches/issues/4368)) ([f3268fb](https://github.com/ReVanced/revanced-patches/commit/f3268fb03ca25fb5465e36015b6c9dec2c84a655)) + +# [5.13.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.14...v5.13.0-dev.15) (2025-02-25) + + +### Bug Fixes + +* **YouTube - Hide player components:** Show correct end video thumbnail if `Hide end screen suggested video` is enabled ([#4502](https://github.com/ReVanced/revanced-patches/issues/4502)) ([6c4885a](https://github.com/ReVanced/revanced-patches/commit/6c4885a1d5dfff50100b01840b5552d92e83ee4a)) + +# [5.13.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.13...v5.13.0-dev.14) (2025-02-25) + + +### Bug Fixes + +* **YouTube - Swipe controls:** Adjust the overlay text size ([#4503](https://github.com/ReVanced/revanced-patches/issues/4503)) ([6dc4bf7](https://github.com/ReVanced/revanced-patches/commit/6dc4bf75e09ed6f05534919d7b769b720043abce)) + +# [5.13.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.12...v5.13.0-dev.13) (2025-02-24) + + +### Bug Fixes + +* **YouTube:** Resolve button flickering when taping seekbar ([#4500](https://github.com/ReVanced/revanced-patches/issues/4500)) ([1f08047](https://github.com/ReVanced/revanced-patches/commit/1f08047b48cc9555a4887d16ec7219a55a77251f)) + +# [5.13.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.11...v5.13.0-dev.12) (2025-02-24) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Use correct number formatting if using a different ReVanced language ([edf66f4](https://github.com/ReVanced/revanced-patches/commit/edf66f4e16d46156cb8b8e31d18cb8dbcb87737e)) + +# [5.13.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.10...v5.13.0-dev.11) (2025-02-23) + + +### Bug Fixes + +* **TikTok:** Resolve startup app crash ([18c0fc2](https://github.com/ReVanced/revanced-patches/commit/18c0fc2a7f186f50a904fd25dbaa739abdd24993)) + +# [5.13.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.9...v5.13.0-dev.10) (2025-02-22) + + +### Bug Fixes + +* **YouTube - Copy video URL:** Use correct button ordering ([5e622cc](https://github.com/ReVanced/revanced-patches/commit/5e622ccf66d34af31c6026fa7f4d332460c6ecb0)) + +# [5.13.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.8...v5.13.0-dev.9) (2025-02-22) + + +### Bug Fixes + +* **YouTube:** Do not hide player controls when using double tap to skip forward ([#4487](https://github.com/ReVanced/revanced-patches/issues/4487)) ([63fe870](https://github.com/ReVanced/revanced-patches/commit/63fe870d48ca2217327b952bde241b7f16ced850)) + +# [5.13.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.7...v5.13.0-dev.8) (2025-02-22) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Force old settings menus if spoofing to older app targets ([#4490](https://github.com/ReVanced/revanced-patches/issues/4490)) ([45e7c46](https://github.com/ReVanced/revanced-patches/commit/45e7c46dd9c70c926b8b1a97ada668f90f5f6f8c)) + +# [5.13.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.6...v5.13.0-dev.7) (2025-02-22) + + +### Bug Fixes + +* **TikTok:** Resolve startup app crash ([6466398](https://github.com/ReVanced/revanced-patches/commit/64663983b84de1f28636205f61bf0a24c83968d1)) + +# [5.13.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.5...v5.13.0-dev.6) (2025-02-21) + + +### Features + +* **YouTube - Navigation buttons:** Add 'Hide notifications' setting ([#4485](https://github.com/ReVanced/revanced-patches/issues/4485)) ([506d241](https://github.com/ReVanced/revanced-patches/commit/506d2414bbc760e764e5a514b32926083d6ecb6b)) + +# [5.13.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.4...v5.13.0-dev.5) (2025-02-19) + + +### Bug Fixes + +* **TikTok:** Resolve startup app crash ([c14bc24](https://github.com/ReVanced/revanced-patches/commit/c14bc244550de30eca975ca7c09e8eb0c47534b5)) + +# [5.13.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.3...v5.13.0-dev.4) (2025-02-19) + + +### Bug Fixes + +* **TikTok:** Resolve startup app crash ([d700076](https://github.com/ReVanced/revanced-patches/commit/d7000768a5e5a688c9f4e48858ac34e352222c1e)) + +# [5.13.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.2...v5.13.0-dev.3) (2025-02-19) + + +### Bug Fixes + +* **YouTube:** Fix player button fade out animations ([#4469](https://github.com/ReVanced/revanced-patches/issues/4469)) ([bf8e775](https://github.com/ReVanced/revanced-patches/commit/bf8e7759f9bdbdfef419a879fb3dd7cf0dff0098)) + +# [5.13.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.13.0-dev.1...v5.13.0-dev.2) (2025-02-18) + + +### Bug Fixes + +* **YouTube - Hide video action buttons:** Move 'Disable Like and Subscribe glow' to action buttons settings menu ([29b265d](https://github.com/ReVanced/revanced-patches/commit/29b265d8fdaa48502650be9623bfc518a57a0bb1)) + +# [5.13.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.12.0...v5.13.0-dev.1) (2025-02-18) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Do not hide 'Show anyway' button in search results ([4ac8854](https://github.com/ReVanced/revanced-patches/commit/4ac8854b99808a8957f3b0b7438e1e0cdedffbaf)) + + +### Features + +* **YouTube - Swipe controls:** Swipe controls UI improvements ([#4422](https://github.com/ReVanced/revanced-patches/issues/4422)) ([198e4d2](https://github.com/ReVanced/revanced-patches/commit/198e4d2a2315c24a09eb9ecfefbd131a75384d2c)) + +# [5.12.0](https://github.com/ReVanced/revanced-patches/compare/v5.11.0...v5.12.0) (2025-02-17) + + +### Bug Fixes + +* Allow changing default settings for existing app installs ([#4464](https://github.com/ReVanced/revanced-patches/issues/4464)) ([1bd7986](https://github.com/ReVanced/revanced-patches/commit/1bd7986823e774a929c8a9102a7cc96e245d5274)) +* **Windy.app:** Remove obsolete `Unlock pro` patch ([#4428](https://github.com/ReVanced/revanced-patches/issues/4428)) ([83d116e](https://github.com/ReVanced/revanced-patches/commit/83d116e8fd3935ee431cfdf0b8e095d04ee77259)) +* **YouTube - Spoof video streams:** Change default client to `Android TV` ([#4465](https://github.com/ReVanced/revanced-patches/issues/4465)) ([0412c79](https://github.com/ReVanced/revanced-patches/commit/0412c7901dc8599b6079d9c3ba26452f88af642b)) +* **YouTube:** Remove obsolete 18.x targets ([#4454](https://github.com/ReVanced/revanced-patches/issues/4454)) ([a006758](https://github.com/ReVanced/revanced-patches/commit/a0067581d0f877e1b4eb1f888a25786f09676b2e)) + + +### Features + +* **Return YouTube Dislike:** add `Show estimated likes` setting ([#4443](https://github.com/ReVanced/revanced-patches/issues/4443)) ([9a88b42](https://github.com/ReVanced/revanced-patches/commit/9a88b4239fd63d5f91105fec8e7d59d318a5d09a)) +* **YouTube - SponsorBlock:** Redesign skip buttons ([#4427](https://github.com/ReVanced/revanced-patches/issues/4427)) ([8f4883f](https://github.com/ReVanced/revanced-patches/commit/8f4883fc002420bfb4056401e23445c99e1d3fce)) +* **YouTube Music:** Support version `8.05.50` ([#4439](https://github.com/ReVanced/revanced-patches/issues/4439)) ([b31fed9](https://github.com/ReVanced/revanced-patches/commit/b31fed98901fcda1bce6f05eb0de63280c689fa0)) +* **YouTube Music:** Support version `8.05.51` ([128441e](https://github.com/ReVanced/revanced-patches/commit/128441e78bc0d096c3fc2f57782ab90c39c3ae4b)) + +# [5.12.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.12.0-dev.6...v5.12.0-dev.7) (2025-02-16) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Change default client to `Android TV` ([#4465](https://github.com/ReVanced/revanced-patches/issues/4465)) ([0412c79](https://github.com/ReVanced/revanced-patches/commit/0412c7901dc8599b6079d9c3ba26452f88af642b)) + + +### Features + +* **YouTube Music:** Support version `8.05.51` ([128441e](https://github.com/ReVanced/revanced-patches/commit/128441e78bc0d096c3fc2f57782ab90c39c3ae4b)) + +# [5.12.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.12.0-dev.5...v5.12.0-dev.6) (2025-02-16) + + +### Bug Fixes + +* Allow changing default settings for existing app installs ([#4464](https://github.com/ReVanced/revanced-patches/issues/4464)) ([1bd7986](https://github.com/ReVanced/revanced-patches/commit/1bd7986823e774a929c8a9102a7cc96e245d5274)) + +# [5.12.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.12.0-dev.4...v5.12.0-dev.5) (2025-02-13) + + +### Bug Fixes + +* **YouTube:** Remove obsolete 18.x targets ([#4454](https://github.com/ReVanced/revanced-patches/issues/4454)) ([a006758](https://github.com/ReVanced/revanced-patches/commit/a0067581d0f877e1b4eb1f888a25786f09676b2e)) + +# [5.12.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.12.0-dev.3...v5.12.0-dev.4) (2025-02-11) + + +### Features + +* **YouTube Music:** Support version `8.05.50` ([#4439](https://github.com/ReVanced/revanced-patches/issues/4439)) ([b31fed9](https://github.com/ReVanced/revanced-patches/commit/b31fed98901fcda1bce6f05eb0de63280c689fa0)) + +# [5.12.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.12.0-dev.2...v5.12.0-dev.3) (2025-02-11) + + +### Bug Fixes + +* **Windy.app:** Remove obsolete `Unlock pro` patch ([#4428](https://github.com/ReVanced/revanced-patches/issues/4428)) ([83d116e](https://github.com/ReVanced/revanced-patches/commit/83d116e8fd3935ee431cfdf0b8e095d04ee77259)) + +# [5.12.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.12.0-dev.1...v5.12.0-dev.2) (2025-02-11) + + +### Features + +* **Return YouTube Dislike:** add `Show estimated likes` setting ([#4443](https://github.com/ReVanced/revanced-patches/issues/4443)) ([9a88b42](https://github.com/ReVanced/revanced-patches/commit/9a88b4239fd63d5f91105fec8e7d59d318a5d09a)) + +# [5.12.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.11.0...v5.12.0-dev.1) (2025-02-10) + + +### Features + +* **YouTube - SponsorBlock:** Redesign skip buttons ([#4427](https://github.com/ReVanced/revanced-patches/issues/4427)) ([8f4883f](https://github.com/ReVanced/revanced-patches/commit/8f4883fc002420bfb4056401e23445c99e1d3fce)) + +# [5.11.0](https://github.com/ReVanced/revanced-patches/compare/v5.10.0...v5.11.0) (2025-02-07) + + +### Bug Fixes + +* Fix broken `Remove screen capture restriction`, `Remove screenshot restriction`, `Spoof Wi-Fi connection`, and `Export internal data documents provider` patch ([#4405](https://github.com/ReVanced/revanced-patches/issues/4405)) ([1d52b74](https://github.com/ReVanced/revanced-patches/commit/1d52b7478d34e699d8c629eeaa9fdbb470b7d5c8)) +* **YouTube - Enable slide to seek:** Change patch to default include ([50358cd](https://github.com/ReVanced/revanced-patches/commit/50358cddea3eef4051d248040d23f774521dce00)) +* **YouTube - Hide layout components:** Hide new type of community post ([#4404](https://github.com/ReVanced/revanced-patches/issues/4404)) ([f67ab2b](https://github.com/ReVanced/revanced-patches/commit/f67ab2baf25d543ceb55fcec48bda441ebf2b998)) +* **YouTube - Theme:** Use custom seekbar color for cairo startup animation ([#4399](https://github.com/ReVanced/revanced-patches/issues/4399)) ([1cba294](https://github.com/ReVanced/revanced-patches/commit/1cba2948a6787118eb380ffcec35ee4fb99447ea)) + + +### Features + +* **YouTube - Change start page:** Add additional start pages ([#4413](https://github.com/ReVanced/revanced-patches/issues/4413)) ([b434182](https://github.com/ReVanced/revanced-patches/commit/b434182df69313c4eb5f0dfd98101cb80e46ead2)) + +# [5.11.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.11.0-dev.1...v5.11.0-dev.2) (2025-02-06) + + +### Bug Fixes + +* Fix broken `Remove screen capture restriction`, `Remove screenshot restriction`, `Spoof Wi-Fi connection`, and `Export internal data documents provider` patch ([#4405](https://github.com/ReVanced/revanced-patches/issues/4405)) ([1d52b74](https://github.com/ReVanced/revanced-patches/commit/1d52b7478d34e699d8c629eeaa9fdbb470b7d5c8)) + +# [5.11.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.10.1-dev.3...v5.11.0-dev.1) (2025-02-05) + + +### Features + +* **YouTube - Change start page:** Add additional start pages ([#4413](https://github.com/ReVanced/revanced-patches/issues/4413)) ([b434182](https://github.com/ReVanced/revanced-patches/commit/b434182df69313c4eb5f0dfd98101cb80e46ead2)) + +## [5.10.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.10.1-dev.2...v5.10.1-dev.3) (2025-02-03) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new type of community post ([#4404](https://github.com/ReVanced/revanced-patches/issues/4404)) ([f67ab2b](https://github.com/ReVanced/revanced-patches/commit/f67ab2baf25d543ceb55fcec48bda441ebf2b998)) + +## [5.10.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.10.1-dev.1...v5.10.1-dev.2) (2025-02-03) + + +### Bug Fixes + +* **YouTube - Enable slide to seek:** Change patch to default include ([50358cd](https://github.com/ReVanced/revanced-patches/commit/50358cddea3eef4051d248040d23f774521dce00)) + +## [5.10.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.10.0...v5.10.1-dev.1) (2025-02-02) + + +### Bug Fixes + +* **YouTube - Theme:** Use custom seekbar color for cairo startup animation ([#4399](https://github.com/ReVanced/revanced-patches/issues/4399)) ([1cba294](https://github.com/ReVanced/revanced-patches/commit/1cba2948a6787118eb380ffcec35ee4fb99447ea)) + +# [5.10.0](https://github.com/ReVanced/revanced-patches/compare/v5.9.0...v5.10.0) (2025-01-31) + + +### Bug Fixes + +* **SwissId - Play integrity Removal:** Add recommended app version ([#4370](https://github.com/ReVanced/revanced-patches/issues/4370)) ([d8ed474](https://github.com/ReVanced/revanced-patches/commit/d8ed474b165f094fdedc32caaae1f82ebc99eb3d)) +* Use correct path to fix invalid file paths ([5ff4ee8](https://github.com/ReVanced/revanced-patches/commit/5ff4ee823da55c7b135eab8b62e07be465612b55)) +* **YouTube - Hide ads:** fix 'Hide the Visit store button on channel pages' not working ([#4364](https://github.com/ReVanced/revanced-patches/issues/4364)) ([9d63ea9](https://github.com/ReVanced/revanced-patches/commit/9d63ea9a10ab5128ce18a1f53a946e84550da258)) +* **YouTube - Hide Ads:** Hide end screen store banner without leaving empty space ([#4367](https://github.com/ReVanced/revanced-patches/issues/4367)) ([7e68390](https://github.com/ReVanced/revanced-patches/commit/7e683906418434dd4e2104337d73a2292415c615)) +* **YouTube - Hide ads:** Hide new types of tablet ads ([574bcc8](https://github.com/ReVanced/revanced-patches/commit/574bcc844746b7445ec3e93b47daceafefad85e7)) +* **YouTube - Hide layout components:** Hide new kind of community post ([#4341](https://github.com/ReVanced/revanced-patches/issues/4341)) ([02685c4](https://github.com/ReVanced/revanced-patches/commit/02685c4567aca55f22d45dc238a7d1f0ea264143)) +* **YouTube - Hide seekbar:** Do not hide player seekbar if hide feed seekbar is enabled ([#4333](https://github.com/ReVanced/revanced-patches/issues/4333)) ([f5cf6f2](https://github.com/ReVanced/revanced-patches/commit/f5cf6f2a445492d33815a9772f49deac2d70eba9)) +* **YouTube - Hide video description components:** Use correct string key names ([0f28c2b](https://github.com/ReVanced/revanced-patches/commit/0f28c2b44c0051ea7ab3136433b84c73321cf5bd)) +* **YouTube - Spoof video streams:** Update settings side effects summary text ([#4369](https://github.com/ReVanced/revanced-patches/issues/4369)) ([e5b3aa1](https://github.com/ReVanced/revanced-patches/commit/e5b3aa1cc6a2465cb006487d528de888bc7cd430)) +* **YouTube - Theme:** Fix 19.25 - 19.45 patch error ([5b47a5f](https://github.com/ReVanced/revanced-patches/commit/5b47a5f0f6299daaae209341064fd85f16ca18a6)) +* **YouTube - Theme:** Replace custom seekbar gradient colors instead of disabling ([#4329](https://github.com/ReVanced/revanced-patches/issues/4329)) ([f03da98](https://github.com/ReVanced/revanced-patches/commit/f03da983051021e0c372557a5354d5d967409564)) + + +### Features + +* **YouTube - Hide ads:** Add `Hide end screen store banner` ([#4351](https://github.com/ReVanced/revanced-patches/issues/4351)) ([5505087](https://github.com/ReVanced/revanced-patches/commit/55050878028fed82b0f583a9f7ba06b8f267f8ec)) +* **YouTube - Hide video description components:** Add `Hide How this content was made section` ([#4355](https://github.com/ReVanced/revanced-patches/issues/4355)) ([68ec54e](https://github.com/ReVanced/revanced-patches/commit/68ec54ef850ae8d6461dd0ef2846e6efbb59e482)) +* **YouTube - Theme:** Add option to use custom seekbar accent color ([#4337](https://github.com/ReVanced/revanced-patches/issues/4337)) ([952b4fc](https://github.com/ReVanced/revanced-patches/commit/952b4fc4c9291e1a3e71437b503857763c973dd4)) +* **YouTube:** Add patch `Disable HDR video` ([#4347](https://github.com/ReVanced/revanced-patches/issues/4347)) ([0528f7c](https://github.com/ReVanced/revanced-patches/commit/0528f7cad856a2b1347e41944167b0583fc4a3d9)) + +# [5.10.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.10...v5.10.0-dev.11) (2025-01-30) + + +### Bug Fixes + +* Use correct path to fix invalid file paths ([5ff4ee8](https://github.com/ReVanced/revanced-patches/commit/5ff4ee823da55c7b135eab8b62e07be465612b55)) + +# [5.10.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.9...v5.10.0-dev.10) (2025-01-29) + + +### Bug Fixes + +* **YouTube - Hide ads:** Hide new types of tablet ads ([574bcc8](https://github.com/ReVanced/revanced-patches/commit/574bcc844746b7445ec3e93b47daceafefad85e7)) + +# [5.10.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.8...v5.10.0-dev.9) (2025-01-29) + + +### Bug Fixes + +* **SwissId - Play integrity Removal:** Add recommended app version ([#4370](https://github.com/ReVanced/revanced-patches/issues/4370)) ([d8ed474](https://github.com/ReVanced/revanced-patches/commit/d8ed474b165f094fdedc32caaae1f82ebc99eb3d)) + +# [5.10.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.7...v5.10.0-dev.8) (2025-01-29) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Update settings side effects summary text ([#4369](https://github.com/ReVanced/revanced-patches/issues/4369)) ([e5b3aa1](https://github.com/ReVanced/revanced-patches/commit/e5b3aa1cc6a2465cb006487d528de888bc7cd430)) + +# [5.10.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.6...v5.10.0-dev.7) (2025-01-29) + + +### Bug Fixes + +* **YouTube - Hide ads:** fix 'Hide the Visit store button on channel pages' not working ([#4364](https://github.com/ReVanced/revanced-patches/issues/4364)) ([9d63ea9](https://github.com/ReVanced/revanced-patches/commit/9d63ea9a10ab5128ce18a1f53a946e84550da258)) + +# [5.10.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.5...v5.10.0-dev.6) (2025-01-29) + + +### Bug Fixes + +* **YouTube - Hide Ads:** Hide end screen store banner without leaving empty space ([#4367](https://github.com/ReVanced/revanced-patches/issues/4367)) ([7e68390](https://github.com/ReVanced/revanced-patches/commit/7e683906418434dd4e2104337d73a2292415c615)) + +# [5.10.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.4...v5.10.0-dev.5) (2025-01-27) + + +### Bug Fixes + +* **YouTube - Hide video description components:** Use correct string key names ([0f28c2b](https://github.com/ReVanced/revanced-patches/commit/0f28c2b44c0051ea7ab3136433b84c73321cf5bd)) + +# [5.10.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.3...v5.10.0-dev.4) (2025-01-27) + + +### Features + +* **YouTube - Hide video description components:** Add `Hide How this content was made section` ([#4355](https://github.com/ReVanced/revanced-patches/issues/4355)) ([68ec54e](https://github.com/ReVanced/revanced-patches/commit/68ec54ef850ae8d6461dd0ef2846e6efbb59e482)) + +# [5.10.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.2...v5.10.0-dev.3) (2025-01-27) + + +### Features + +* **YouTube - Hide ads:** Add `Hide end screen store banner` ([#4351](https://github.com/ReVanced/revanced-patches/issues/4351)) ([5505087](https://github.com/ReVanced/revanced-patches/commit/55050878028fed82b0f583a9f7ba06b8f267f8ec)) + +# [5.10.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.10.0-dev.1...v5.10.0-dev.2) (2025-01-25) + + +### Features + +* **YouTube:** Add patch `Disable HDR video` ([#4347](https://github.com/ReVanced/revanced-patches/issues/4347)) ([0528f7c](https://github.com/ReVanced/revanced-patches/commit/0528f7cad856a2b1347e41944167b0583fc4a3d9)) + +# [5.10.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.9.1-dev.4...v5.10.0-dev.1) (2025-01-23) + + +### Features + +* **YouTube - Theme:** Add option to use custom seekbar accent color ([#4337](https://github.com/ReVanced/revanced-patches/issues/4337)) ([952b4fc](https://github.com/ReVanced/revanced-patches/commit/952b4fc4c9291e1a3e71437b503857763c973dd4)) + +## [5.9.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.9.1-dev.3...v5.9.1-dev.4) (2025-01-22) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new kind of community post ([#4341](https://github.com/ReVanced/revanced-patches/issues/4341)) ([02685c4](https://github.com/ReVanced/revanced-patches/commit/02685c4567aca55f22d45dc238a7d1f0ea264143)) + +## [5.9.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.9.1-dev.2...v5.9.1-dev.3) (2025-01-22) + + +### Bug Fixes + +* **YouTube - Hide seekbar:** Do not hide player seekbar if hide feed seekbar is enabled ([#4333](https://github.com/ReVanced/revanced-patches/issues/4333)) ([f5cf6f2](https://github.com/ReVanced/revanced-patches/commit/f5cf6f2a445492d33815a9772f49deac2d70eba9)) + +## [5.9.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.9.1-dev.1...v5.9.1-dev.2) (2025-01-22) + + +### Bug Fixes + +* **YouTube - Theme:** Fix 19.25 - 19.45 patch error ([5b47a5f](https://github.com/ReVanced/revanced-patches/commit/5b47a5f0f6299daaae209341064fd85f16ca18a6)) + +## [5.9.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.9.0...v5.9.1-dev.1) (2025-01-21) + + +### Bug Fixes + +* **YouTube - Theme:** Replace custom seekbar gradient colors instead of disabling ([#4329](https://github.com/ReVanced/revanced-patches/issues/4329)) ([f03da98](https://github.com/ReVanced/revanced-patches/commit/f03da983051021e0c372557a5354d5d967409564)) + +# [5.9.0](https://github.com/ReVanced/revanced-patches/compare/v5.8.1...v5.9.0) (2025-01-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Resolve playback issues after changing from cellular to wifi ([#4277](https://github.com/ReVanced/revanced-patches/issues/4277)) ([e93e1c8](https://github.com/ReVanced/revanced-patches/commit/e93e1c8ec3367e941034e9c4e3725ec1db429a60)) +* **YouTube - Spoof video streams:** Update client user-agent ([#4304](https://github.com/ReVanced/revanced-patches/issues/4304)) ([7917871](https://github.com/ReVanced/revanced-patches/commit/7917871f510b6b805370ef98a0cf8a4e2df0e900)) + + +### Features + +* **YouTube - Hide feed components:** Handle new type of surveys ([#4295](https://github.com/ReVanced/revanced-patches/issues/4295)) ([c770e03](https://github.com/ReVanced/revanced-patches/commit/c770e03f3801367cb531af860fbdfa43dca89af0)) +* **YouTube - Playback speed:** Add option to change 2x tap and hold speed ([#4307](https://github.com/ReVanced/revanced-patches/issues/4307)) ([02fb26e](https://github.com/ReVanced/revanced-patches/commit/02fb26e9458fb8635d497e6e78f964055244d738)) +* **YouTube - Settings:** Add option to use new Cairo settings menus ([#4305](https://github.com/ReVanced/revanced-patches/issues/4305)) ([7b8a2a2](https://github.com/ReVanced/revanced-patches/commit/7b8a2a2721ab5351f8c0251401aceddf0c5327df)) + +# [5.9.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.9.0-dev.3...v5.9.0-dev.4) (2025-01-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Update client user-agent ([#4304](https://github.com/ReVanced/revanced-patches/issues/4304)) ([7917871](https://github.com/ReVanced/revanced-patches/commit/7917871f510b6b805370ef98a0cf8a4e2df0e900)) + +# [5.9.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.9.0-dev.2...v5.9.0-dev.3) (2025-01-19) + + +### Features + +* **YouTube - Settings:** Add option to use new Cairo settings menus ([#4305](https://github.com/ReVanced/revanced-patches/issues/4305)) ([7b8a2a2](https://github.com/ReVanced/revanced-patches/commit/7b8a2a2721ab5351f8c0251401aceddf0c5327df)) + +# [5.9.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.9.0-dev.1...v5.9.0-dev.2) (2025-01-18) + + +### Features + +* **YouTube - Playback speed:** Add option to change 2x tap and hold speed ([#4307](https://github.com/ReVanced/revanced-patches/issues/4307)) ([02fb26e](https://github.com/ReVanced/revanced-patches/commit/02fb26e9458fb8635d497e6e78f964055244d738)) + +# [5.9.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.8.2-dev.1...v5.9.0-dev.1) (2025-01-17) + + +### Features + +* **YouTube - Hide feed components:** Handle new type of surveys ([#4295](https://github.com/ReVanced/revanced-patches/issues/4295)) ([c770e03](https://github.com/ReVanced/revanced-patches/commit/c770e03f3801367cb531af860fbdfa43dca89af0)) + +## [5.8.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.8.1...v5.8.2-dev.1) (2025-01-09) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Resolve playback issues after changing from cellular to wifi ([#4277](https://github.com/ReVanced/revanced-patches/issues/4277)) ([e93e1c8](https://github.com/ReVanced/revanced-patches/commit/e93e1c8ec3367e941034e9c4e3725ec1db429a60)) + +## [5.8.1](https://github.com/ReVanced/revanced-patches/compare/v5.8.0...v5.8.1) (2025-01-07) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add 'Android Creator' ([#4262](https://github.com/ReVanced/revanced-patches/issues/4262)) ([0479dd2](https://github.com/ReVanced/revanced-patches/commit/0479dd265e09b0accdf6ff6b00c8e938dc5b96c7)) + +## [5.8.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.8.0...v5.8.1-dev.1) (2025-01-06) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add 'Android Creator' ([#4262](https://github.com/ReVanced/revanced-patches/issues/4262)) ([0479dd2](https://github.com/ReVanced/revanced-patches/commit/0479dd265e09b0accdf6ff6b00c8e938dc5b96c7)) + +# [5.8.0](https://github.com/ReVanced/revanced-patches/compare/v5.7.2...v5.8.0) (2024-12-30) + + +### Bug Fixes + +* **GmsCore support:** Do not show battery optimization error on Android Automotive devices (Google built-in) ([#4218](https://github.com/ReVanced/revanced-patches/issues/4218)) ([d6e389c](https://github.com/ReVanced/revanced-patches/commit/d6e389cc43bc40724f032b230f70048276349a19)) +* **YouTube - Exit fullscreen mode:** Exit fullscreen mode of first video opened after cold start ([be5cf2e](https://github.com/ReVanced/revanced-patches/commit/be5cf2e834d87d51b5d3061d46bd7154d6306787)) +* **YouTube - Force original audio:** If stream spoofing to Android then show a summary text why force audio is not available ([#4220](https://github.com/ReVanced/revanced-patches/issues/4220)) ([029aee8](https://github.com/ReVanced/revanced-patches/commit/029aee8023f096413fc80a2c583b4fe55ecb10ac)) +* **YouTube - Spoof video streams:** Ignore harmless error toast if hide ads is disabled ([c3423bb](https://github.com/ReVanced/revanced-patches/commit/c3423bb9e531cfa52f6d28e0b98bbe8ab8684c30)) + + +### Features + +* **Swipe controls:** Add option to enable/disable fullscreen swipe to next video ([#4222](https://github.com/ReVanced/revanced-patches/issues/4222)) ([119092f](https://github.com/ReVanced/revanced-patches/commit/119092fafa4129849246df15fe8076ed3b491b85)) +* **YouTube - Hide Shorts components:** Add option to hide Shorts in watch history ([#4214](https://github.com/ReVanced/revanced-patches/issues/4214)) ([19c2742](https://github.com/ReVanced/revanced-patches/commit/19c2742aa367367c77bb50ddad6f8a20fef8ea0a)) +* **YouTube - Spoof app version:** Add 'Restore old navigation and toolbar icons' ([f84e459](https://github.com/ReVanced/revanced-patches/commit/f84e459d3d54b3001586796ab4e114ebadf09043)) +* **YouTube:** Add `Change form factor` patch ([#4217](https://github.com/ReVanced/revanced-patches/issues/4217)) ([644ac5b](https://github.com/ReVanced/revanced-patches/commit/644ac5baa68b209a32300149a2efa009b776f9a7)) +* **YouTube:** Add `Exit fullscreen mode` patch ([#4223](https://github.com/ReVanced/revanced-patches/issues/4223)) ([bb5d03b](https://github.com/ReVanced/revanced-patches/commit/bb5d03bd89a3f932c77e4e9de90174c374933688)) +* **YouTube:** Add in app option to select a preferred language for ReVanced specific text ([#4231](https://github.com/ReVanced/revanced-patches/issues/4231)) ([3932af3](https://github.com/ReVanced/revanced-patches/commit/3932af397ae89a0b30191cd870bd6cddb7a078db)) + +# [5.8.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.8.0-dev.7...v5.8.0-dev.8) (2024-12-28) + + +### Features + +* **YouTube:** Add in app option to select a preferred language for ReVanced specific text ([#4231](https://github.com/ReVanced/revanced-patches/issues/4231)) ([3932af3](https://github.com/ReVanced/revanced-patches/commit/3932af397ae89a0b30191cd870bd6cddb7a078db)) + +# [5.8.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.8.0-dev.6...v5.8.0-dev.7) (2024-12-27) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Ignore harmless error toast if hide ads is disabled ([c3423bb](https://github.com/ReVanced/revanced-patches/commit/c3423bb9e531cfa52f6d28e0b98bbe8ab8684c30)) + +# [5.8.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.8.0-dev.5...v5.8.0-dev.6) (2024-12-27) + + +### Bug Fixes + +* **YouTube - Exit fullscreen mode:** Exit fullscreen mode of first video opened after cold start ([be5cf2e](https://github.com/ReVanced/revanced-patches/commit/be5cf2e834d87d51b5d3061d46bd7154d6306787)) + +# [5.8.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.8.0-dev.4...v5.8.0-dev.5) (2024-12-27) + + +### Features + +* **YouTube:** Add `Change form factor` patch ([#4217](https://github.com/ReVanced/revanced-patches/issues/4217)) ([644ac5b](https://github.com/ReVanced/revanced-patches/commit/644ac5baa68b209a32300149a2efa009b776f9a7)) + +# [5.8.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.8.0-dev.3...v5.8.0-dev.4) (2024-12-27) + + +### Bug Fixes + +* **GmsCore support:** Do not show battery optimization error on Android Automotive devices (Google built-in) ([#4218](https://github.com/ReVanced/revanced-patches/issues/4218)) ([d6e389c](https://github.com/ReVanced/revanced-patches/commit/d6e389cc43bc40724f032b230f70048276349a19)) + + +### Features + +* **Swipe controls:** Add option to enable/disable fullscreen swipe to next video ([#4222](https://github.com/ReVanced/revanced-patches/issues/4222)) ([119092f](https://github.com/ReVanced/revanced-patches/commit/119092fafa4129849246df15fe8076ed3b491b85)) +* **YouTube:** Add `Exit fullscreen mode` patch ([#4223](https://github.com/ReVanced/revanced-patches/issues/4223)) ([bb5d03b](https://github.com/ReVanced/revanced-patches/commit/bb5d03bd89a3f932c77e4e9de90174c374933688)) + +# [5.8.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.8.0-dev.2...v5.8.0-dev.3) (2024-12-26) + + +### Bug Fixes + +* **YouTube - Force original audio:** If stream spoofing to Android then show a summary text why force audio is not available ([#4220](https://github.com/ReVanced/revanced-patches/issues/4220)) ([029aee8](https://github.com/ReVanced/revanced-patches/commit/029aee8023f096413fc80a2c583b4fe55ecb10ac)) + +# [5.8.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.8.0-dev.1...v5.8.0-dev.2) (2024-12-24) + + +### Features + +* **YouTube - Spoof app version:** Add 'Restore old navigation and toolbar icons' ([f84e459](https://github.com/ReVanced/revanced-patches/commit/f84e459d3d54b3001586796ab4e114ebadf09043)) + +# [5.8.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.7.2...v5.8.0-dev.1) (2024-12-24) + + +### Features + +* **YouTube - Hide Shorts components:** Add option to hide Shorts in watch history ([#4214](https://github.com/ReVanced/revanced-patches/issues/4214)) ([19c2742](https://github.com/ReVanced/revanced-patches/commit/19c2742aa367367c77bb50ddad6f8a20fef8ea0a)) + +## [5.7.2](https://github.com/ReVanced/revanced-patches/compare/v5.7.1...v5.7.2) (2024-12-24) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Don't hide Shorts channel bar when toggling for video player ([9af6412](https://github.com/ReVanced/revanced-patches/commit/9af6412d92ec31e612eaabba6578453da0fc61d6)) +* **YouTube - Spoof video streams:** Add iOS TV client, restore iOS 'force AVC', show client type in stats for nerds ([#4202](https://github.com/ReVanced/revanced-patches/issues/4202)) ([ab29f80](https://github.com/ReVanced/revanced-patches/commit/ab29f808a9f55b5ab0055533c1a6de549b0631a6)) + +## [5.7.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.7.2-dev.1...v5.7.2-dev.2) (2024-12-23) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Don't hide Shorts channel bar when toggling for video player ([9af6412](https://github.com/ReVanced/revanced-patches/commit/9af6412d92ec31e612eaabba6578453da0fc61d6)) + +## [5.7.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.7.1...v5.7.2-dev.1) (2024-12-23) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add iOS TV client, restore iOS 'force AVC', show client type in stats for nerds ([#4202](https://github.com/ReVanced/revanced-patches/issues/4202)) ([ab29f80](https://github.com/ReVanced/revanced-patches/commit/ab29f808a9f55b5ab0055533c1a6de549b0631a6)) + +## [5.7.1](https://github.com/ReVanced/revanced-patches/compare/v5.7.0...v5.7.1) (2024-12-23) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Show a toast and not a dialog if segment submitted successfully ([134b189](https://github.com/ReVanced/revanced-patches/commit/134b189791113dcf1a1cb7c87b8a0954f432730c)) +* **YouTube - Spoof video streams:** Use 2 letter device language code ([33ff997](https://github.com/ReVanced/revanced-patches/commit/33ff9972000581aca92262f984efb114eeeb9537)) +* **YouTube - Spoof video streams:** Use Android VR authentication if using default audio language ([#4191](https://github.com/ReVanced/revanced-patches/issues/4191)) ([98773cc](https://github.com/ReVanced/revanced-patches/commit/98773cc7d46e5c9c7715b82c8006f1ccbcc5443c)) +* **YouTube - Theme:** Use dark theme color for status and navigation bar ([0240efe](https://github.com/ReVanced/revanced-patches/commit/0240efe33e5444625ca2b760c861c9046d3dc836)) +* **YouTube:** Do not reset playback speed to 1.0x after closing comment thread (Fixes stock YouTube bug) ([#4195](https://github.com/ReVanced/revanced-patches/issues/4195)) ([dda788c](https://github.com/ReVanced/revanced-patches/commit/dda788c58c789d4f91646ea8e8a8077f590ab6b3)) + +## [5.7.1-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.7.1-dev.4...v5.7.1-dev.5) (2024-12-22) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Use 2 letter device language code ([33ff997](https://github.com/ReVanced/revanced-patches/commit/33ff9972000581aca92262f984efb114eeeb9537)) + +## [5.7.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.7.1-dev.3...v5.7.1-dev.4) (2024-12-22) + + +### Bug Fixes + +* **YouTube:** Do not reset playback speed to 1.0x after closing comment thread (Fixes stock YouTube bug) ([#4195](https://github.com/ReVanced/revanced-patches/issues/4195)) ([dda788c](https://github.com/ReVanced/revanced-patches/commit/dda788c58c789d4f91646ea8e8a8077f590ab6b3)) + +## [5.7.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.7.1-dev.2...v5.7.1-dev.3) (2024-12-22) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Show a toast and not a dialog if segment submitted successfully ([134b189](https://github.com/ReVanced/revanced-patches/commit/134b189791113dcf1a1cb7c87b8a0954f432730c)) + +## [5.7.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.7.1-dev.1...v5.7.1-dev.2) (2024-12-22) + + +### Bug Fixes + +* **YouTube - Theme:** Use dark theme color for status and navigation bar ([0240efe](https://github.com/ReVanced/revanced-patches/commit/0240efe33e5444625ca2b760c861c9046d3dc836)) + +## [5.7.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.7.0...v5.7.1-dev.1) (2024-12-22) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Use Android VR authentication if using default audio language ([#4191](https://github.com/ReVanced/revanced-patches/issues/4191)) ([98773cc](https://github.com/ReVanced/revanced-patches/commit/98773cc7d46e5c9c7715b82c8006f1ccbcc5443c)) + +# [5.7.0](https://github.com/ReVanced/revanced-patches/compare/v5.6.0...v5.7.0) (2024-12-22) + + +### Bug Fixes + +* **YouTube - Force original audio:** Use correct availability for settings UI ([a7eedcb](https://github.com/ReVanced/revanced-patches/commit/a7eedcb4cca6b7b12629c478c24c0899c80e3615)) +* **YouTube - Spoof video stream:** Remove UI client type setting. Allow setting default audio language. ([#4184](https://github.com/ReVanced/revanced-patches/issues/4184)) ([99f3f29](https://github.com/ReVanced/revanced-patches/commit/99f3f29c649bf7693c05bbce2bb49bd53e05f050)) +* **YouTube - Spoof video streams:** Remove iOS, add clients Android TV and Android Creator ([#4180](https://github.com/ReVanced/revanced-patches/issues/4180)) ([86abfb2](https://github.com/ReVanced/revanced-patches/commit/86abfb2b0d4675f0a1cb9ab244783075bfe89281)) +* **YouTube:** Change fingerprints to support a wider range of target versions ([8a09174](https://github.com/ReVanced/revanced-patches/commit/8a09174def205a26ce49cb7815097e235069526a)) + + +### Features + +* **YouTube:** Support version `19.47.53` ([#4182](https://github.com/ReVanced/revanced-patches/issues/4182)) ([2089e61](https://github.com/ReVanced/revanced-patches/commit/2089e613d36c45352db7d852aaee0087b1c3e1a4)) + +# [5.7.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.6.1-dev.4...v5.7.0-dev.1) (2024-12-21) + + +### Features + +* **YouTube:** Support version `19.47.53` ([#4182](https://github.com/ReVanced/revanced-patches/issues/4182)) ([2089e61](https://github.com/ReVanced/revanced-patches/commit/2089e613d36c45352db7d852aaee0087b1c3e1a4)) + +## [5.6.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.6.1-dev.3...v5.6.1-dev.4) (2024-12-21) + + +### Bug Fixes + +* **YouTube - Spoof video stream:** Remove UI client type setting. Allow setting default audio language. ([#4184](https://github.com/ReVanced/revanced-patches/issues/4184)) ([99f3f29](https://github.com/ReVanced/revanced-patches/commit/99f3f29c649bf7693c05bbce2bb49bd53e05f050)) + +## [5.6.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.6.1-dev.2...v5.6.1-dev.3) (2024-12-21) + + +### Bug Fixes + +* **YouTube - Force original audio:** Use correct availability for settings UI ([a7eedcb](https://github.com/ReVanced/revanced-patches/commit/a7eedcb4cca6b7b12629c478c24c0899c80e3615)) + +## [5.6.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.6.1-dev.1...v5.6.1-dev.2) (2024-12-21) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Remove iOS, add clients Android TV and Android Creator ([#4180](https://github.com/ReVanced/revanced-patches/issues/4180)) ([86abfb2](https://github.com/ReVanced/revanced-patches/commit/86abfb2b0d4675f0a1cb9ab244783075bfe89281)) + +## [5.6.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.6.0...v5.6.1-dev.1) (2024-12-21) + + +### Bug Fixes + +* **YouTube:** Change fingerprints to support a wider range of target versions ([8a09174](https://github.com/ReVanced/revanced-patches/commit/8a09174def205a26ce49cb7815097e235069526a)) + +# [5.6.0](https://github.com/ReVanced/revanced-patches/compare/v5.5.1...v5.6.0) (2024-12-20) + + +### Bug Fixes + +* **Twitter - Change link sharing domain:** Use correct extension package ([ad7fab6](https://github.com/ReVanced/revanced-patches/commit/ad7fab67319ba23f267d27da9b74266965fc4be3)) +* **YouTube - Force original audio:** Use correct original audio stream if app language is not English ([0d20171](https://github.com/ReVanced/revanced-patches/commit/0d2017133efac230887b5c2a331d87159df8af11)) +* **YouTube - Hide layout components:** Hide new kind of community post ([#4155](https://github.com/ReVanced/revanced-patches/issues/4155)) ([08f68cb](https://github.com/ReVanced/revanced-patches/commit/08f68cb5d33f2cfe656d2f93d159c69981f31418)) +* **YouTube - Miniplayer:** Use estimated maximum on screen size for devices with low density screens ([#4150](https://github.com/ReVanced/revanced-patches/issues/4150)) ([2694158](https://github.com/ReVanced/revanced-patches/commit/2694158c3c9935ede21c96832533222f850068df)) +* **YouTube - Open Shorts in regular player:** Do not show the miniplayer after opening a Short while a video is playing ([894e366](https://github.com/ReVanced/revanced-patches/commit/894e36665d17d5a3a5728961d424dffc55faa50b)) +* **YouTube - SponsorBlock:** Show create new segment error messages using a dialog ([#4148](https://github.com/ReVanced/revanced-patches/issues/4148)) ([5870906](https://github.com/ReVanced/revanced-patches/commit/587090636dfff0b358b15026cf7d47c65a4296dc)) +* **YouTube - Spoof video streams:** Change default spoofing to iOS, allow setting a default language with Android VR ([#4171](https://github.com/ReVanced/revanced-patches/issues/4171)) ([171b4e7](https://github.com/ReVanced/revanced-patches/commit/171b4e7e40066e38fba773b7a6525e9a038779ef)) +* **YouTube - Spoof video streams:** Update iOS client version ([df3aeed](https://github.com/ReVanced/revanced-patches/commit/df3aeed3b173e408fad80197a89ec5d003a2b328)) + + +### Features + +* **YouTube:** Add `Open Shorts in regular player` patch ([#4153](https://github.com/ReVanced/revanced-patches/issues/4153)) ([c7c5e5b](https://github.com/ReVanced/revanced-patches/commit/c7c5e5b2b9cf63d8225bb6bd5e735ddf945b6c29)) + +# [5.6.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.6.0-dev.5...v5.6.0-dev.6) (2024-12-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Update iOS client version ([df3aeed](https://github.com/ReVanced/revanced-patches/commit/df3aeed3b173e408fad80197a89ec5d003a2b328)) + +# [5.6.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.6.0-dev.4...v5.6.0-dev.5) (2024-12-20) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Change default spoofing to iOS, allow setting a default language with Android VR ([#4171](https://github.com/ReVanced/revanced-patches/issues/4171)) ([171b4e7](https://github.com/ReVanced/revanced-patches/commit/171b4e7e40066e38fba773b7a6525e9a038779ef)) + +# [5.6.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.6.0-dev.3...v5.6.0-dev.4) (2024-12-20) + + +### Bug Fixes + +* **YouTube - Force original audio:** Use correct original audio stream if app language is not English ([0d20171](https://github.com/ReVanced/revanced-patches/commit/0d2017133efac230887b5c2a331d87159df8af11)) + +# [5.6.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.6.0-dev.2...v5.6.0-dev.3) (2024-12-20) + + +### Bug Fixes + +* **Twitter - Change link sharing domain:** Use correct extension package ([ad7fab6](https://github.com/ReVanced/revanced-patches/commit/ad7fab67319ba23f267d27da9b74266965fc4be3)) + +# [5.6.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.6.0-dev.1...v5.6.0-dev.2) (2024-12-19) + + +### Bug Fixes + +* **YouTube - Open Shorts in regular player:** Do not show the miniplayer after opening a Short while a video is playing ([894e366](https://github.com/ReVanced/revanced-patches/commit/894e36665d17d5a3a5728961d424dffc55faa50b)) + +# [5.6.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.5.2-dev.2...v5.6.0-dev.1) (2024-12-19) + + +### Features + +* **YouTube:** Add `Open Shorts in regular player` patch ([#4153](https://github.com/ReVanced/revanced-patches/issues/4153)) ([c7c5e5b](https://github.com/ReVanced/revanced-patches/commit/c7c5e5b2b9cf63d8225bb6bd5e735ddf945b6c29)) + +## [5.5.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.5.2-dev.1...v5.5.2-dev.2) (2024-12-17) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Hide new kind of community post ([#4155](https://github.com/ReVanced/revanced-patches/issues/4155)) ([08f68cb](https://github.com/ReVanced/revanced-patches/commit/08f68cb5d33f2cfe656d2f93d159c69981f31418)) + +## [5.5.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.5.1...v5.5.2-dev.1) (2024-12-17) + + +### Bug Fixes + +* **YouTube - Miniplayer:** Use estimated maximum on screen size for devices with low density screens ([#4150](https://github.com/ReVanced/revanced-patches/issues/4150)) ([2694158](https://github.com/ReVanced/revanced-patches/commit/2694158c3c9935ede21c96832533222f850068df)) +* **YouTube - SponsorBlock:** Show create new segment error messages using a dialog ([#4148](https://github.com/ReVanced/revanced-patches/issues/4148)) ([5870906](https://github.com/ReVanced/revanced-patches/commit/587090636dfff0b358b15026cf7d47c65a4296dc)) + +## [5.5.1](https://github.com/ReVanced/revanced-patches/compare/v5.5.0...v5.5.1) (2024-12-16) + + +### Bug Fixes + +* **YouTube:** Fix string translations ([52e04d3](https://github.com/ReVanced/revanced-patches/commit/52e04d340c1a85f3d683c67a15ae96529432d5fe)) + +## [5.5.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.5.0...v5.5.1-dev.1) (2024-12-16) + + +### Bug Fixes + +* **YouTube:** Fix string translations ([52e04d3](https://github.com/ReVanced/revanced-patches/commit/52e04d340c1a85f3d683c67a15ae96529432d5fe)) + +# [5.5.0](https://github.com/ReVanced/revanced-patches/compare/v5.4.0...v5.5.0) (2024-12-16) + + +### Bug Fixes + +* **Twitch:** Change recommended target to the latest app version ([fb32972](https://github.com/ReVanced/revanced-patches/commit/fb32972f4de92dac1fc5d73f56a392a671c4e94b)) +* **YouTube - Spoof video streams:** Make livestreams start at the current time when using iOS client ([#4137](https://github.com/ReVanced/revanced-patches/issues/4137)) ([140f484](https://github.com/ReVanced/revanced-patches/commit/140f484b4b251b0dfa94163a63f61f45f5302052)) +* **YouTube Music:** Add `Spoof client patch` to fix playback ([#4132](https://github.com/ReVanced/revanced-patches/issues/4132)) ([b092508](https://github.com/ReVanced/revanced-patches/commit/b0925088e8b41636e285cb234593d545604ce461)) + + +### Features + +* **YouTube - Hide feed components:** Remove obsolete `Hide search result shelf header` option ([#4134](https://github.com/ReVanced/revanced-patches/issues/4134)) ([c71443a](https://github.com/ReVanced/revanced-patches/commit/c71443a08883ab10ef2553213c03b00e7c580a43)) +* **YouTube - Navigation buttons:** Add options to disable translucent status bar and navigation bar ([#4133](https://github.com/ReVanced/revanced-patches/issues/4133)) ([a2d2141](https://github.com/ReVanced/revanced-patches/commit/a2d2141cec9b0b4929e07a8010889b21c324b229)) +* **YouTube:** Add `Force original audio` patch ([#4122](https://github.com/ReVanced/revanced-patches/issues/4122)) ([f4aa440](https://github.com/ReVanced/revanced-patches/commit/f4aa4406080b91f01d623e54b11b99ea849ddcdf)) + +# [5.5.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.5.0-dev.4...v5.5.0-dev.5) (2024-12-16) + + +### Features + +* **YouTube - Navigation buttons:** Add options to disable translucent status bar and navigation bar ([#4133](https://github.com/ReVanced/revanced-patches/issues/4133)) ([a2d2141](https://github.com/ReVanced/revanced-patches/commit/a2d2141cec9b0b4929e07a8010889b21c324b229)) + +# [5.5.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.5.0-dev.3...v5.5.0-dev.4) (2024-12-16) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Make livestreams start at the current time when using iOS client ([#4137](https://github.com/ReVanced/revanced-patches/issues/4137)) ([140f484](https://github.com/ReVanced/revanced-patches/commit/140f484b4b251b0dfa94163a63f61f45f5302052)) + +# [5.5.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.5.0-dev.2...v5.5.0-dev.3) (2024-12-16) + + +### Features + +* **YouTube - Hide feed components:** Remove obsolete `Hide search result shelf header` option ([#4134](https://github.com/ReVanced/revanced-patches/issues/4134)) ([c71443a](https://github.com/ReVanced/revanced-patches/commit/c71443a08883ab10ef2553213c03b00e7c580a43)) + +# [5.5.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.5.0-dev.1...v5.5.0-dev.2) (2024-12-16) + + +### Bug Fixes + +* **YouTube Music:** Add `Spoof client patch` to fix playback ([#4132](https://github.com/ReVanced/revanced-patches/issues/4132)) ([b092508](https://github.com/ReVanced/revanced-patches/commit/b0925088e8b41636e285cb234593d545604ce461)) + +# [5.5.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.4.1-dev.1...v5.5.0-dev.1) (2024-12-15) + + +### Features + +* **YouTube:** Add `Force original audio` patch ([#4122](https://github.com/ReVanced/revanced-patches/issues/4122)) ([f4aa440](https://github.com/ReVanced/revanced-patches/commit/f4aa4406080b91f01d623e54b11b99ea849ddcdf)) + +## [5.4.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.4.0...v5.4.1-dev.1) (2024-12-14) + + +### Bug Fixes + +* **Twitch:** Change recommended target to the latest app version ([fb32972](https://github.com/ReVanced/revanced-patches/commit/fb32972f4de92dac1fc5d73f56a392a671c4e94b)) + +# [5.4.0](https://github.com/ReVanced/revanced-patches/compare/v5.3.0...v5.4.0) (2024-12-14) + + +### Bug Fixes + +* **GmsCore support:** Adjust presentation of battery optimization dialog ([#4091](https://github.com/ReVanced/revanced-patches/issues/4091)) ([5d8fc1b](https://github.com/ReVanced/revanced-patches/commit/5d8fc1bcd4e453298cfac086cdbdf279612bfb63)) +* **TikTok - Settings:** Use correct colors for dark mode ([#4087](https://github.com/ReVanced/revanced-patches/issues/4087)) ([6bd22ff](https://github.com/ReVanced/revanced-patches/commit/6bd22ffa7e8af4d8f5d2d3b1711bd92c44b4e4aa)) +* **TikTok - SIM Spoof:** Change patch to default off to fix login ([#4084](https://github.com/ReVanced/revanced-patches/issues/4084)) ([f4659a3](https://github.com/ReVanced/revanced-patches/commit/f4659a328eaf600e1e5f02a66fa2af4b6d8dc7c1)) +* **YouTube - Hide ads:** Hide new type of featured promotions ([#4113](https://github.com/ReVanced/revanced-patches/issues/4113)) ([13c7592](https://github.com/ReVanced/revanced-patches/commit/13c7592b21defd27e3a7aa9b219ffc0247bb5914)) +* **YouTube - Spoof video streams:** Fix error toast that is sometimes shown ([#4090](https://github.com/ReVanced/revanced-patches/issues/4090)) ([4c46cb2](https://github.com/ReVanced/revanced-patches/commit/4c46cb27a02c6f29626cd769b6a8e825645d5b16)) +* **YouTube - Spoof video streams:** Resolve playback of age restricted videos ([#4096](https://github.com/ReVanced/revanced-patches/issues/4096)) ([839a404](https://github.com/ReVanced/revanced-patches/commit/839a4045f1bb1759d89047834e0b7695781e82a3)) +* **YouTube Music - Bypass certificate checks:** Add a recommended target version ([#4104](https://github.com/ReVanced/revanced-patches/issues/4104)) ([17a5a6c](https://github.com/ReVanced/revanced-patches/commit/17a5a6c1691b0c23f601d3355b72f122c2bd5dcb)) +* **YouTube Music - Spoof video streams:** Disable stable volume ([#4097](https://github.com/ReVanced/revanced-patches/issues/4097)) ([16bb9df](https://github.com/ReVanced/revanced-patches/commit/16bb9dfc299612f3922724c136878606987ab132)) + + +### Features + +* Add Internal data documents provider patch ([#3830](https://github.com/ReVanced/revanced-patches/issues/3830)) ([cb22f65](https://github.com/ReVanced/revanced-patches/commit/cb22f652ed678d81ffda9ece659b3971225d6931)) +* **Change package name:** Add options to change provider and permission package names to handle installation conflicts ([75c740c](https://github.com/ReVanced/revanced-patches/commit/75c740c6ba2e0c62e567f7dc90cdad368fc4f372)) +* **Twitch:** Make patches compatible with latest versions ([#4099](https://github.com/ReVanced/revanced-patches/issues/4099)) ([eecfbb7](https://github.com/ReVanced/revanced-patches/commit/eecfbb7122a9072e55e687f2c003f63108654888)) +* **YouTube - Comments:** Add `Hide 'Chat summary'` ([#4110](https://github.com/ReVanced/revanced-patches/issues/4110)) ([269493c](https://github.com/ReVanced/revanced-patches/commit/269493cd198604f1438ea2850fb68fe900d0e56f)) + +# [5.4.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.10...v5.4.0-dev.11) (2024-12-14) + + +### Features + +* **Twitch:** Make patches compatible with latest versions ([#4099](https://github.com/ReVanced/revanced-patches/issues/4099)) ([eecfbb7](https://github.com/ReVanced/revanced-patches/commit/eecfbb7122a9072e55e687f2c003f63108654888)) + +# [5.4.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.9...v5.4.0-dev.10) (2024-12-13) + + +### Bug Fixes + +* **YouTube - Hide ads:** Hide new type of featured promotions ([#4113](https://github.com/ReVanced/revanced-patches/issues/4113)) ([13c7592](https://github.com/ReVanced/revanced-patches/commit/13c7592b21defd27e3a7aa9b219ffc0247bb5914)) + +# [5.4.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.8...v5.4.0-dev.9) (2024-12-12) + + +### Features + +* **YouTube - Comments:** Add `Hide 'Chat summary'` ([#4110](https://github.com/ReVanced/revanced-patches/issues/4110)) ([269493c](https://github.com/ReVanced/revanced-patches/commit/269493cd198604f1438ea2850fb68fe900d0e56f)) + +# [5.4.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.7...v5.4.0-dev.8) (2024-12-11) + + +### Bug Fixes + +* **YouTube Music - Bypass certificate checks:** Add a recommended target version ([#4104](https://github.com/ReVanced/revanced-patches/issues/4104)) ([17a5a6c](https://github.com/ReVanced/revanced-patches/commit/17a5a6c1691b0c23f601d3355b72f122c2bd5dcb)) + +# [5.4.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.6...v5.4.0-dev.7) (2024-12-10) + + +### Bug Fixes + +* **GmsCore support:** Adjust presentation of battery optimization dialog ([#4091](https://github.com/ReVanced/revanced-patches/issues/4091)) ([5d8fc1b](https://github.com/ReVanced/revanced-patches/commit/5d8fc1bcd4e453298cfac086cdbdf279612bfb63)) + +# [5.4.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.5...v5.4.0-dev.6) (2024-12-10) + + +### Bug Fixes + +* **YouTube Music - Spoof video streams:** Disable stable volume ([#4097](https://github.com/ReVanced/revanced-patches/issues/4097)) ([16bb9df](https://github.com/ReVanced/revanced-patches/commit/16bb9dfc299612f3922724c136878606987ab132)) + +# [5.4.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.4...v5.4.0-dev.5) (2024-12-10) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Resolve playback of age restricted videos ([#4096](https://github.com/ReVanced/revanced-patches/issues/4096)) ([839a404](https://github.com/ReVanced/revanced-patches/commit/839a4045f1bb1759d89047834e0b7695781e82a3)) + +# [5.4.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.3...v5.4.0-dev.4) (2024-12-10) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Fix error toast that is sometimes shown ([#4090](https://github.com/ReVanced/revanced-patches/issues/4090)) ([4c46cb2](https://github.com/ReVanced/revanced-patches/commit/4c46cb27a02c6f29626cd769b6a8e825645d5b16)) + +# [5.4.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.2...v5.4.0-dev.3) (2024-12-09) + + +### Bug Fixes + +* **TikTok - Settings:** Use correct colors for dark mode ([#4087](https://github.com/ReVanced/revanced-patches/issues/4087)) ([6bd22ff](https://github.com/ReVanced/revanced-patches/commit/6bd22ffa7e8af4d8f5d2d3b1711bd92c44b4e4aa)) + +# [5.4.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.4.0-dev.1...v5.4.0-dev.2) (2024-12-09) + + +### Bug Fixes + +* **TikTok - SIM Spoof:** Change patch to default off to fix login ([#4084](https://github.com/ReVanced/revanced-patches/issues/4084)) ([f4659a3](https://github.com/ReVanced/revanced-patches/commit/f4659a328eaf600e1e5f02a66fa2af4b6d8dc7c1)) + + +### Features + +* Add Internal data documents provider patch ([#3830](https://github.com/ReVanced/revanced-patches/issues/3830)) ([cb22f65](https://github.com/ReVanced/revanced-patches/commit/cb22f652ed678d81ffda9ece659b3971225d6931)) + +# [5.4.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.3.0...v5.4.0-dev.1) (2024-12-09) + + +### Features + +* **Change package name:** Add options to change provider and permission package names to handle installation conflicts ([75c740c](https://github.com/ReVanced/revanced-patches/commit/75c740c6ba2e0c62e567f7dc90cdad368fc4f372)) + +# [5.3.0](https://github.com/ReVanced/revanced-patches/compare/v5.2.3...v5.3.0) (2024-12-09) + + +### Bug Fixes + +* **Change package name:** Prevent applying the patch to known incompatible apps ([#3943](https://github.com/ReVanced/revanced-patches/issues/3943)) ([44936e7](https://github.com/ReVanced/revanced-patches/commit/44936e71e846f72f7279950232a5dba37765ceb3)) +* **Reddit:** Fix patches by using correct extension class ([70bdc68](https://github.com/ReVanced/revanced-patches/commit/70bdc6840d465399625aa1ae0259f49e72711955)) +* **Sync for Reddit:** Fix patches by using correct extension name ([030093e](https://github.com/ReVanced/revanced-patches/commit/030093e913aab3fab43935eedbaeba0f6c0491bb)) +* **Twitter:** Merge correct extension by depending on correct extension patch ([8281cf6](https://github.com/ReVanced/revanced-patches/commit/8281cf6a3eead8cc25a277371e0b0ab2be982497)) +* **YouTube - Spoof video streams:** Add missing preferred language preference to the settings ([630633c](https://github.com/ReVanced/revanced-patches/commit/630633cf57c65c65e5578046413e17670ae336e8)) +* **YouTube - Spoof video streams:** Enable opus codec by updating iOS client version ([#4063](https://github.com/ReVanced/revanced-patches/issues/4063)) ([0af156f](https://github.com/ReVanced/revanced-patches/commit/0af156f18972c5f089af4bb69824968d2a47d18f)) +* **YouTube - Spoof video streams:** Update `Force AVC` client data ([#4064](https://github.com/ReVanced/revanced-patches/issues/4064)) ([7d537dd](https://github.com/ReVanced/revanced-patches/commit/7d537ddff4bb5421fa320741275131a66ef5c7bb)) +* **YouTube Music - Permanent shuffle:** Remove obsolete and non functional patch ([#4073](https://github.com/ReVanced/revanced-patches/issues/4073)) ([fbc6ab6](https://github.com/ReVanced/revanced-patches/commit/fbc6ab6a357b351f02d4d486ddc2072cf53199c3)) + + +### Features + +* **Nyx:** Remove broken `Unlock pro` patch ([1fe8b16](https://github.com/ReVanced/revanced-patches/commit/1fe8b164eab0c4fa80ab2da2581977f5111a2858)) +* **YouTube - Spoof video streams:** Allow picking a default audio language track ([#4050](https://github.com/ReVanced/revanced-patches/issues/4050)) ([ede666b](https://github.com/ReVanced/revanced-patches/commit/ede666b5cb64fcbaa1334ad8bef79e2634ced113)) +* **YouTube Music:** Add `Spoof video streams` patch to fix playback ([#4065](https://github.com/ReVanced/revanced-patches/issues/4065)) ([cf3116a](https://github.com/ReVanced/revanced-patches/commit/cf3116a7583d09c25c798a85687a056f143656f0)) +* **YouTube:** Add `Open videos fullscreen` patch ([#4069](https://github.com/ReVanced/revanced-patches/issues/4069)) ([296d63b](https://github.com/ReVanced/revanced-patches/commit/296d63bd42c338a01efbcb2df702e5822d05a5f1)) + +# [5.3.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.3.0-dev.6...v5.3.0-dev.7) (2024-12-09) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Add missing preferred language preference to the settings ([630633c](https://github.com/ReVanced/revanced-patches/commit/630633cf57c65c65e5578046413e17670ae336e8)) + +# [5.3.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.3.0-dev.5...v5.3.0-dev.6) (2024-12-09) + + +### Features + +* **YouTube - Spoof video streams:** Allow picking a default audio language track ([#4050](https://github.com/ReVanced/revanced-patches/issues/4050)) ([ede666b](https://github.com/ReVanced/revanced-patches/commit/ede666b5cb64fcbaa1334ad8bef79e2634ced113)) + +# [5.3.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.3.0-dev.4...v5.3.0-dev.5) (2024-12-09) + + +### Bug Fixes + +* **Change package name:** Prevent applying the patch to known incompatible apps ([#3943](https://github.com/ReVanced/revanced-patches/issues/3943)) ([44936e7](https://github.com/ReVanced/revanced-patches/commit/44936e71e846f72f7279950232a5dba37765ceb3)) +* **YouTube Music - Permanent shuffle:** Remove obsolete and non functional patch ([#4073](https://github.com/ReVanced/revanced-patches/issues/4073)) ([fbc6ab6](https://github.com/ReVanced/revanced-patches/commit/fbc6ab6a357b351f02d4d486ddc2072cf53199c3)) + + +### Features + +* **YouTube:** Add `Open videos fullscreen` patch ([#4069](https://github.com/ReVanced/revanced-patches/issues/4069)) ([296d63b](https://github.com/ReVanced/revanced-patches/commit/296d63bd42c338a01efbcb2df702e5822d05a5f1)) + +# [5.3.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.3.0-dev.3...v5.3.0-dev.4) (2024-12-09) + + +### Features + +* **Nyx:** Remove broken `Unlock pro` patch ([1fe8b16](https://github.com/ReVanced/revanced-patches/commit/1fe8b164eab0c4fa80ab2da2581977f5111a2858)) + +# [5.3.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.3.0-dev.2...v5.3.0-dev.3) (2024-12-09) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Update `Force AVC` client data ([#4064](https://github.com/ReVanced/revanced-patches/issues/4064)) ([7d537dd](https://github.com/ReVanced/revanced-patches/commit/7d537ddff4bb5421fa320741275131a66ef5c7bb)) + +# [5.3.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.3.0-dev.1...v5.3.0-dev.2) (2024-12-08) + + +### Bug Fixes + +* **Reddit:** Fix patches by using correct extension class ([70bdc68](https://github.com/ReVanced/revanced-patches/commit/70bdc6840d465399625aa1ae0259f49e72711955)) + +# [5.3.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.4-dev.3...v5.3.0-dev.1) (2024-12-08) + + +### Features + +* **YouTube Music:** Add `Spoof video streams` patch to fix playback ([#4065](https://github.com/ReVanced/revanced-patches/issues/4065)) ([cf3116a](https://github.com/ReVanced/revanced-patches/commit/cf3116a7583d09c25c798a85687a056f143656f0)) + +## [5.2.4-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.4-dev.2...v5.2.4-dev.3) (2024-12-07) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Enable opus codec by updating iOS client version ([#4063](https://github.com/ReVanced/revanced-patches/issues/4063)) ([0af156f](https://github.com/ReVanced/revanced-patches/commit/0af156f18972c5f089af4bb69824968d2a47d18f)) + +## [5.2.4-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.4-dev.1...v5.2.4-dev.2) (2024-12-07) + + +### Bug Fixes + +* **Sync for Reddit:** Fix patches by using correct extension name ([030093e](https://github.com/ReVanced/revanced-patches/commit/030093e913aab3fab43935eedbaeba0f6c0491bb)) + +## [5.2.4-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.3...v5.2.4-dev.1) (2024-12-07) + + +### Bug Fixes + +* **Twitter:** Merge correct extension by depending on correct extension patch ([8281cf6](https://github.com/ReVanced/revanced-patches/commit/8281cf6a3eead8cc25a277371e0b0ab2be982497)) + +## [5.2.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3) (2024-12-06) + + +### Bug Fixes + +* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b)) + +## [5.2.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.2...v5.2.3-dev.1) (2024-12-06) + + +### Bug Fixes + +* **YouTube Music - GmsCore support:** Resolve patching errors ([#4056](https://github.com/ReVanced/revanced-patches/issues/4056)) ([38a4bad](https://github.com/ReVanced/revanced-patches/commit/38a4bad5b890e3906d77d22efeabd8f38653508b)) + +## [5.2.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2) (2024-12-06) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([4017185](https://github.com/ReVanced/revanced-patches/commit/4017185e760c0569e6644b94bbe66a84fa245b4b)) + ## [5.2.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.1...v5.2.2-dev.1) (2024-12-05) diff --git a/README.md b/README.md index a7ebabe533..a7a9db8855 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,9 @@ Thank you for considering contributing to ReVanced Patches. You can find the con To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation). -## 📜 Licence +## 📜 License ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information. [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files. Any modifications to ReVanced Patches must also be made available under the GPL, -along with build & install instructions. \ No newline at end of file +along with build & install instructions. diff --git a/adsfund.json b/adsfund.json new file mode 100644 index 0000000000..f451581bb5 --- /dev/null +++ b/adsfund.json @@ -0,0 +1,8 @@ +{ + "info": "This is verification file for ads.fund project", + "project": { + "name": "Revanced Patches", + "walletAddress": "0x7ab4091e00363654bf84B34151225742cd92FCE5", + "tokenAddress": "0xadf325f255083a3f3d9a9d01ffb3db52a148d802" + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000000..fcaeeb54cd --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { + alias(libs.plugins.android.library) apply false +} diff --git a/crowdin.yml b/crowdin.yml index 148f321cd2..81022c88c4 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,8 +1,9 @@ project_id_env: "CROWDIN_PROJECT_ID" api_token_env: "CROWDIN_PERSONAL_TOKEN" -preserve_hierarchy: false +preserve_hierarchy: true files: - source: patches/src/main/resources/addresources/values/strings.xml + dest: patches.xml translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml skip_untranslated_strings: true diff --git a/extensions/all/misc/adb/hide-adb/build.gradle.kts b/extensions/all/misc/adb/hide-adb/build.gradle.kts new file mode 100644 index 0000000000..42eb9984c0 --- /dev/null +++ b/extensions/all/misc/adb/hide-adb/build.gradle.kts @@ -0,0 +1,9 @@ +android { + defaultConfig { + minSdk = 21 + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/adb/hide-adb/src/main/AndroidManifest.xml b/extensions/all/misc/adb/hide-adb/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/all/misc/adb/hide-adb/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/all/misc/adb/hide-adb/src/main/java/app/revanced/extension/all/misc/hide/adb/HideAdbPatch.java b/extensions/all/misc/adb/hide-adb/src/main/java/app/revanced/extension/all/misc/hide/adb/HideAdbPatch.java new file mode 100644 index 0000000000..f0b4f458be --- /dev/null +++ b/extensions/all/misc/adb/hide-adb/src/main/java/app/revanced/extension/all/misc/hide/adb/HideAdbPatch.java @@ -0,0 +1,28 @@ +package app.revanced.extension.all.misc.hide.adb; + +import android.content.ContentResolver; +import android.provider.Settings; + +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("unused") +public final class HideAdbPatch { + private static final List SPOOF_SETTINGS = Arrays.asList("adb_enabled", "adb_wifi_enabled", "development_settings_enabled"); + + public static int getInt(ContentResolver cr, String name) throws Settings.SettingNotFoundException { + if (SPOOF_SETTINGS.contains(name)) { + return 0; + } + + return Settings.Global.getInt(cr, name); + } + + public static int getInt(ContentResolver cr, String name, int def) { + if (SPOOF_SETTINGS.contains(name)) { + return 0; + } + + return Settings.Global.getInt(cr, name, def); + } +} diff --git a/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/build.gradle.kts b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/build.gradle.kts index bc416f685c..c269c9862f 100644 --- a/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/build.gradle.kts +++ b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/build.gradle.kts @@ -1,4 +1,8 @@ -android.namespace = "app.revanced.extension" +android { + defaultConfig { + minSdk = 23 + } +} dependencies { compileOnly(libs.annotation) diff --git a/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/connectivity/wifi/spoof/SpoofWifiPatch.java b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.java similarity index 99% rename from extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/connectivity/wifi/spoof/SpoofWifiPatch.java rename to extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.java index 5f00bd730f..9c1702f1bc 100644 --- a/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/connectivity/wifi/spoof/SpoofWifiPatch.java +++ b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.java @@ -1,4 +1,4 @@ -package app.revanced.extension.all.connectivity.wifi.spoof; +package app.revanced.extension.all.misc.connectivity.wifi.spoof; import android.app.PendingIntent; import android.content.Context; @@ -12,7 +12,7 @@ import android.os.Handler; import androidx.annotation.RequiresApi; -/** @noinspection deprecation, unused */ +@SuppressWarnings({"deprecation", "unused"}) public class SpoofWifiPatch { // Used to check what the (real or fake) active network is (take a look at `hasTransport`). diff --git a/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/build.gradle.kts b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/build.gradle.kts new file mode 100644 index 0000000000..42eb9984c0 --- /dev/null +++ b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/build.gradle.kts @@ -0,0 +1,9 @@ +android { + defaultConfig { + minSdk = 21 + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/AndroidManifest.xml b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java new file mode 100644 index 0000000000..ad9d48f6ec --- /dev/null +++ b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java @@ -0,0 +1,339 @@ +package app.revanced.extension.all.misc.directory.documentsprovider; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.DocumentsProvider; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStat; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Objects; + +/** + * A DocumentsProvider that allows access to the app's internal data directory. + */ +@SuppressLint("LongLogTag") +public class InternalDataDocumentsProvider extends DocumentsProvider { + private static final String[] rootColumns = + {"root_id", "mime_types", "flags", "icon", "title", "summary", "document_id"}; + 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 String packageName; + private File dataDirectory; + + /** + * Recursively delete a file or directory and all its children. + * + * @param root The file or directory to delete. + * @return True if the file or directory and all its children were successfully deleted. + */ + private static boolean deleteRecursively(File root) { + // If root is a directory, delete all children first + 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) { + File[] files = root.listFiles(); + if (files != null) { + for (File file : files) { + if (!deleteRecursively(file)) { + return false; + } + } + } + } + } catch (ErrnoException e) { + Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e); + } + } + + // Delete file or empty directory + return root.delete(); + } + + /** + * Resolve the MIME type of a file based on its extension. + * + * @param file The file to resolve the MIME type for. + * @return The MIME type of the file. + */ + private static String resolveMimeType(File file) { + if (file.isDirectory()) { + return DocumentsContract.Document.MIME_TYPE_DIR; + } + + String name = file.getName(); + int indexOfExtDot = name.lastIndexOf('.'); + if (indexOfExtDot < 0) { + // No extension + return "application/octet-stream"; + } + + String extension = name.substring(indexOfExtDot + 1).toLowerCase(); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + return mimeType != null ? mimeType : "application/octet-stream"; + } + + @Override + public final boolean onCreate() { + return true; + } + + @Override + public final void attachInfo(Context context, ProviderInfo providerInfo) { + super.attachInfo(context, providerInfo); + + this.packageName = context.getPackageName(); + this.dataDirectory = context.getFilesDir().getParentFile(); + } + + @Override + public final String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { + File directory = resolveDocumentId(parentDocumentId); + File file = new File(directory, displayName); + + // If file already exists, append a number to the name + int i = 2; + while (file.exists()) { + file = new File(directory, displayName + " (" + i + ")"); + i++; + } + + try { + // Create the file or directory + if (mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR) ? file.mkdir() : file.createNewFile()) { + // Return the document ID of the new entity + if (!parentDocumentId.endsWith("/")) { + parentDocumentId = parentDocumentId + "/"; + } + return parentDocumentId + file.getName(); + } + } catch (IOException e) { + // Do nothing. We are throwing a FileNotFoundException later if the file could not be created. + } + throw new FileNotFoundException("Failed to create document in " + parentDocumentId + " with name " + displayName); + } + + @Override + public final void deleteDocument(String documentId) throws FileNotFoundException { + File file = resolveDocumentId(documentId); + if (!deleteRecursively(file)) { + throw new FileNotFoundException("Failed to delete document " + documentId); + } + } + + @Override + public final String getDocumentType(String documentId) throws FileNotFoundException { + return resolveMimeType(resolveDocumentId(documentId)); + } + + @Override + public final boolean isChildDocument(String parentDocumentId, String documentId) { + return documentId.startsWith(parentDocumentId); + } + + @Override + public final String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException { + File source = resolveDocumentId(sourceDocumentId); + File dest = resolveDocumentId(targetParentDocumentId); + + File file = new File(dest, source.getName()); + if (!file.exists() && source.renameTo(file)) { + // Return the new document ID + if (targetParentDocumentId.endsWith("/")) { + return targetParentDocumentId + file.getName(); + } + return targetParentDocumentId + "/" + file.getName(); + } + + throw new FileNotFoundException("Failed to move document from " + sourceDocumentId + " to " + targetParentDocumentId); + } + + @Override + public final ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { + File file = resolveDocumentId(documentId); + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode)); + } + + @Override + public final Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { + if (parentDocumentId.endsWith("/")) { + parentDocumentId = parentDocumentId.substring(0, parentDocumentId.length() - 1); + } + + if (projection == null) { + projection = directoryColumns; + } + + MatrixCursor cursor = new MatrixCursor(projection); + File children = resolveDocumentId(parentDocumentId); + + // Collect all children + File[] files = children.listFiles(); + if (files != null) { + for (File file : files) { + addRowForDocument(cursor, parentDocumentId + "/" + file.getName(), file); + } + } + return cursor; + } + + @Override + public final Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { + if (projection == null) { + projection = directoryColumns; + } + + MatrixCursor cursor = new MatrixCursor(projection); + addRowForDocument(cursor, documentId, null); + return cursor; + } + + @Override + public final Cursor queryRoots(String[] projection) { + ApplicationInfo info = Objects.requireNonNull(getContext()).getApplicationInfo(); + String appName = info.loadLabel(getContext().getPackageManager()).toString(); + + if (projection == null) { + projection = rootColumns; + } + + MatrixCursor cursor = new MatrixCursor(projection); + MatrixCursor.RowBuilder row = cursor.newRow(); + row.add(DocumentsContract.Root.COLUMN_ROOT_ID, this.packageName); + row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, this.packageName); + row.add(DocumentsContract.Root.COLUMN_SUMMARY, this.packageName); + row.add(DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.FLAG_LOCAL_ONLY | + DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD); + row.add(DocumentsContract.Root.COLUMN_TITLE, appName); + row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*"); + row.add(DocumentsContract.Root.COLUMN_ICON, info.icon); + return cursor; + } + + @Override + public final void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException { + deleteDocument(documentId); + } + + @Override + public final String renameDocument(String documentId, String displayName) throws FileNotFoundException { + File file = resolveDocumentId(documentId); + if (!file.renameTo(new File(file.getParentFile(), displayName))) { + throw new FileNotFoundException("Failed to rename document from " + documentId + " to " + displayName); + } + + // Return the new document ID + return documentId.substring(0, documentId.lastIndexOf('/', documentId.length() - 2)) + "/" + displayName; + } + + /** + * Resolve a file instance for a given document ID. + * + * @param fullContentPath The document ID to resolve. + * @return File object for the given document ID. + * @throws FileNotFoundException If the document ID is invalid or the file does not exist. + */ + private File resolveDocumentId(String fullContentPath) throws FileNotFoundException { + if (!fullContentPath.startsWith(this.packageName)) { + throw new FileNotFoundException(fullContentPath + " not found"); + } + String path = fullContentPath.substring(this.packageName.length()); + + // Resolve the relative path within /data/data/{PKG} + File file; + if (path.equals("/") || path.isEmpty()) { + file = this.dataDirectory; + } else { + // Remove leading slash + String relativePath = path.substring(1); + file = new File(this.dataDirectory, relativePath); + } + + if (!file.exists()) { + throw new FileNotFoundException(fullContentPath + " not found"); + } + return file; + } + + /** + * Add a row containing all file properties to a MatrixCursor for a given document ID. + * + * @param cursor The cursor to add the row to. + * @param documentId The document ID to add the row for. + * @param file The file to add the row for. If null, the file will be resolved from the document ID. + * @throws FileNotFoundException If the file does not exist. + */ + private void addRowForDocument(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException { + if (file == null) { + file = resolveDocumentId(documentId); + } + + int flags = 0; + if (file.isDirectory()) { + // Prefer list view for directories + flags = flags | DocumentsContract.Document.FLAG_DIR_PREFERS_LAST_MODIFIED; + } + + if (file.canWrite()) { + if (file.isDirectory()) { + flags = flags | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE; + } + + flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_WRITE | + DocumentsContract.Document.FLAG_SUPPORTS_DELETE | + DocumentsContract.Document.FLAG_SUPPORTS_RENAME | + DocumentsContract.Document.FLAG_SUPPORTS_MOVE; + } + + MatrixCursor.RowBuilder row = cursor.newRow(); + row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId); + row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.getName()); + row.add(DocumentsContract.Document.COLUMN_SIZE, file.length()); + row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, resolveMimeType(file)); + row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified()); + row.add(DocumentsContract.Document.COLUMN_FLAGS, flags); + + // Custom columns + row.add("full_path", file.getAbsolutePath()); + + // Add lstat column + String path = file.getPath(); + try { + StringBuilder sb = new StringBuilder(); + StructStat lstat = Os.lstat(path); + sb.append(lstat.st_mode); + sb.append(";"); + sb.append(lstat.st_uid); + sb.append(";"); + sb.append(lstat.st_gid); + // Append symlink target if it is a symlink + if ((lstat.st_mode & S_IFMT) == S_IFLNK) { + sb.append(";"); + sb.append(Os.readlink(path)); + } + row.add("lstat_info", sb.toString()); + } catch (Exception ex) { + Log.e("InternalDocumentsProvider", "Failed to get lstat info for " + path, ex); + } + } +} diff --git a/extensions/all/misc/disable-play-integrity/build.gradle.kts b/extensions/all/misc/disable-play-integrity/build.gradle.kts new file mode 100644 index 0000000000..b3a57874e5 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/build.gradle.kts @@ -0,0 +1,13 @@ +android { + defaultConfig { + minSdk = 21 + } + + buildFeatures { + aidl = true + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl new file mode 100644 index 0000000000..7b8f59f1d1 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityService.aidl @@ -0,0 +1,8 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; + +interface IExpressIntegrityService { + oneway void requestIntegrityToken(in Bundle request, IExpressIntegrityServiceCallback callback) = 2; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl new file mode 100644 index 0000000000..624167afb0 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IExpressIntegrityServiceCallback.aidl @@ -0,0 +1,5 @@ +package com.google.android.play.core.integrity.protocol; + +interface IExpressIntegrityServiceCallback { + oneway void onRequestExpressIntegrityTokenResult(in Bundle result) = 2; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl new file mode 100644 index 0000000000..bb1bcd5518 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityService.aidl @@ -0,0 +1,8 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; + +interface IIntegrityService { + oneway void requestIntegrityToken(in Bundle request, IIntegrityServiceCallback callback) = 1; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl new file mode 100644 index 0000000000..9485ec1694 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/aidl/com/google/android/play/core/integrity/protocol/IIntegrityServiceCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.play.core.integrity.protocol; + +import android.os.Bundle; + +interface IIntegrityServiceCallback { + oneway void onResult(in Bundle result) = 1; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java new file mode 100644 index 0000000000..31c2ca6dba --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/ext/PackageId.java @@ -0,0 +1,10 @@ +package android.ext; +/** @hide */ +// Int values that are assigned to packages in this interface can be retrieved at runtime from +// ApplicationInfo.ext().getPackageId() or from AndroidPackage.ext().getPackageId() (in system_server). +// +// PackageIds are assigned to parsed APKs only after they are verified, either by a certificate check +// or by a check that the APK is stored on an immutable OS partition. +public interface PackageId { + String PLAY_STORE_NAME = "com.android.vending"; +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java new file mode 100644 index 0000000000..a01806441a --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/android/os/BinderWrapper.java @@ -0,0 +1,62 @@ +package android.os; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.FileDescriptor; + +/** @hide */ +public class BinderWrapper implements IBinder { + protected final IBinder base; + + public BinderWrapper(IBinder base) { + this.base = base; + } + + @Override + public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + return base.transact(code, data, reply, flags); + } + + @Nullable + @Override + public IInterface queryLocalInterface(@NonNull String descriptor) { + return base.queryLocalInterface(descriptor); + } + + @Nullable + @Override + public String getInterfaceDescriptor() throws RemoteException { + return base.getInterfaceDescriptor(); + } + + @Override + public boolean pingBinder() { + return base.pingBinder(); + } + + @Override + public boolean isBinderAlive() { + return base.isBinderAlive(); + } + + @Override + public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dump(fd, args); + } + + @Override + public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dumpAsync(fd, args); + } + + @Override + public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException { + base.linkToDeath(recipient, flags); + } + + @Override + public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { + return base.unlinkToDeath(recipient, flags); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java new file mode 100644 index 0000000000..3bd88d2a65 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/ClassicPlayIntegrityServiceWrapper.java @@ -0,0 +1,41 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.internal.os.FakeBackgroundHandler; +import com.google.android.play.core.integrity.protocol.IIntegrityService; +import com.google.android.play.core.integrity.protocol.IIntegrityServiceCallback; + +class ClassicPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { + + ClassicPlayIntegrityServiceWrapper(IBinder base) { + super(base); + requestIntegrityTokenTxnCode = 2; // IIntegrityService.Stub.TRANSACTION_requestIntegrityToken + } + + static class TokenRequestStub extends IIntegrityService.Stub { + public void requestIntegrityToken(Bundle request, IIntegrityServiceCallback callback) { + Runnable r = () -> { + var result = new Bundle(); + // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/IntegrityErrorCode.html#API_NOT_AVAILABLE + final int API_NOT_AVAILABLE = -1; + result.putInt("error", API_NOT_AVAILABLE); + try { + callback.onResult(result); + } catch (RemoteException e) { + Log.e("IIntegrityService.Stub", "", e); + } + }; + FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay()); + } + }; + + @Override + protected Binder createTokenRequestStub() { + return new TokenRequestStub(); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java new file mode 100644 index 0000000000..0418b4fe73 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityServiceWrapper.java @@ -0,0 +1,48 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.os.Binder; +import android.os.BinderWrapper; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.util.Log; + +import androidx.annotation.Nullable; + +abstract class PlayIntegrityServiceWrapper extends BinderWrapper { + final String TAG; + protected int requestIntegrityTokenTxnCode; + + public PlayIntegrityServiceWrapper(IBinder base) { + super(base); + TAG = getClass().getSimpleName(); + } + + protected abstract Binder createTokenRequestStub(); + + @Override + public boolean transact(int code, Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + if (code == requestIntegrityTokenTxnCode) { + if (maybeStubOutIntegrityTokenRequest(code, data, reply, flags)) { + return true; + } + } + return super.transact(code, data, reply, flags); + } + + private boolean maybeStubOutIntegrityTokenRequest(int code, Parcel data, @Nullable Parcel reply, int flags) { + Log.d(TAG, "integrity token request detected"); + + try { + createTokenRequestStub().transact(code, data, reply, flags); + } catch (RemoteException e) { + // this is a local call + throw new IllegalStateException(e); + } + return true; + } + + protected static long getTokenRequestResultDelay() { + return 500L; + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java new file mode 100644 index 0000000000..6ff4720cce --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/PlayIntegrityUtils.java @@ -0,0 +1,35 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.content.Intent; +import android.content.ServiceConnection; +import android.ext.PackageId; +import android.os.IBinder; +import androidx.annotation.Nullable; +import app.grapheneos.gmscompat.lib.util.ServiceConnectionWrapper; +import java.util.function.UnaryOperator; + +public class PlayIntegrityUtils { + + public static @Nullable ServiceConnection maybeReplaceServiceConnection(Intent service, ServiceConnection orig) { + if (PackageId.PLAY_STORE_NAME.equals(service.getPackage())) { + UnaryOperator binderOverride = null; + + final String CLASSIC_SERVICE = + "com.google.android.play.core.integrityservice.BIND_INTEGRITY_SERVICE"; + final String STANDARD_SERVICE = + "com.google.android.play.core.expressintegrityservice.BIND_EXPRESS_INTEGRITY_SERVICE"; + + String action = service.getAction(); + if (STANDARD_SERVICE.equals(action)) { + binderOverride = StandardPlayIntegrityServiceWrapper::new; + } else if (CLASSIC_SERVICE.equals(action)) { + binderOverride = ClassicPlayIntegrityServiceWrapper::new; + } + + if (binderOverride != null) { + return new ServiceConnectionWrapper(orig, binderOverride); + } + } + return null; + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java new file mode 100644 index 0000000000..c1c4937f0c --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/playintegrity/StandardPlayIntegrityServiceWrapper.java @@ -0,0 +1,42 @@ +package app.grapheneos.gmscompat.lib.playintegrity; + +import android.annotation.SuppressLint; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import com.android.internal.os.FakeBackgroundHandler; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityService; +import com.google.android.play.core.integrity.protocol.IExpressIntegrityServiceCallback; + +@SuppressLint("LongLogTag") +class StandardPlayIntegrityServiceWrapper extends PlayIntegrityServiceWrapper { + + StandardPlayIntegrityServiceWrapper(IBinder base) { + super(base); + requestIntegrityTokenTxnCode = 3; // IExpressIntegrityService.Stub.TRANSACTION_requestIntegrityToken + } + + static class TokenRequestStub extends IExpressIntegrityService.Stub { + public void requestIntegrityToken(Bundle request, IExpressIntegrityServiceCallback callback) { + Runnable r = () -> { + var result = new Bundle(); + // https://developer.android.com/google/play/integrity/reference/com/google/android/play/core/integrity/model/StandardIntegrityErrorCode.html#API_NOT_AVAILABLE + final int API_NOT_AVAILABLE = -1; + result.putInt("error", API_NOT_AVAILABLE); + try { + callback.onRequestExpressIntegrityTokenResult(result); + } catch (RemoteException e) { + Log.e("IExpressIntegrityService.Stub", "", e); + } + }; + FakeBackgroundHandler.getHandler().postDelayed(r, getTokenRequestResultDelay()); + } + }; + + @Override + protected Binder createTokenRequestStub() { + return new TokenRequestStub(); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java new file mode 100644 index 0000000000..9edfc39f85 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/grapheneos/gmscompat/lib/util/ServiceConnectionWrapper.java @@ -0,0 +1,49 @@ +package app.grapheneos.gmscompat.lib.util; + +import android.content.ComponentName; +import android.content.ServiceConnection; +import android.os.Build; +import android.os.IBinder; + +import java.util.function.UnaryOperator; + +public class ServiceConnectionWrapper implements ServiceConnection { + private final ServiceConnection base; + private final UnaryOperator binderOverride; + + public ServiceConnectionWrapper(ServiceConnection base, UnaryOperator binderOverride) { + this.base = base; + this.binderOverride = binderOverride; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + IBinder override = binderOverride.apply(service); + if (override != null) { + service = override; + } + } + + base.onServiceConnected(name, service); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + base.onServiceDisconnected(name); + } + + @Override + public void onBindingDied(ComponentName name) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + base.onBindingDied(name); + } + } + + @Override + public void onNullBinding(ComponentName name) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + base.onNullBinding(name); + } + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java new file mode 100644 index 0000000000..4dd09f693f --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/app/revanced/extension/play/DisablePlayIntegrityPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.play; + +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import app.grapheneos.gmscompat.lib.playintegrity.PlayIntegrityUtils; + +public class DisablePlayIntegrityPatch { + public static boolean bindService(Context context, Intent service, ServiceConnection conn, int flags) { + ServiceConnection override = PlayIntegrityUtils.maybeReplaceServiceConnection(service, conn); + if (override != null) { + conn = override; + } + + return context.bindService(service, conn, flags); + } +} diff --git a/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java new file mode 100644 index 0000000000..6b4cb92b45 --- /dev/null +++ b/extensions/all/misc/disable-play-integrity/src/main/java/com/android/internal/os/FakeBackgroundHandler.java @@ -0,0 +1,11 @@ +package com.android.internal.os; + +import android.os.Handler; +import android.os.Looper; + +public class FakeBackgroundHandler { + + public static Handler getHandler() { + return new Handler(Looper.getMainLooper()); + } +} diff --git a/extensions/all/misc/screencapture/remove-screen-capture-restriction/build.gradle.kts b/extensions/all/misc/screencapture/remove-screen-capture-restriction/build.gradle.kts index bc416f685c..42eb9984c0 100644 --- a/extensions/all/misc/screencapture/remove-screen-capture-restriction/build.gradle.kts +++ b/extensions/all/misc/screencapture/remove-screen-capture-restriction/build.gradle.kts @@ -1,4 +1,8 @@ -android.namespace = "app.revanced.extension" +android { + defaultConfig { + minSdk = 21 + } +} dependencies { compileOnly(libs.annotation) diff --git a/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch.java b/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/misc/screencapture/removerestriction/RemoveScreenCaptureRestrictionPatch.java similarity index 78% rename from extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch.java rename to extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/misc/screencapture/removerestriction/RemoveScreenCaptureRestrictionPatch.java index 1dac341441..653d4d3942 100644 --- a/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch.java +++ b/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/misc/screencapture/removerestriction/RemoveScreenCaptureRestrictionPatch.java @@ -1,11 +1,12 @@ -package app.revanced.extension.all.screencapture.removerestriction; +package app.revanced.extension.all.misc.screencapture.removerestriction; import android.media.AudioAttributes; import android.os.Build; import androidx.annotation.RequiresApi; -public final class RemoveScreencaptureRestrictionPatch { +@SuppressWarnings("unused") +public final class RemoveScreenCaptureRestrictionPatch { // Member of AudioAttributes.Builder @RequiresApi(api = Build.VERSION_CODES.Q) public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) { diff --git a/extensions/all/misc/screenshot/remove-screenshot-restriction/build.gradle.kts b/extensions/all/misc/screenshot/remove-screenshot-restriction/build.gradle.kts index 88c859b78f..42eb9984c0 100644 --- a/extensions/all/misc/screenshot/remove-screenshot-restriction/build.gradle.kts +++ b/extensions/all/misc/screenshot/remove-screenshot-restriction/build.gradle.kts @@ -1 +1,9 @@ -android.namespace = "app.revanced.extension" +android { + defaultConfig { + minSdk = 21 + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java b/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/misc/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java similarity index 82% rename from extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java rename to extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/misc/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java index fd5c427d37..22254f6285 100644 --- a/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java +++ b/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/misc/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java @@ -1,8 +1,9 @@ -package app.revanced.extension.all.screenshot.removerestriction; +package app.revanced.extension.all.misc.screenshot.removerestriction; import android.view.Window; import android.view.WindowManager; +@SuppressWarnings("unused") public class RemoveScreenshotRestrictionPatch { public static void addFlags(Window window, int flags) { diff --git a/extensions/baconreader/build.gradle.kts b/extensions/baconreader/build.gradle.kts new file mode 100644 index 0000000000..843fd12cc9 --- /dev/null +++ b/extensions/baconreader/build.gradle.kts @@ -0,0 +1,11 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(libs.annotation) + compileOnly(libs.okhttp) +} + +android { + defaultConfig { + minSdk = 22 + } +} diff --git a/extensions/baconreader/src/main/AndroidManifest.xml b/extensions/baconreader/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/baconreader/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/baconreader/src/main/java/app/revanced/extension/baconreader/FixRedgifsApiPatch.java b/extensions/baconreader/src/main/java/app/revanced/extension/baconreader/FixRedgifsApiPatch.java new file mode 100644 index 0000000000..943da63fae --- /dev/null +++ b/extensions/baconreader/src/main/java/app/revanced/extension/baconreader/FixRedgifsApiPatch.java @@ -0,0 +1,22 @@ +package app.revanced.extension.baconreader; + +import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch; +import okhttp3.OkHttpClient; + +/** + * @noinspection unused + */ +public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch { + static { + INSTANCE = new FixRedgifsApiPatch(); + } + + public String getDefaultUserAgent() { + // BaconReader uses a static user agent for Redgifs API calls + return "BaconReader"; + } + + public static OkHttpClient install(OkHttpClient.Builder builder) { + return builder.addInterceptor(INSTANCE).build(); + } +} diff --git a/extensions/boostforreddit/build.gradle.kts b/extensions/boostforreddit/build.gradle.kts index 54c06871b9..d84b488441 100644 --- a/extensions/boostforreddit/build.gradle.kts +++ b/extensions/boostforreddit/build.gradle.kts @@ -1,4 +1,12 @@ dependencies { compileOnly(project(":extensions:shared:library")) compileOnly(project(":extensions:boostforreddit:stub")) + compileOnly(libs.annotation) + compileOnly(libs.okhttp) +} + +android { + defaultConfig { + minSdk = 21 + } } diff --git a/extensions/boostforreddit/src/main/java/app/revanced/extension/boostforreddit/FixRedgifsApiPatch.java b/extensions/boostforreddit/src/main/java/app/revanced/extension/boostforreddit/FixRedgifsApiPatch.java new file mode 100644 index 0000000000..92757dabf4 --- /dev/null +++ b/extensions/boostforreddit/src/main/java/app/revanced/extension/boostforreddit/FixRedgifsApiPatch.java @@ -0,0 +1,22 @@ +package app.revanced.extension.boostforreddit; + +import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch; +import okhttp3.OkHttpClient; + +/** + * @noinspection unused + */ +public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch { + static { + INSTANCE = new FixRedgifsApiPatch(); + } + + public String getDefaultUserAgent() { + // Boost uses a static user agent for Redgifs API calls + return "Boost"; + } + + public static OkHttpClient createClient() { + return new OkHttpClient.Builder().addInterceptor(INSTANCE).build(); + } +} diff --git a/extensions/boostforreddit/stub/build.gradle.kts b/extensions/boostforreddit/stub/build.gradle.kts index c1cc5794c0..b4bee8809f 100644 --- a/extensions/boostforreddit/stub/build.gradle.kts +++ b/extensions/boostforreddit/stub/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - id(libs.plugins.android.library.get().pluginId) + alias(libs.plugins.android.library) } android { namespace = "app.revanced.extension" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 24 diff --git a/extensions/cricbuzz/build.gradle.kts b/extensions/cricbuzz/build.gradle.kts new file mode 100644 index 0000000000..b09ca9effe --- /dev/null +++ b/extensions/cricbuzz/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:cricbuzz:stub")) +} + +android { + defaultConfig { + minSdk = 21 + } +} diff --git a/extensions/cricbuzz/src/main/AndroidManifest.xml b/extensions/cricbuzz/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/cricbuzz/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/cricbuzz/src/main/java/app/revanced/extension/cricbuzz/HideAdsPatch.java b/extensions/cricbuzz/src/main/java/app/revanced/extension/cricbuzz/HideAdsPatch.java new file mode 100644 index 0000000000..9338e2978f --- /dev/null +++ b/extensions/cricbuzz/src/main/java/app/revanced/extension/cricbuzz/HideAdsPatch.java @@ -0,0 +1,28 @@ +package app.revanced.extension.cricbuzz.ads; + +import com.cricbuzz.android.data.rest.model.BottomBar; +import java.util.List; +import java.util.Iterator; +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class HideAdsPatch { + + /** + * Injection point. + */ + public static void filterCb11(List list) { + try { + Iterator iterator = list.iterator(); + while (iterator.hasNext()) { + BottomBar bar = iterator.next(); + if (bar.getName().equals("Cricbuzz11")) { + Logger.printInfo(() -> "Removing Cricbuzz11 bar: " + bar); + iterator.remove(); + } + } + } catch (Exception ex) { + Logger.printException(() -> "filterCb11 failure", ex); + } + } +} \ No newline at end of file diff --git a/extensions/cricbuzz/stub/build.gradle.kts b/extensions/cricbuzz/stub/build.gradle.kts new file mode 100644 index 0000000000..7744c0eaac --- /dev/null +++ b/extensions/cricbuzz/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} diff --git a/extensions/cricbuzz/stub/src/main/AndroidManifest.xml b/extensions/cricbuzz/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/cricbuzz/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/cricbuzz/stub/src/main/java/com/cricbuzz/android/data/rest/model/BottomBar.java b/extensions/cricbuzz/stub/src/main/java/com/cricbuzz/android/data/rest/model/BottomBar.java new file mode 100644 index 0000000000..2b2660c320 --- /dev/null +++ b/extensions/cricbuzz/stub/src/main/java/com/cricbuzz/android/data/rest/model/BottomBar.java @@ -0,0 +1,5 @@ +package com.cricbuzz.android.data.rest.model; + +public final class BottomBar { + public final String getName() { throw new UnsupportedOperationException(); } +} \ No newline at end of file diff --git a/extensions/instagram/build.gradle.kts b/extensions/instagram/build.gradle.kts new file mode 100644 index 0000000000..9b476b1c81 --- /dev/null +++ b/extensions/instagram/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) +} + +android { + defaultConfig { + minSdk = 26 + } +} diff --git a/extensions/instagram/src/main/AndroidManifest.xml b/extensions/instagram/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/instagram/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/feed/LimitFeedToFollowedProfiles.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/feed/LimitFeedToFollowedProfiles.java new file mode 100644 index 0000000000..3154dd9f7a --- /dev/null +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/feed/LimitFeedToFollowedProfiles.java @@ -0,0 +1,26 @@ +package app.revanced.extension.instagram.feed; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") +public class LimitFeedToFollowedProfiles { + + /** + * Injection point. + */ + public static Map setFollowingHeader(Map requestHeaderMap) { + String paginationHeaderName = "pagination_source"; + + // Patch the header only if it's trying to fetch the default feed + String currentHeader = requestHeaderMap.get(paginationHeaderName); + if (currentHeader != null && !currentHeader.equals("feed_recs")) { + return requestHeaderMap; + } + + // Create new map as original is unmodifiable. + Map patchedRequestHeaderMap = new HashMap<>(requestHeaderMap); + patchedRequestHeaderMap.put(paginationHeaderName, "following"); + return patchedRequestHeaderMap; + } +} diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/hide/navigation/HideNavigationButtonsPatch.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/hide/navigation/HideNavigationButtonsPatch.java new file mode 100644 index 0000000000..1dd99e204f --- /dev/null +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/hide/navigation/HideNavigationButtonsPatch.java @@ -0,0 +1,33 @@ +package app.revanced.extension.instagram.hide.navigation; + +import java.lang.reflect.Field; +import java.util.List; + +@SuppressWarnings("unused") +public class HideNavigationButtonsPatch { + + /** + * Injection point. + * @param navigationButtonsList the list of navigation buttons, as an (obfuscated) Enum type + * @param buttonNameToRemove the name of the button we want to remove + * @param enumNameField the field in the nav button enum class which contains the name of the button + * @return the patched list of navigation buttons + */ + public static List removeNavigationButtonByName( + List navigationButtonsList, + String buttonNameToRemove, + String enumNameField + ) + throws IllegalAccessException, NoSuchFieldException { + for (Object button : navigationButtonsList) { + Field f = button.getClass().getDeclaredField(enumNameField); + String currentButtonEnumName = (String) f.get(button); + + if (buttonNameToRemove.equals(currentButtonEnumName)) { + navigationButtonsList.remove(button); + break; + } + } + return navigationButtonsList; + } +} diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/links/OpenLinksExternallyPatch.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/links/OpenLinksExternallyPatch.java new file mode 100644 index 0000000000..49db896c23 --- /dev/null +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/links/OpenLinksExternallyPatch.java @@ -0,0 +1,30 @@ +package app.revanced.extension.instagram.misc.links; + +import android.net.Uri; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +@SuppressWarnings("unused") +public final class OpenLinksExternallyPatch { + + /** + * Injection point. + */ + public static boolean openExternally(String url) { + try { + // The "url" parameter to this function will be of the form. + // https://l.instagram.com/?u=&e= + String actualUrl = Uri.parse(url).getQueryParameter("u"); + if (actualUrl != null) { + Utils.openLink(actualUrl); + return true; + } + + } catch (Exception ex) { + Logger.printException(() -> "openExternally failure", ex); + } + + return false; + } +} diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/privacy/SanitizeSharingLinksPatch.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/privacy/SanitizeSharingLinksPatch.java new file mode 100644 index 0000000000..058ee19f90 --- /dev/null +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/privacy/SanitizeSharingLinksPatch.java @@ -0,0 +1,15 @@ +package app.revanced.extension.instagram.misc.privacy; + +import app.revanced.extension.shared.privacy.LinkSanitizer; + +@SuppressWarnings("unused") +public final class SanitizeSharingLinksPatch { + private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh"); + + /** + * Injection point. + */ + public static String sanitizeSharingLink(String url) { + return sanitizer.sanitizeURLString(url); + } +} diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/domain/ChangeLinkSharingDomainPatch.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/domain/ChangeLinkSharingDomainPatch.java new file mode 100644 index 0000000000..77eea7e847 --- /dev/null +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/domain/ChangeLinkSharingDomainPatch.java @@ -0,0 +1,33 @@ +package app.revanced.extension.instagram.misc.share.domain; + +import android.net.Uri; +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public final class ChangeLinkSharingDomainPatch { + + private static String getCustomShareDomain() { + // Method is modified during patching. + throw new IllegalStateException(); + } + + /** + * Injection point. + */ + public static String setCustomShareDomain(String url) { + try { + Uri uri = Uri.parse(url); + Uri.Builder builder = uri + .buildUpon() + .authority(getCustomShareDomain()) + .clearQuery(); + + String patchedUrl = builder.build().toString(); + Logger.printInfo(() -> "Domain change from : " + url + " to: " + patchedUrl); + return patchedUrl; + } catch (Exception ex) { + Logger.printException(() -> "setCustomShareDomain failure with " + url, ex); + return url; + } + } +} diff --git a/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/privacy/SanitizeSharingLinksPatch.java b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/privacy/SanitizeSharingLinksPatch.java new file mode 100644 index 0000000000..0566f68acb --- /dev/null +++ b/extensions/instagram/src/main/java/app/revanced/extension/instagram/misc/share/privacy/SanitizeSharingLinksPatch.java @@ -0,0 +1,15 @@ +package app.revanced.extension.instagram.misc.share.privacy; + +import app.revanced.extension.shared.privacy.LinkSanitizer; + +@SuppressWarnings("unused") +public final class SanitizeSharingLinksPatch { + private static final LinkSanitizer sanitizer = new LinkSanitizer("igsh"); + + /** + * Injection point. + */ + public static String sanitizeSharingLink(String url) { + return sanitizer.sanitizeURLString(url); + } +} diff --git a/extensions/messenger/build.gradle.kts b/extensions/messenger/build.gradle.kts new file mode 100644 index 0000000000..36b080b27b --- /dev/null +++ b/extensions/messenger/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) +} + +android { + defaultConfig { + minSdk = 24 + } +} diff --git a/extensions/messenger/src/main/AndroidManifest.xml b/extensions/messenger/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/messenger/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/messenger/src/main/java/app/revanced/extension/messenger/metaai/RemoveMetaAIPatch.java b/extensions/messenger/src/main/java/app/revanced/extension/messenger/metaai/RemoveMetaAIPatch.java new file mode 100644 index 0000000000..c09fe7edc9 --- /dev/null +++ b/extensions/messenger/src/main/java/app/revanced/extension/messenger/metaai/RemoveMetaAIPatch.java @@ -0,0 +1,25 @@ +package app.revanced.extension.messenger.metaai; + +import java.util.*; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class RemoveMetaAIPatch { + private static final Set loggedIDs = Collections.synchronizedSet(new HashSet<>()); + + public static boolean overrideBooleanFlag(long id, boolean value) { + try { + if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) { + if (loggedIDs.add(id)) + Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false"); + + return false; + } + } catch (Exception ex) { + Logger.printException(() -> "overrideBooleanFlag failure", ex); + } + + return value; + } +} diff --git a/extensions/music/build.gradle.kts b/extensions/music/build.gradle.kts new file mode 100644 index 0000000000..f84a54a0d3 --- /dev/null +++ b/extensions/music/build.gradle.kts @@ -0,0 +1,11 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:youtube:stub")) + compileOnly(libs.annotation) +} + +android { + defaultConfig { + minSdk = 26 + } +} diff --git a/extensions/music/src/main/AndroidManifest.xml b/extensions/music/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/music/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/music/src/main/java/app/revanced/extension/music/VersionCheckUtils.java b/extensions/music/src/main/java/app/revanced/extension/music/VersionCheckUtils.java new file mode 100644 index 0000000000..76331a720b --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/VersionCheckUtils.java @@ -0,0 +1,12 @@ +package app.revanced.extension.music; + +import app.revanced.extension.shared.Utils; + +public class VersionCheckUtils { + private static boolean isVersionOrGreater(String version) { + return Utils.getAppVersionName().compareTo(version) >= 0; + } + + public static final boolean IS_8_40_OR_GREATER = isVersionOrGreater("8.40.00"); +} + diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeMiniplayerColorPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeMiniplayerColorPatch.java new file mode 100644 index 0000000000..ec941f7f3b --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeMiniplayerColorPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.music.patches; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class ChangeMiniplayerColorPatch { + + /** + * Injection point + */ + public static boolean changeMiniplayerColor() { + return Settings.CHANGE_MINIPLAYER_COLOR.get(); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/ForceOriginalAudioPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/ForceOriginalAudioPatch.java new file mode 100644 index 0000000000..26589623e3 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/ForceOriginalAudioPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.music.patches; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class ForceOriginalAudioPatch { + + /** + * Injection point. + */ + public static void setEnabled() { + app.revanced.extension.shared.patches.ForceOriginalAudioPatch.setEnabled( + Settings.FORCE_ORIGINAL_AUDIO.get(), + Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() + ); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideButtonsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideButtonsPatch.java new file mode 100644 index 0000000000..5794baa9b1 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideButtonsPatch.java @@ -0,0 +1,49 @@ +package app.revanced.extension.music.patches; + +import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition; + +import android.view.View; +import android.view.ViewGroup; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class HideButtonsPatch { + + /** + * Injection point + */ + public static int hideCastButton(int original) { + return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original; + } + + /** + * Injection point + */ + public static void hideCastButton(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON, view); + } + + /** + * Injection point + */ + public static boolean hideHistoryButton(boolean original) { + return original && !Settings.HIDE_HISTORY_BUTTON.get(); + } + + /** + * Injection point + */ + public static void hideNotificationButton(View view) { + if (view.getParent() instanceof ViewGroup viewGroup) { + hideViewBy0dpUnderCondition(Settings.HIDE_NOTIFICATION_BUTTON, viewGroup); + } + } + + /** + * Injection point + */ + public static void hideSearchButton(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_SEARCH_BUTTON, view); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCategoryBarPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCategoryBarPatch.java new file mode 100644 index 0000000000..f0b0901555 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideCategoryBarPatch.java @@ -0,0 +1,18 @@ +package app.revanced.extension.music.patches; + +import static app.revanced.extension.shared.Utils.hideViewBy0dpUnderCondition; + +import android.view.View; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class HideCategoryBarPatch { + + /** + * Injection point + */ + public static void hideCategoryBar(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_CATEGORY_BAR, view); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideGetPremiumPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideGetPremiumPatch.java new file mode 100644 index 0000000000..658c0e59af --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideGetPremiumPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.music.patches; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class HideGetPremiumPatch { + + /** + * Injection point + */ + public static boolean hideGetPremiumLabel() { + return Settings.HIDE_GET_PREMIUM_LABEL.get(); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/HideVideoAdsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideVideoAdsPatch.java new file mode 100644 index 0000000000..9c4d51ee37 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/HideVideoAdsPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.music.patches; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class HideVideoAdsPatch { + + /** + * Injection point + */ + public static boolean showVideoAds(boolean original) { + if (Settings.HIDE_VIDEO_ADS.get()) { + return false; + } + return original; + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/NavigationBarPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/NavigationBarPatch.java new file mode 100644 index 0000000000..6131401ce2 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/NavigationBarPatch.java @@ -0,0 +1,86 @@ +package app.revanced.extension.music.patches; + +import static app.revanced.extension.shared.Utils.hideViewUnderCondition; + +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class NavigationBarPatch { + private static String lastYTNavigationEnumName = ""; + + public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) { + if (ytNavigationEnumName != null) { + lastYTNavigationEnumName = ytNavigationEnumName.name(); + } + } + + public static void hideNavigationLabel(TextView textview) { + hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR_LABEL.get(), textview); + } + + public static void hideNavigationButton(View view) { + // Hide entire navigation bar. + if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) { + hideViewUnderCondition(true, (View) view.getParent()); + return; + } + + // Hide navigation buttons based on their type. + for (NavigationButton button : NavigationButton.values()) { + if (button.ytEnumNames.contains(lastYTNavigationEnumName)) { + hideViewUnderCondition(button.hidden, view); + break; + } + } + } + + private enum NavigationButton { + HOME( + Arrays.asList( + "TAB_HOME" + ), + Settings.HIDE_NAVIGATION_BAR_HOME_BUTTON.get() + ), + SAMPLES( + Arrays.asList( + "TAB_SAMPLES" + ), + Settings.HIDE_NAVIGATION_BAR_SAMPLES_BUTTON.get() + ), + EXPLORE( + Arrays.asList( + "TAB_EXPLORE" + ), + Settings.HIDE_NAVIGATION_BAR_EXPLORE_BUTTON.get() + ), + LIBRARY( + Arrays.asList( + "LIBRARY_MUSIC", + "TAB_BOOKMARK" // YouTube Music 8.24+ + ), + Settings.HIDE_NAVIGATION_BAR_LIBRARY_BUTTON.get() + ), + UPGRADE( + Arrays.asList( + "TAB_MUSIC_PREMIUM" + ), + Settings.HIDE_NAVIGATION_BAR_UPGRADE_BUTTON.get() + ); + + private final List ytEnumNames; + private final boolean hidden; + + NavigationButton(List ytEnumNames, boolean hidden) { + this.ytEnumNames = ytEnumNames; + this.hidden = hidden; + } + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/PermanentRepeatPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/PermanentRepeatPatch.java new file mode 100644 index 0000000000..b44b0a3f1c --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/PermanentRepeatPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.music.patches; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class PermanentRepeatPatch { + + /** + * Injection point + */ + public static boolean permanentRepeat() { + return Settings.PERMANENT_REPEAT.get(); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java new file mode 100644 index 0000000000..46c85a8edd --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/spoof/SpoofVideoStreamsPatch.java @@ -0,0 +1,30 @@ +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_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; + +import java.util.List; + +import app.revanced.extension.shared.spoof.ClientType; + +@SuppressWarnings("unused") +public class SpoofVideoStreamsPatch { + + /** + * Injection point. + */ + public static void setClientOrderToUse() { + List availableClients = List.of( + ANDROID_REEL, + ANDROID_VR_1_43_32, + VISIONOS, + ANDROID_VR_1_61_48 + ); + + app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch.setClientsToUse( + availableClients, SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/theme/ThemePatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/theme/ThemePatch.java new file mode 100644 index 0000000000..3f4e396699 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/theme/ThemePatch.java @@ -0,0 +1,27 @@ +package app.revanced.extension.music.patches.theme; + +import app.revanced.extension.shared.theme.BaseThemePatch; + +@SuppressWarnings("unused") +public class ThemePatch extends BaseThemePatch { + + // Color constants used in relation with litho components. + private static final int[] DARK_VALUES = { + 0xFF212121, // Comments box background. + 0xFF030303, // Button container background in album. + 0xFF000000, // Button container background in playlist. + }; + + /** + * Injection point. + *

+ * Change the color of Litho components. + * If the color of the component matches one of the values, return the background color. + * + * @param originalValue The original color value. + * @return The new or original color value. + */ + public static int getValue(int originalValue) { + return processColorValue(originalValue, DARK_VALUES, null); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java new file mode 100644 index 0000000000..c3874f655c --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/MusicActivityHook.java @@ -0,0 +1,148 @@ +package app.revanced.extension.music.settings; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +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; + +/** + * Hooks GoogleApiActivity to inject a custom {@link MusicPreferenceFragment} with a toolbar and search. + */ +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. + */ + @SuppressWarnings("unused") + public static void initialize(Activity parentActivity) { + // Must touch the Music settings to ensure the class is loaded and + // the values can be found when setting the UI preferences. + // Logging anything under non debug ensures this is set. + Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get()); + + // YT Music always uses dark mode. + Utils.setIsDarkModeEnabled(true); + + BaseActivityHook.initialize(new MusicActivityHook(), parentActivity); + } + + /** + * Sets the fixed theme for the activity. + */ + @Override + protected void customizeActivityTheme(Activity activity) { + // 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")); + } + + /** + * Returns the fixed background color for the toolbar. + */ + @Override + protected int getToolbarBackgroundColor() { + return Utils.getResourceColor("ytm_color_black"); + } + + /** + * Returns the navigation icon with a color filter applied. + */ + @Override + protected Drawable getNavigationIcon() { + Drawable navigationIcon = MusicPreferenceFragment.getBackButtonDrawable(); + navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); + return navigationIcon; + } + + /** + * Returns the click listener that finishes the activity when the navigation icon is clicked. + */ + @Override + protected View.OnClickListener getNavigationClickListener(Activity activity) { + return view -> { + if (searchViewController != null && searchViewController.isSearchActive()) { + searchViewController.closeSearch(); + } else { + activity.finish(); + } + }; + } + + /** + * Adds search view components to the toolbar for {@link MusicPreferenceFragment}. + * + * @param activity The activity hosting the toolbar. + * @param toolbar The configured toolbar. + * @param fragment The PreferenceFragment associated with the activity. + */ + @Override + protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) { + if (fragment instanceof MusicPreferenceFragment) { + searchViewController = MusicSearchViewController.addSearchViewComponents( + activity, toolbar, (MusicPreferenceFragment) fragment); + } + } + + /** + * Creates a new {@link MusicPreferenceFragment} for the activity. + */ + @Override + protected PreferenceFragment createPreferenceFragment() { + return new MusicPreferenceFragment(); + } + + /** + * Injection point. + *

+ * Overrides {@link Activity#finish()} of the injection Activity. + * + * @return if the original activity finish method should be allowed to run. + */ + @SuppressWarnings("unused") + public static boolean handleFinish() { + return MusicSearchViewController.handleFinish(searchViewController); + } + + /** + * Injection point. + *

+ * Decides whether to use bold icons. + */ + @SuppressWarnings("unused") + public static boolean useBoldIcons(boolean original) { + return Utils.appIsUsingBoldIcons(); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java new file mode 100644 index 0000000000..7decd29b8a --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -0,0 +1,41 @@ +package app.revanced.extension.music.settings; + +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.BooleanSetting; +import app.revanced.extension.shared.settings.EnumSetting; +import app.revanced.extension.shared.spoof.ClientType; + +public class Settings extends YouTubeAndMusicSettings { + + // Ads + public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_music_hide_video_ads", TRUE, true); + public static final BooleanSetting HIDE_GET_PREMIUM_LABEL = new BooleanSetting("revanced_music_hide_get_premium_label", TRUE, true); + + // General + public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_music_hide_cast_button", TRUE, true); + public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_music_hide_category_bar", FALSE, true); + public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_music_hide_history_button", FALSE, true); + public static final BooleanSetting HIDE_SEARCH_BUTTON = new BooleanSetting("revanced_music_hide_search_button", FALSE, true); + public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_music_hide_notification_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR_HOME_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_home_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR_SAMPLES_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_samples_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR_EXPLORE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_explore_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR_LIBRARY_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_library_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true); + + // Player + public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true); + public static final BooleanSetting PERMANENT_REPEAT = new BooleanSetting("revanced_music_play_permanent_repeat", FALSE, true); + + // Miscellaneous + public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client_type", + ClientType.ANDROID_REEL, true, parent(SPOOF_VIDEO_STREAMS)); + + public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", TRUE, true); +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java new file mode 100644 index 0000000000..86e5173420 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/preference/MusicPreferenceFragment.java @@ -0,0 +1,93 @@ +package app.revanced.extension.music.settings.preference; + +import android.app.Dialog; +import android.preference.PreferenceScreen; +import android.widget.Toolbar; + +import app.revanced.extension.music.settings.MusicActivityHook; +import app.revanced.extension.shared.GmsCoreSupport; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment; + +/** + * Preference fragment for ReVanced settings. + */ +@SuppressWarnings("deprecation") +public class MusicPreferenceFragment extends ToolbarPreferenceFragment { + /** + * The main PreferenceScreen used to display the current set of preferences. + */ + private PreferenceScreen preferenceScreen; + + /** + * Initializes the preference fragment. + */ + @Override + protected void initialize() { + super.initialize(); + + try { + preferenceScreen = getPreferenceScreen(); + Utils.sortPreferenceGroups(preferenceScreen); + setPreferenceScreenToolbar(preferenceScreen); + + // Clunky work around until preferences are custom classes that manage themselves. + // Custom branding only works with non-root install. But the preferences must be + // added during patched because of difficulties detecting during patching if it's + // a root install. So instead the non-functional preferences are removed during + // runtime if the app is mount (root) installation. + if (GmsCoreSupport.isPackageNameOriginal()) { + removePreferences( + BaseSettings.CUSTOM_BRANDING_ICON.key, + BaseSettings.CUSTOM_BRANDING_NAME.key); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Called when the fragment starts. + */ + @Override + public void onStart() { + super.onStart(); + try { + // Initialize search controller if needed + if (MusicActivityHook.searchViewController != null) { + // Trigger search data collection after fragment is ready. + MusicActivityHook.searchViewController.initializeSearchData(); + } + } catch (Exception ex) { + Logger.printException(() -> "onStart failure", ex); + } + } + + /** + * Sets toolbar for all nested preference screens. + */ + @Override + protected void customizeToolbar(Toolbar toolbar) { + MusicActivityHook.setToolbarLayoutParams(toolbar); + } + + /** + * Perform actions after toolbar setup. + */ + @Override + protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) { + if (MusicActivityHook.searchViewController != null + && MusicActivityHook.searchViewController.isSearchActive()) { + toolbar.post(() -> MusicActivityHook.searchViewController.closeSearch()); + } + } + + /** + * Returns the preference screen for external access by SearchViewController. + */ + public PreferenceScreen getPreferenceScreenForSearch() { + return preferenceScreen; + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java new file mode 100644 index 0000000000..65ccd4ea1a --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchResultsAdapter.java @@ -0,0 +1,28 @@ +package app.revanced.extension.music.settings.search; + +import android.content.Context; +import android.preference.PreferenceScreen; + +import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter; +import app.revanced.extension.shared.settings.search.BaseSearchViewController; +import app.revanced.extension.shared.settings.search.BaseSearchResultItem; + +import java.util.List; + +/** + * Music-specific search results adapter. + */ +@SuppressWarnings("deprecation") +public class MusicSearchResultsAdapter extends BaseSearchResultsAdapter { + + public MusicSearchResultsAdapter(Context context, List items, + BaseSearchViewController.BasePreferenceFragment fragment, + BaseSearchViewController searchViewController) { + super(context, items, fragment, searchViewController); + } + + @Override + protected PreferenceScreen getMainPreferenceScreen() { + return fragment.getPreferenceScreenForSearch(); + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java new file mode 100644 index 0000000000..6681a2f027 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/search/MusicSearchViewController.java @@ -0,0 +1,71 @@ +package app.revanced.extension.music.settings.search; + +import android.app.Activity; +import android.preference.Preference; +import android.preference.PreferenceScreen; +import android.view.View; +import android.widget.Toolbar; + +import app.revanced.extension.music.settings.preference.MusicPreferenceFragment; +import app.revanced.extension.shared.settings.search.*; + +/** + * Music-specific search view controller implementation. + */ +@SuppressWarnings("deprecation") +public class MusicSearchViewController extends BaseSearchViewController { + + public static MusicSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar, + MusicPreferenceFragment fragment) { + return new MusicSearchViewController(activity, toolbar, fragment); + } + + private MusicSearchViewController(Activity activity, Toolbar toolbar, MusicPreferenceFragment fragment) { + super(activity, toolbar, new PreferenceFragmentAdapter(fragment)); + } + + @Override + protected BaseSearchResultsAdapter createSearchResultsAdapter() { + return new MusicSearchResultsAdapter(activity, filteredSearchItems, fragment, this); + } + + @Override + protected boolean isSpecialPreferenceGroup(Preference preference) { + // Music doesn't have SponsorBlock, so no special groups. + return false; + } + + @Override + protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) { + // Music doesn't have special preferences. + // This method can be empty or handle music-specific preferences if any. + } + + // Static method for handling Activity finish + public static boolean handleFinish(MusicSearchViewController searchViewController) { + if (searchViewController != null && searchViewController.isSearchActive()) { + searchViewController.closeSearch(); + return true; + } + return false; + } + + // Adapter to wrap MusicPreferenceFragment to BasePreferenceFragment interface. + private record PreferenceFragmentAdapter(MusicPreferenceFragment fragment) implements BasePreferenceFragment { + + @Override + public PreferenceScreen getPreferenceScreenForSearch() { + return fragment.getPreferenceScreenForSearch(); + } + + @Override + public View getView() { + return fragment.getView(); + } + + @Override + public Activity getActivity() { + return fragment.getActivity(); + } + } +} diff --git a/extensions/nothingx/build.gradle.kts b/extensions/nothingx/build.gradle.kts new file mode 100644 index 0000000000..ed2b78c5f6 --- /dev/null +++ b/extensions/nothingx/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:nothingx:stub")) +} + +android { + defaultConfig { + minSdk = 23 + } +} \ No newline at end of file diff --git a/extensions/nothingx/src/main/AndroidManifest.xml b/extensions/nothingx/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/nothingx/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java b/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java new file mode 100644 index 0000000000..c301ae2fb3 --- /dev/null +++ b/extensions/nothingx/src/main/java/app/revanced/extension/nothingx/patches/ShowK1TokensPatch.java @@ -0,0 +1,590 @@ +package app.revanced.extension.nothingx.patches; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Application; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Patches to expose the K1 token for Nothing X app to enable pairing with GadgetBridge. + */ +@SuppressWarnings("unused") +public class ShowK1TokensPatch { + + private static final String TAG = "ReVanced"; + private static final String PACKAGE_NAME = "com.nothing.smartcenter"; + private static final String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e"; + private static final String PREFS_NAME = "revanced_nothingx_prefs"; + private static final String KEY_DONT_SHOW_DIALOG = "dont_show_k1_dialog"; + + // Colors + private static final int COLOR_BG = 0xFF1E1E1E; + private static final int COLOR_CARD = 0xFF2D2D2D; + private static final int COLOR_TEXT_PRIMARY = 0xFFFFFFFF; + private static final int COLOR_TEXT_SECONDARY = 0xFFB0B0B0; + private static final int COLOR_ACCENT = 0xFFFF9500; + private static final int COLOR_TOKEN_BG = 0xFF3A3A3A; + private static final int COLOR_BUTTON_POSITIVE = 0xFFFF9500; + private static final int COLOR_BUTTON_NEGATIVE = 0xFFFF6B6B; + + // Match standalone K1: k1:, K1:, k1>, etc. + private static final Pattern K1_STANDALONE_PATTERN = Pattern.compile("(?i)(?:k1\\s*[:>]\\s*)([0-9a-f]{32})"); + // Match combined r3+k1: format (64 chars = r3(32) + k1(32)) + private static final Pattern K1_COMBINED_PATTERN = Pattern.compile("(?i)r3\\+k1\\s*:\\s*([0-9a-f]{64})"); + + private static volatile boolean k1Logged = false; + private static volatile boolean lifecycleCallbacksRegistered = false; + private static Context appContext; + + /** + * Get K1 tokens from database and log files. + * Call this after the app initializes. + * + * @param context Application context + */ + public static void showK1Tokens(Context context) { + if (k1Logged) { + return; + } + + appContext = context.getApplicationContext(); + + Set allTokens = new LinkedHashSet<>(); + + // First try to get from database. + String dbToken = getK1TokensFromDatabase(); + if (dbToken != null) { + allTokens.add(dbToken); + } + + // Then get from log files. + Set 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 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 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 getK1TokensFromLogFiles() { + Set pairingTokens = new LinkedHashSet<>(); + Set 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 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 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(); + } + } +} diff --git a/extensions/nothingx/stub/build.gradle.kts b/extensions/nothingx/stub/build.gradle.kts new file mode 100644 index 0000000000..fcadc678c4 --- /dev/null +++ b/extensions/nothingx/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} \ No newline at end of file diff --git a/extensions/nothingx/stub/src/main/AndroidManifest.xml b/extensions/nothingx/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/nothingx/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/nunl/build.gradle.kts b/extensions/nunl/build.gradle.kts new file mode 100644 index 0000000000..ab48531bba --- /dev/null +++ b/extensions/nunl/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:nunl:stub")) +} + +android { + defaultConfig { + minSdk = 26 + } +} diff --git a/extensions/nunl/src/main/AndroidManifest.xml b/extensions/nunl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/nunl/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java b/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java new file mode 100644 index 0000000000..2e4ab5b069 --- /dev/null +++ b/extensions/nunl/src/main/java/app/revanced/extension/nunl/ads/HideAdsPatch.java @@ -0,0 +1,114 @@ +package app.revanced.extension.nunl.ads; + +import nl.nu.performance.api.client.interfaces.Block; +import nl.nu.performance.api.client.unions.SmallArticleLinkFlavor; +import nl.nu.performance.api.client.objects.*; + +import java.util.ArrayList; +import java.util.List; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class HideAdsPatch { + private static final String[] blockedHeaderBlocks = { + "Aanbiedingen (Adverteerders)", + "Aangeboden door NUshop" + }; + + // "Rubrieken" menu links to ads. + private static final String[] blockedLinkBlocks = { + "Van onze adverteerders" + }; + + public static void filterAds(List blocks) { + try { + ArrayList cleanedList = new ArrayList<>(); + + boolean skipFullHeader = false; + boolean skipUntilDivider = false; + + int index = 0; + while (index < blocks.size()) { + Block currentBlock = blocks.get(index); + + // Because of pagination, we might not see the Divider in front of it. + // Just remove it as is and leave potential extra spacing visible on the screen. + if (currentBlock instanceof DpgBannerBlock) { + index++; + continue; + } + + if (index + 1 < blocks.size()) { + // Filter Divider -> DpgMediaBanner -> Divider. + if (currentBlock instanceof DividerBlock + && blocks.get(index + 1) instanceof DpgBannerBlock) { + index += 2; + continue; + } + + // Filter Divider -> LinkBlock (... -> LinkBlock -> LinkBlock-> LinkBlock -> Divider). + if (currentBlock instanceof DividerBlock + && blocks.get(index + 1) instanceof LinkBlock linkBlock) { + Link link = linkBlock.getLink(); + if (link != null && link.getTitle() != null) { + for (String blockedLinkBlock : blockedLinkBlocks) { + if (blockedLinkBlock.equals(link.getTitle().getText())) { + skipUntilDivider = true; + break; + } + } + if (skipUntilDivider) { + index++; + continue; + } + } + } + } + + // Skip LinkBlocks with a "flavor" claiming to be "isPartner" (sponsored inline ads). + if (currentBlock instanceof LinkBlock linkBlock + && linkBlock.getLink() != null + && linkBlock.getLink().getLinkFlavor() instanceof SmallArticleLinkFlavor smallArticleLinkFlavor + && smallArticleLinkFlavor.isPartner() != null + && smallArticleLinkFlavor.isPartner()) { + index++; + continue; + } + + if (currentBlock instanceof DividerBlock) { + skipUntilDivider = false; + } + + // Filter HeaderBlock with known ads until next HeaderBlock. + if (currentBlock instanceof HeaderBlock headerBlock) { + StyledText headerText = headerBlock.getTitle(); + if (headerText != null) { + skipFullHeader = false; + for (String blockedHeaderBlock : blockedHeaderBlocks) { + if (blockedHeaderBlock.equals(headerText.getText())) { + skipFullHeader = true; + break; + } + } + if (skipFullHeader) { + index++; + continue; + } + } + } + + if (!skipFullHeader && !skipUntilDivider) { + cleanedList.add(currentBlock); + } + index++; + } + + // Replace list in-place to not deal with moving the result to the correct register in smali. + blocks.clear(); + blocks.addAll(cleanedList); + } catch (Exception ex) { + Logger.printException(() -> "filterAds failure", ex); + } + } +} diff --git a/extensions/nunl/stub/build.gradle.kts b/extensions/nunl/stub/build.gradle.kts new file mode 100644 index 0000000000..7905271b26 --- /dev/null +++ b/extensions/nunl/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 26 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} diff --git a/extensions/nunl/stub/src/main/AndroidManifest.xml b/extensions/nunl/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/nunl/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java new file mode 100644 index 0000000000..3514f360cb --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/interfaces/Block.java @@ -0,0 +1,5 @@ +package nl.nu.performance.api.client.interfaces; + +public class Block { + +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java new file mode 100644 index 0000000000..0351aec049 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DividerBlock.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.interfaces.Block; + +public class DividerBlock extends Block { + +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java new file mode 100644 index 0000000000..ac300b0539 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/DpgBannerBlock.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.interfaces.Block; + +public class DpgBannerBlock extends Block { + +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java new file mode 100644 index 0000000000..7b1f7ad192 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/HeaderBlock.java @@ -0,0 +1,9 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.interfaces.Block; + +public class HeaderBlock extends Block { + public final StyledText getTitle() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java new file mode 100644 index 0000000000..771d11dad1 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/Link.java @@ -0,0 +1,13 @@ +package nl.nu.performance.api.client.objects; + +import nl.nu.performance.api.client.unions.LinkFlavor; + +public class Link { + public final StyledText getTitle() { + throw new UnsupportedOperationException("Stub"); + } + + public final LinkFlavor getLinkFlavor() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java new file mode 100644 index 0000000000..dea1950573 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/LinkBlock.java @@ -0,0 +1,10 @@ +package nl.nu.performance.api.client.objects; + +import android.os.Parcelable; +import nl.nu.performance.api.client.interfaces.Block; + +public abstract class LinkBlock extends Block implements Parcelable { + public final Link getLink() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java new file mode 100644 index 0000000000..719403eb4e --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/objects/StyledText.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.objects; + +public class StyledText { + public final String getText() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java new file mode 100644 index 0000000000..08413d3fd9 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/LinkFlavor.java @@ -0,0 +1,4 @@ +package nl.nu.performance.api.client.unions; + +public interface LinkFlavor { +} diff --git a/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java new file mode 100644 index 0000000000..4dcbf23cb9 --- /dev/null +++ b/extensions/nunl/stub/src/main/java/nl/nu/performance/api/client/unions/SmallArticleLinkFlavor.java @@ -0,0 +1,7 @@ +package nl.nu.performance.api.client.unions; + +public class SmallArticleLinkFlavor implements LinkFlavor { + public final Boolean isPartner() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/primevideo/build.gradle.kts b/extensions/primevideo/build.gradle.kts new file mode 100644 index 0000000000..17a3c31a21 --- /dev/null +++ b/extensions/primevideo/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:primevideo:stub")) +} + +android { + defaultConfig { + minSdk = 21 + } +} diff --git a/extensions/primevideo/src/main/AndroidManifest.xml b/extensions/primevideo/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/primevideo/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java new file mode 100644 index 0000000000..d0a97810a2 --- /dev/null +++ b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/ads/SkipAdsPatch.java @@ -0,0 +1,36 @@ +package app.revanced.extension.primevideo.ads; + +import com.amazon.avod.fsm.SimpleTrigger; +import com.amazon.avod.media.ads.AdBreak; +import com.amazon.avod.media.ads.internal.state.AdBreakTrigger; +import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType; +import com.amazon.avod.media.playback.VideoPlayer; +import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public final class SkipAdsPatch { + public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) { + try { + AdBreak adBreak = trigger.getBreak(); + + // There are two scenarios when entering the original method: + // 1. Player naturally entered an ad break while watching a video. + // 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break, + // user is forced to watch an ad before continuing. + // + // Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing + // target. Otherwise, just calculate when the ad break should end and skip to there. + if (trigger.getSeekStartPosition() != null) + player.seekTo(trigger.getSeekTarget().getTotalMilliseconds()); + else + player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds()); + + // Send "end of ads" trigger to state machine so everything doesn't get whacky. + state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION)); + } catch (Exception ex) { + Logger.printException(() -> "Failed skipping ads", ex); + } + } +} \ No newline at end of file diff --git a/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java new file mode 100644 index 0000000000..b11ec0875d --- /dev/null +++ b/extensions/primevideo/src/main/java/app/revanced/extension/primevideo/videoplayer/PlaybackSpeedPatch.java @@ -0,0 +1,207 @@ +package app.revanced.extension.primevideo.videoplayer; + +import android.app.AlertDialog; +import android.content.Context; +import android.graphics.RectF; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import java.util.Arrays; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.ui.Dim; + +import com.amazon.video.sdk.player.Player; + +public class PlaybackSpeedPatch { + private static Player player; + private static final float[] SPEED_VALUES = {0.5f, 0.7f, 0.8f, 0.9f, 0.95f, 1.0f, 1.05f, 1.1f, 1.2f, 1.3f, 1.5f, 2.0f}; + private static final String SPEED_BUTTON_TAG = "speed_overlay"; + + public static void setPlayer(Player playerInstance) { + player = playerInstance; + if (player != null) { + // Reset playback rate when switching between episodes to ensure correct display. + player.setPlaybackRate(1.0f); + } + } + + public static void initializeSpeedOverlay(View userControlsView) { + try { + LinearLayout buttonContainer = Utils.getChildViewByResourceName(userControlsView, "ButtonContainerPlayerTop"); + + // If the speed overlay exists we should return early. + if (Utils.getChildView(buttonContainer, false, child -> + child instanceof ImageView && SPEED_BUTTON_TAG.equals(child.getTag())) != null) { + return; + } + + ImageView speedButton = createSpeedButton(userControlsView.getContext()); + speedButton.setOnClickListener(v -> changePlaybackSpeed(speedButton)); + buttonContainer.addView(speedButton, 0); + + } catch (IllegalArgumentException e) { + Logger.printException(() -> "initializeSpeedOverlay, no button container found", e); + } catch (Exception e) { + Logger.printException(() -> "initializeSpeedOverlay failure", e); + } + } + + private static ImageView createSpeedButton(Context context) { + ImageView speedButton = new ImageView(context); + speedButton.setContentDescription("Playback Speed"); + speedButton.setTag(SPEED_BUTTON_TAG); + speedButton.setClickable(true); + speedButton.setFocusable(true); + speedButton.setScaleType(ImageView.ScaleType.CENTER); + + SpeedIconDrawable speedIcon = new SpeedIconDrawable(); + speedButton.setImageDrawable(speedIcon); + + speedButton.setMinimumWidth(Dim.dp48); + speedButton.setMinimumHeight(Dim.dp48); + + return speedButton; + } + + private static String[] getSpeedOptions() { + String[] options = new String[SPEED_VALUES.length]; + for (int i = 0; i < SPEED_VALUES.length; i++) { + options[i] = SPEED_VALUES[i] + "x"; + } + return options; + } + + private static void changePlaybackSpeed(ImageView imageView) { + if (player == null) { + Logger.printException(() -> "Player not available"); + return; + } + + try { + player.pause(); + AlertDialog dialog = createSpeedPlaybackDialog(imageView); + dialog.setOnDismissListener(dialogInterface -> player.play()); + dialog.show(); + + } catch (Exception e) { + Logger.printException(() -> "changePlaybackSpeed", e); + } + } + + private static AlertDialog createSpeedPlaybackDialog(ImageView imageView) { + Context context = imageView.getContext(); + int currentSelection = getCurrentSpeedSelection(); + + return new AlertDialog.Builder(context) + .setTitle("Select Playback Speed") + .setSingleChoiceItems(getSpeedOptions(), currentSelection, + PlaybackSpeedPatch::handleSpeedSelection) + .create(); + } + + private static int getCurrentSpeedSelection() { + try { + float currentRate = player.getPlaybackRate(); + int index = Arrays.binarySearch(SPEED_VALUES, currentRate); + return Math.max(index, 0); // Use slowest speed if not found. + } catch (Exception e) { + Logger.printException(() -> "getCurrentSpeedSelection error getting current playback speed", e); + return 0; + } + } + + private static void handleSpeedSelection(android.content.DialogInterface dialog, int selectedIndex) { + try { + float selectedSpeed = SPEED_VALUES[selectedIndex]; + player.setPlaybackRate(selectedSpeed); + player.play(); + } catch (Exception e) { + Logger.printException(() -> "handleSpeedSelection error setting playback speed", e); + } finally { + dialog.dismiss(); + } + } +} + +class SpeedIconDrawable extends Drawable { + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + + @Override + public void draw(Canvas canvas) { + int w = getBounds().width(); + int h = getBounds().height(); + float centerX = w / 2f; + // Position gauge in lower portion. + float centerY = h * 0.7f; + float radius = Math.min(w, h) / 2f * 0.8f; + + paint.setColor(Color.WHITE); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(radius * 0.1f); + + // Draw semicircle. + RectF oval = new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius); + canvas.drawArc(oval, 180, 180, false, paint); + + // Draw three tick marks. + paint.setStrokeWidth(radius * 0.06f); + for (int i = 0; i < 3; i++) { + float angle = 180 + (i * 45); // 180°, 225°, 270°. + float angleRad = (float) Math.toRadians(angle); + + float startX = centerX + (radius * 0.8f) * (float) Math.cos(angleRad); + float startY = centerY + (radius * 0.8f) * (float) Math.sin(angleRad); + float endX = centerX + radius * (float) Math.cos(angleRad); + float endY = centerY + radius * (float) Math.sin(angleRad); + + canvas.drawLine(startX, startY, endX, endY, paint); + } + + // Draw needle. + paint.setStrokeWidth(radius * 0.08f); + float needleAngle = 200; // Slightly right of center. + float needleAngleRad = (float) Math.toRadians(needleAngle); + + float needleEndX = centerX + (radius * 0.6f) * (float) Math.cos(needleAngleRad); + float needleEndY = centerY + (radius * 0.6f) * (float) Math.sin(needleAngleRad); + + canvas.drawLine(centerX, centerY, needleEndX, needleEndY, paint); + + // Center dot. + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(centerX, centerY, radius * 0.06f, paint); + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public int getIntrinsicWidth() { + return Dim.dp32; + } + + @Override + public int getIntrinsicHeight() { + return Dim.dp32; + } +} diff --git a/extensions/primevideo/stub/build.gradle.kts b/extensions/primevideo/stub/build.gradle.kts new file mode 100644 index 0000000000..7744c0eaac --- /dev/null +++ b/extensions/primevideo/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 21 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} diff --git a/extensions/primevideo/stub/src/main/AndroidManifest.xml b/extensions/primevideo/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/primevideo/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java new file mode 100644 index 0000000000..b537fe0402 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/SimpleTrigger.java @@ -0,0 +1,6 @@ +package com.amazon.avod.fsm; + +public final class SimpleTrigger implements Trigger { + public SimpleTrigger(T triggerType) { + } +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java new file mode 100644 index 0000000000..95741308c3 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/StateBase.java @@ -0,0 +1,7 @@ +package com.amazon.avod.fsm; + +public abstract class StateBase { + // This method orginally has protected access (modified in patch code). + public void doTrigger(Trigger trigger) { + } +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java new file mode 100644 index 0000000000..282f0f2004 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/fsm/Trigger.java @@ -0,0 +1,4 @@ +package com.amazon.avod.fsm; + +public interface Trigger { +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java new file mode 100644 index 0000000000..cc90e43cdc --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/TimeSpan.java @@ -0,0 +1,7 @@ +package com.amazon.avod.media; + +public final class TimeSpan { + public long getTotalMilliseconds() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java new file mode 100644 index 0000000000..9a950434dc --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/AdBreak.java @@ -0,0 +1,7 @@ +package com.amazon.avod.media.ads; + +import com.amazon.avod.media.TimeSpan; + +public interface AdBreak { + TimeSpan getDurationExcludingAux(); +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java new file mode 100644 index 0000000000..f417660ed7 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakState.java @@ -0,0 +1,4 @@ +package com.amazon.avod.media.ads.internal.state; + +public abstract class AdBreakState extends AdEnabledPlaybackState { +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java new file mode 100644 index 0000000000..f8b3995650 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdBreakTrigger.java @@ -0,0 +1,18 @@ +package com.amazon.avod.media.ads.internal.state; + +import com.amazon.avod.media.ads.AdBreak; +import com.amazon.avod.media.TimeSpan; + +public class AdBreakTrigger { + public AdBreak getBreak() { + throw new UnsupportedOperationException(); + } + + public TimeSpan getSeekTarget() { + throw new UnsupportedOperationException(); + } + + public TimeSpan getSeekStartPosition() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java new file mode 100644 index 0000000000..445aad580a --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlaybackState.java @@ -0,0 +1,8 @@ +package com.amazon.avod.media.ads.internal.state; + +import com.amazon.avod.fsm.StateBase; +import com.amazon.avod.media.playback.state.PlayerStateType; +import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType; + +public class AdEnabledPlaybackState extends StateBase { +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java new file mode 100644 index 0000000000..e7951e9342 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/AdEnabledPlayerTriggerType.java @@ -0,0 +1,5 @@ +package com.amazon.avod.media.ads.internal.state; + +public enum AdEnabledPlayerTriggerType { + NO_MORE_ADS_SKIP_TRANSITION +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java new file mode 100644 index 0000000000..07c198013f --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/ads/internal/state/ServerInsertedAdBreakState.java @@ -0,0 +1,4 @@ +package com.amazon.avod.media.ads.internal.state; + +public class ServerInsertedAdBreakState extends AdBreakState { +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java new file mode 100644 index 0000000000..4f82e98727 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/VideoPlayer.java @@ -0,0 +1,13 @@ +package com.amazon.avod.media.playback; + +public interface VideoPlayer { + long getCurrentPosition(); + + void seekTo(long positionMs); + + void pause(); + + void play(); + + boolean isPlaying(); +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java new file mode 100644 index 0000000000..202723285e --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/PlayerStateType.java @@ -0,0 +1,4 @@ +package com.amazon.avod.media.playback.state; + +public interface PlayerStateType { +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java new file mode 100644 index 0000000000..eac139f9bf --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/avod/media/playback/state/trigger/PlayerTriggerType.java @@ -0,0 +1,4 @@ +package com.amazon.avod.media.playback.state.trigger; + +public interface PlayerTriggerType { +} \ No newline at end of file diff --git a/extensions/primevideo/stub/src/main/java/com/amazon/video/sdk/player/Player.java b/extensions/primevideo/stub/src/main/java/com/amazon/video/sdk/player/Player.java new file mode 100644 index 0000000000..bd609e1964 --- /dev/null +++ b/extensions/primevideo/stub/src/main/java/com/amazon/video/sdk/player/Player.java @@ -0,0 +1,11 @@ +package com.amazon.video.sdk.player; + +public interface Player { + float getPlaybackRate(); + + void setPlaybackRate(float rate); + + void play(); + + void pause(); +} \ No newline at end of file diff --git a/extensions/reddit/build.gradle.kts b/extensions/reddit/build.gradle.kts index 8693f97f53..75c8d7a179 100644 --- a/extensions/reddit/build.gradle.kts +++ b/extensions/reddit/build.gradle.kts @@ -1,3 +1,9 @@ dependencies { compileOnly(project(":extensions:reddit:stub")) } + +android { + defaultConfig { + minSdk = 28 + } +} diff --git a/extensions/reddit/src/main/java/app/revanced/extension/patches/FilterPromotedLinksPatch.java b/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java similarity index 83% rename from extensions/reddit/src/main/java/app/revanced/extension/patches/FilterPromotedLinksPatch.java rename to extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java index 5b3e61b2ae..12cdc88345 100644 --- a/extensions/reddit/src/main/java/app/revanced/extension/patches/FilterPromotedLinksPatch.java +++ b/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java @@ -1,12 +1,16 @@ -package app.revanced.extension.patches; +package app.revanced.extension.reddit.patches; import com.reddit.domain.model.ILink; import java.util.ArrayList; import java.util.List; +@SuppressWarnings("unused") public final class FilterPromotedLinksPatch { + /** + * Injection point. + * * Filters list from promoted links. **/ public static List filterChildren(final Iterable links) { diff --git a/extensions/reddit/stub/build.gradle.kts b/extensions/reddit/stub/build.gradle.kts index c1cc5794c0..b4bee8809f 100644 --- a/extensions/reddit/stub/build.gradle.kts +++ b/extensions/reddit/stub/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - id(libs.plugins.android.library.get().pluginId) + alias(libs.plugins.android.library) } android { namespace = "app.revanced.extension" - compileSdk = 33 + compileSdk = 34 defaultConfig { minSdk = 24 diff --git a/extensions/samsung/radio/build.gradle.kts b/extensions/samsung/radio/build.gradle.kts new file mode 100644 index 0000000000..15d386efb3 --- /dev/null +++ b/extensions/samsung/radio/build.gradle.kts @@ -0,0 +1,10 @@ +dependencies { + compileOnly(project(":extensions:shared:library")) + compileOnly(project(":extensions:samsung:radio:stub")) +} + +android { + defaultConfig { + minSdk = 26 + } +} diff --git a/extensions/samsung/radio/src/main/AndroidManifest.xml b/extensions/samsung/radio/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/samsung/radio/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java new file mode 100644 index 0000000000..72c5addc4c --- /dev/null +++ b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/misc/fix/crash/FixCrashPatch.java @@ -0,0 +1,24 @@ +package app.revanced.extension.samsung.radio.misc.fix.crash; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@SuppressWarnings("unused") +public final class FixCrashPatch { + /** + * Injection point. + *

+ * Add the required permissions to the request list to avoid crashes on API 34+. + **/ + public static final String[] fixPermissionRequestList(String[] perms) { + List permsList = new ArrayList<>(Arrays.asList(perms)); + if (permsList.contains("android.permission.POST_NOTIFICATIONS")) { + permsList.addAll(Arrays.asList("android.permission.RECORD_AUDIO", "android.permission.READ_PHONE_STATE", "android.permission.FOREGROUND_SERVICE_MICROPHONE")); + } + if (permsList.contains("android.permission.RECORD_AUDIO")) { + permsList.add("android.permission.FOREGROUND_SERVICE_MICROPHONE"); + } + return permsList.toArray(new String[0]); + } +} diff --git a/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java new file mode 100644 index 0000000000..19b6c3e822 --- /dev/null +++ b/extensions/samsung/radio/src/main/java/app/revanced/extension/samsung/radio/restrictions/device/BypassDeviceChecksPatch.java @@ -0,0 +1,19 @@ +package app.revanced.extension.samsung.radio.restrictions.device; + +import android.os.SemSystemProperties; + +import java.util.Arrays; + +@SuppressWarnings("unused") +public final class BypassDeviceChecksPatch { + + /** + * Injection point. + *

+ * Check if the device has the required hardware + **/ + public static final boolean checkIfDeviceIsIncompatible(String[] deviceList) { + String currentDevice = SemSystemProperties.getSalesCode(); + return Arrays.asList(deviceList).contains(currentDevice); + } +} diff --git a/extensions/samsung/radio/stub/build.gradle.kts b/extensions/samsung/radio/stub/build.gradle.kts new file mode 100644 index 0000000000..b4bee8809f --- /dev/null +++ b/extensions/samsung/radio/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} diff --git a/extensions/samsung/radio/stub/src/main/AndroidManifest.xml b/extensions/samsung/radio/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/samsung/radio/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java b/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java new file mode 100644 index 0000000000..33a4b4400c --- /dev/null +++ b/extensions/samsung/radio/stub/src/main/java/android/os/SemSystemProperties.java @@ -0,0 +1,7 @@ +package android.os; + +public class SemSystemProperties { + public static String getSalesCode() { + throw new UnsupportedOperationException("Stub"); + } +} \ No newline at end of file diff --git a/extensions/shared/build.gradle.kts b/extensions/shared/build.gradle.kts index 2da2e1e89c..3eb6ff48c7 100644 --- a/extensions/shared/build.gradle.kts +++ b/extensions/shared/build.gradle.kts @@ -1,3 +1,10 @@ dependencies { implementation(project(":extensions:shared:library")) + compileOnly(libs.okhttp) +} + +android { + defaultConfig { + minSdk = 23 + } } diff --git a/extensions/shared/library/build.gradle.kts b/extensions/shared/library/build.gradle.kts index 3cbb560695..8215e513ad 100644 --- a/extensions/shared/library/build.gradle.kts +++ b/extensions/shared/library/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.android.library") + alias(libs.plugins.android.library) } android { @@ -18,4 +18,7 @@ android { dependencies { compileOnly(libs.annotation) + compileOnly(libs.okhttp) + compileOnly(libs.protobuf.javalite) + implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements")) } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ByteTrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java similarity index 89% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/ByteTrieSearch.java rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java index 162e0b0405..c91de4a7aa 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/ByteTrieSearch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java @@ -1,6 +1,4 @@ -package app.revanced.extension.youtube; - -import androidx.annotation.NonNull; +package app.revanced.extension.shared; import java.nio.charset.StandardCharsets; @@ -39,7 +37,7 @@ public final class ByteTrieSearch extends TrieSearch { return replacement; } - public ByteTrieSearch(@NonNull byte[]... patterns) { + public ByteTrieSearch(byte[]... patterns) { super(new ByteTrieNode(), patterns); } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java index 1e2586b2bc..fb7e68963a 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java @@ -1,10 +1,8 @@ package app.revanced.extension.shared; -import static app.revanced.extension.shared.StringRef.str; - import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; +import android.app.Dialog; import android.app.SearchManager; import android.content.Context; import android.content.DialogInterface; @@ -14,145 +12,401 @@ import android.net.Uri; import android.os.Build; import android.os.PowerManager; import android.provider.Settings; +import android.util.Pair; +import android.widget.LinearLayout; -import androidx.annotation.RequiresApi; +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; -/** - * @noinspection unused - */ +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.requests.Route.Method.GET; + +@SuppressWarnings("unused") public class GmsCoreSupport { - public static final String ORIGINAL_UNPATCHED_PACKAGE_NAME = "com.google.android.youtube"; - private static final String GMS_CORE_PACKAGE_NAME - = getGmsCoreVendorGroupId() + ".android.gms"; - private static final Uri GMS_CORE_PROVIDER - = Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix"); - private static final String DONT_KILL_MY_APP_LINK - = "https://dontkillmyapp.com"; + private static GmsCore gmsCore = GmsCore.UNKNOWN; - private static void open(String queryOrLink) { - Intent intent; - try { - // Check if queryOrLink is a valid URL. - new URL(queryOrLink); - - intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink)); - } catch (MalformedURLException e) { - intent = new Intent(Intent.ACTION_WEB_SEARCH); - intent.putExtra(SearchManager.QUERY, queryOrLink); + static { + for (GmsCore core : GmsCore.values()) { + if (core.getGroupId().equals(getGmsCoreVendorGroupId())) { + GmsCoreSupport.gmsCore = core; + break; + } } - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Utils.getContext().startActivity(intent); - - // Gracefully exit, otherwise the broken app will continue to run. - System.exit(0); - } - - private static void showBatteryOptimizationDialog(Activity context, - String dialogMessageRef, - String positiveButtonStringRef, - DialogInterface.OnClickListener onPositiveClickListener) { - // Do not set cancelable to false, to allow using back button to skip the action, - // just in case the check can never be satisfied. - var dialog = new AlertDialog.Builder(context) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setTitle(str("gms_core_dialog_title")) - .setMessage(str(dialogMessageRef)) - .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener) - .create(); - Utils.showDialog(context, dialog); } /** * Injection point. */ - @RequiresApi(api = Build.VERSION_CODES.N) public static void checkGmsCore(Activity context) { - try { - // Verify the user has not included GmsCore for a root installation. - // GmsCore Support changes the package name, but with a mounted installation - // all manifest changes are ignored and the original package name is used. - if (context.getPackageName().equals(ORIGINAL_UNPATCHED_PACKAGE_NAME)) { - Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included"); - // Cannot use localize text here, since the app will load - // resources from the unpatched app and all patch strings are missing. - Utils.showToastLong("The 'GmsCore support' patch breaks mount installations"); + gmsCore.check(context); + } - // Do not exit. If the app exits before launch completes (and without - // opening another activity), then on some devices such as Pixel phone Android 10 - // no toast will be shown and the app will continually be relaunched - // with the appearance of a hung app. - } + private static String getOriginalPackageName() { + return null; // Modified during patching. + } - // Verify GmsCore is installed. + 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; + */ + public static boolean isPackageNameOriginal() { + String originalPackageName = getOriginalPackageName(); + return originalPackageName == null + || originalPackageName.equals(Utils.getContext().getPackageName()); + } + + private enum GmsCore { + REVANCED("app.revanced", "https://github.com/revanced/gmscore/releases/latest", () -> { try { - PackageManager manager = context.getPackageManager(); - manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES); - } catch (PackageManager.NameNotFoundException exception) { - Logger.printInfo(() -> "GmsCore was not found"); - // Cannot show a dialog and must show a toast, - // because on some installations the app crashes before a dialog can be displayed. - Utils.showToastLong(str("gms_core_toast_not_installed_message")); - open(getGmsCoreDownload()); + HttpURLConnection connection = Requester.getConnectionFromRoute( + "https://api.github.com", + new Route(GET, "/repos/revanced/gmscore/releases/latest") + ); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + + int responseCode = connection.getResponseCode(); + if (responseCode != 200) { + Logger.printDebug(() -> "GitHub API returned status code: " + responseCode); + return null; + } + + // Parse the response + JSONObject releaseData = Requester.parseJSONObject(connection); + String tagName = releaseData.optString("tag_name", ""); + connection.disconnect(); + + if (tagName.isEmpty()) { + Logger.printDebug(() -> "No tag_name found in GitHub release data"); + return null; + } + + if (tagName.startsWith("v")) tagName = tagName.substring(1); + + return tagName; + } catch (Exception ex) { + Logger.printInfo(() -> "Failed to fetch latest GmsCore version from GitHub", ex); + return null; + } + }), + 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) { + 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"); return; } - // Check if GmsCore is running in the background. - try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) { - if (client == null) { - Logger.printInfo(() -> "GmsCore is not running in the background"); + Utils.runOnBackgroundThread(() -> { + try { + PackageManager manager = context.getPackageManager(); + var installedVersion = manager.getPackageInfo(packageName, 0).versionName; - showBatteryOptimizationDialog(context, - "gms_core_dialog_not_whitelisted_not_allowed_in_background_message", - "gms_core_dialog_open_website_text", - (dialog, id) -> open(DONT_KILL_MY_APP_LINK)); - return; + // 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; + + 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; + try { + // Check if queryOrLink is a valid URL. + new URL(queryOrLink); + + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink)); + } catch (MalformedURLException e) { + intent = new Intent(Intent.ACTION_WEB_SEARCH); + intent.putExtra(SearchManager.QUERY, queryOrLink); + } + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Utils.getContext().startActivity(intent); + + // Gracefully exit, otherwise the broken app will continue to run. + System.exit(0); + } + + private 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 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); + } + }, 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 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"; } - // Check if GmsCore is whitelisted from battery optimizations. - if (batteryOptimizationsEnabled(context)) { - Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations"); - showBatteryOptimizationDialog(context, - "gms_core_dialog_not_whitelisted_using_battery_optimizations_message", - "gms_core_dialog_continue_text", - (dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context)); + 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; } - } catch (Exception ex) { - Logger.printException(() -> "checkGmsCore failure", ex); + var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return !powerManager.isIgnoringBatteryOptimizations(packageName); + } + + private boolean isAndroidAutomotive(Context context) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); } } - @SuppressLint("BatteryLife") // Permission is part of GmsCore - private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) { - Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null)); - activity.startActivityForResult(intent, 0); - } - - /** - * @return If GmsCore is not whitelisted from battery optimizations. - */ - private static boolean batteryOptimizationsEnabled(Context context) { - var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME); - } - - private static String getGmsCoreDownload() { - final var vendorGroupId = getGmsCoreVendorGroupId(); - //noinspection SwitchStatementWithTooFewBranches - switch (vendorGroupId) { - case "app.revanced": - return "https://github.com/revanced/gmscore/releases/latest"; - default: - return vendorGroupId + ".android.gms"; - } - } - - // Modified by a patch. Do not touch. - private static String getGmsCoreVendorGroupId() { - return "app.revanced"; + @FunctionalInterface + private interface GetLatestVersion { + String get(); } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java index 9df38ac99b..610cd3414f 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java @@ -1,15 +1,27 @@ package app.revanced.extension.shared; +import static app.revanced.extension.shared.settings.BaseSettings.DEBUG; +import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE; +import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR; + import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import app.revanced.extension.shared.settings.BaseSettings; import java.io.PrintWriter; import java.io.StringWriter; -import static app.revanced.extension.shared.settings.BaseSettings.*; +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}. + * + * All methods are thread safe, and are safe to call even + * if {@link Utils#getContext()} is not available. + */ public class Logger { /** @@ -17,140 +29,186 @@ public class Logger { */ @FunctionalInterface public interface LogMessage { + /** + * @return Logger string message. This method is only called if logging is enabled. + */ @NonNull String buildMessageString(); + } - /** - * @return For outer classes, this returns {@link Class#getSimpleName()}. - * For static, inner, or anonymous classes, this returns the simple name of the enclosing class. - *
- * For example, each of these classes return 'SomethingView': - * - * com.company.SomethingView - * com.company.SomethingView$StaticClass - * com.company.SomethingView$1 - * - */ - private String findOuterClassSimpleName() { - var selfClass = this.getClass(); + private enum LogLevel { + DEBUG, + INFO, + ERROR + } - String fullClassName = selfClass.getName(); - final int dollarSignIndex = fullClassName.indexOf('$'); - if (dollarSignIndex < 0) { - return selfClass.getSimpleName(); // Already an outer class. + /** + * Log tag prefix. Only used for system logging. + */ + private static final String REVANCED_LOG_TAG_PREFIX = "revanced: "; + + private static final String LOGGER_CLASS_NAME = Logger.class.getName(); + + /** + * @return For outer classes, this returns {@link Class#getSimpleName()}. + * For static, inner, or anonymous classes, this returns the simple name of the enclosing class. + *
+ * For example, each of these classes returns 'SomethingView': + * + * com.company.SomethingView + * com.company.SomethingView$StaticClass + * com.company.SomethingView$1 + * + */ + private static String getOuterClassSimpleName(Object obj) { + Class logClass = obj.getClass(); + String fullClassName = logClass.getName(); + final int dollarSignIndex = fullClassName.indexOf('$'); + if (dollarSignIndex < 0) { + return logClass.getSimpleName(); // Already an outer class. + } + + // Class is inner, static, or anonymous. + // Parse the simple name full name. + // A class with no package returns index of -1, but incrementing gives index zero which is correct. + final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1; + return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex); + } + + /** + * Internal method to handle logging to Android Log and {@link LogBufferManager}. + * Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer + * with class name but without 'revanced:' prefix. + * + * @param logLevel The log level. + * @param message Log message object. + * @param ex Optional exception. + * @param includeStackTrace If the current stack should be included. + * @param showToast If a toast is to be shown. + */ + private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex, + boolean includeStackTrace, boolean showToast) { + // It's very important that no Settings are used in this method, + // as this code is used when a context is not set and thus referencing + // a setting will crash the app. + String messageString = message.buildMessageString(); + String className = getOuterClassSimpleName(message); + + String logText = messageString; + + // Append exception message if present. + if (ex != null) { + var exceptionMessage = ex.getMessage(); + if (exceptionMessage != null) { + logText += "\nException: " + exceptionMessage; } + } - // Class is inner, static, or anonymous. - // Parse the simple name full name. - // A class with no package returns index of -1, but incrementing gives index zero which is correct. - final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1; - return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex); + if (includeStackTrace) { + var sw = new StringWriter(); + new Throwable().printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + // Remove the stacktrace elements of this class. + final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME); + final int loggerBegins = stackTrace.indexOf('\n', loggerIndex); + logText += stackTrace.substring(loggerBegins); + } + + // Do not include "revanced:" prefix in clipboard logs. + String managerToastString = className + ": " + logText; + LogBufferManager.appendToLogBuffer(managerToastString); + + String logTag = REVANCED_LOG_TAG_PREFIX + className; + switch (logLevel) { + case DEBUG: + if (ex == null) Log.d(logTag, logText); + else Log.d(logTag, logText, ex); + break; + case INFO: + if (ex == null) Log.i(logTag, logText); + else Log.i(logTag, logText, ex); + break; + case ERROR: + if (ex == null) Log.e(logTag, logText); + else Log.e(logTag, logText, ex); + break; + } + + if (showToast) { + Utils.showToastLong(managerToastString); } } - private static final String REVANCED_LOG_PREFIX = "revanced: "; + private static boolean shouldLogDebug() { + // If the app is still starting up and the context is not yet set, + // then allow debug logging regardless what the debug setting actually is. + return Utils.context == null || DEBUG.get(); + } + + private static boolean shouldShowErrorToast() { + return Utils.context != null && DEBUG_TOAST_ON_ERROR.get(); + } + + private static boolean includeStackTrace() { + return Utils.context != null && DEBUG_STACKTRACE.get(); + } /** * Logs debug messages under the outer class name of the code calling this method. - * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} - * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. + *

+ * Whenever possible, the log string should be constructed entirely inside + * {@link LogMessage#buildMessageString()} so the performance cost of + * building strings is paid only if {@link BaseSettings#DEBUG} is enabled. */ - public static void printDebug(@NonNull LogMessage message) { + public static void printDebug(LogMessage message) { printDebug(message, null); } /** * Logs debug messages under the outer class name of the code calling this method. - * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} - * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. + *

+ * Whenever possible, the log string should be constructed entirely inside + * {@link LogMessage#buildMessageString()} so the performance cost of + * building strings is paid only if {@link BaseSettings#DEBUG} is enabled. */ - public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) { - if (DEBUG.get()) { - String logMessage = message.buildMessageString(); - String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); - - if (DEBUG_STACKTRACE.get()) { - var builder = new StringBuilder(logMessage); - var sw = new StringWriter(); - new Throwable().printStackTrace(new PrintWriter(sw)); - - builder.append('\n').append(sw); - logMessage = builder.toString(); - } - - if (ex == null) { - Log.d(logTag, logMessage); - } else { - Log.d(logTag, logMessage, ex); - } + public static void printDebug(LogMessage message, @Nullable Exception ex) { + if (shouldLogDebug()) { + logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false); } } /** * Logs information messages using the outer class name of the code calling this method. */ - public static void printInfo(@NonNull LogMessage message) { + public static void printInfo(LogMessage message) { printInfo(message, null); } /** * Logs information messages using the outer class name of the code calling this method. */ - public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) { - String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); - String logMessage = message.buildMessageString(); - if (ex == null) { - Log.i(logTag, logMessage); - } else { - Log.i(logTag, logMessage, ex); - } + public static void printInfo(LogMessage message, @Nullable Exception ex) { + logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false); } /** * Logs exceptions under the outer class name of the code calling this method. + * Appends the log message, exception (if present), and toast message (if enabled) to logBuffer. */ - public static void printException(@NonNull LogMessage message) { + public static void printException(LogMessage message) { printException(message, null); } /** * Logs exceptions under the outer class name of the code calling this method. *

- * If the calling code is showing it's own error toast, + * If the calling code is showing its own error toast, * instead use {@link #printInfo(LogMessage, Exception)} * * @param message log message * @param ex exception (optional) */ - public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) { - String messageString = message.buildMessageString(); - String outerClassSimpleName = message.findOuterClassSimpleName(); - String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName; - if (ex == null) { - Log.e(logMessage, messageString); - } else { - Log.e(logMessage, messageString, ex); - } - if (DEBUG_TOAST_ON_ERROR.get()) { - Utils.showToastLong(outerClassSimpleName + ": " + messageString); - } + public static void printException(LogMessage message, @Nullable Throwable ex) { + logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast()); } - - /** - * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. - * Normally this method should not be used. - */ - public static void initializationInfo(@NonNull Class callingClass, @NonNull String message) { - Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message); - } - - /** - * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. - * Normally this method should not be used. - */ - public static void initializationException(@NonNull Class callingClass, @NonNull String message, - @Nullable Exception ex) { - Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex); - } - -} \ No newline at end of file +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ResourceType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ResourceType.java new file mode 100644 index 0000000000..48032017a4 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ResourceType.java @@ -0,0 +1,57 @@ +package app.revanced.extension.shared; + +import java.util.HashMap; +import java.util.Map; + +public enum ResourceType { + ANIM("anim"), + ANIMATOR("animator"), + ARRAY("array"), + ATTR("attr"), + BOOL("bool"), + COLOR("color"), + DIMEN("dimen"), + DRAWABLE("drawable"), + FONT("font"), + FRACTION("fraction"), + ID("id"), + INTEGER("integer"), + INTERPOLATOR("interpolator"), + LAYOUT("layout"), + MENU("menu"), + MIPMAP("mipmap"), + NAVIGATION("navigation"), + PLURALS("plurals"), + RAW("raw"), + STRING("string"), + STYLE("style"), + STYLEABLE("styleable"), + TRANSITION("transition"), + VALUES("values"), + XML("xml"); + + private static final Map 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; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java index 4390137de7..c1c2c90d14 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java @@ -70,7 +70,7 @@ public class StringRef { } /** - * Creates a StringRef object that'll not change it's value + * Creates a StringRef object that'll not change its value * * @param value value which toString() method returns when invoked on returned object * @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) { - Context context = Utils.getContext(); + var context = Utils.getContext(); resources = context.getResources(); packageName = context.getPackageName(); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/StringTrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java similarity index 85% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/StringTrieSearch.java rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java index fbff9bebac..9c7b882138 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/StringTrieSearch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java @@ -1,6 +1,4 @@ -package app.revanced.extension.youtube; - -import androidx.annotation.NonNull; +package app.revanced.extension.shared; /** * Text pattern searching using a prefix tree (trie). @@ -28,7 +26,7 @@ public final class StringTrieSearch extends TrieSearch { } } - public StringTrieSearch(@NonNull String... patterns) { + public StringTrieSearch(String... patterns) { super(new StringTrieNode(), patterns); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/TrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java similarity index 90% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/TrieSearch.java rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java index 74fb4685d2..97fa4605d8 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/TrieSearch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java @@ -1,6 +1,5 @@ -package app.revanced.extension.youtube; +package app.revanced.extension.shared; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; @@ -57,11 +56,13 @@ public abstract class TrieSearch { if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) { return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match. } + for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) { if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) { return false; } } + return callback == null || callback.patternMatched(searchText, searchTextIndex - patternStartIndex, patternLength, callbackParameter); } @@ -105,14 +106,18 @@ public abstract class TrieSearch { * Elements not contained can collide with elements the array does contain, * so must compare the nodes character value. * - * Alternatively this array could be a sorted and densely packed array, - * and lookup is done using binary search. - * 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. + /* + * 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. */ @Nullable private TrieNode[] children; @@ -136,7 +141,7 @@ public abstract class TrieSearch { * @param patternLength Length of the pattern. * @param callback Callback, where a value of NULL indicates to always accept a pattern match. */ - private void addPattern(@NonNull T pattern, int patternIndex, int patternLength, + private void addPattern(T pattern, int patternIndex, int patternLength, @Nullable TriePatternMatchedCallback callback) { if (patternIndex == patternLength) { // Reached the end of the pattern. if (endOfPatternCallback == null) { @@ -145,6 +150,7 @@ public abstract class TrieSearch { endOfPatternCallback.add(callback); return; } + if (leaf != null) { // Reached end of the graph and a leaf exist. // Recursively call back into this method and push the existing leaf down 1 level. @@ -159,6 +165,7 @@ public abstract class TrieSearch { leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback); return; } + final char character = getCharValue(pattern, patternIndex); final int arrayIndex = hashIndexForTableSize(children.length, character); TrieNode child = children[arrayIndex]; @@ -183,6 +190,7 @@ public abstract class TrieSearch { //noinspection unchecked TrieNode[] replacement = new TrieNode[replacementArraySize]; addNodeToArray(replacement, child); + boolean collision = false; for (TrieNode existingChild : children) { if (existingChild != null) { @@ -195,6 +203,7 @@ public abstract class TrieSearch { if (collision) { continue; } + children = replacement; return; } @@ -234,6 +243,7 @@ public abstract class TrieSearch { if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) { return true; // Leaf exists and it matched the search text. } + List> endOfPatternCallback = node.endOfPatternCallback; if (endOfPatternCallback != null) { final int matchStartIndex = searchTextIndex - currentMatchLength; @@ -246,6 +256,7 @@ public abstract class TrieSearch { } } } + TrieNode[] children = node.children; if (children == null) { return false; // Reached a graph end point and there's no further patterns to search. @@ -278,9 +289,11 @@ public abstract class TrieSearch { if (leaf != null) { numberOfPointers += 4; // Number of fields in leaf node. } + if (endOfPatternCallback != null) { numberOfPointers += endOfPatternCallback.size(); } + if (children != null) { numberOfPointers += children.length; for (TrieNode child : children) { @@ -308,13 +321,13 @@ public abstract class TrieSearch { private final List patterns = new ArrayList<>(); @SafeVarargs - TrieSearch(@NonNull TrieNode root, @NonNull T... patterns) { + TrieSearch(TrieNode root, T... patterns) { this.root = Objects.requireNonNull(root); addPatterns(patterns); } @SafeVarargs - public final void addPatterns(@NonNull T... patterns) { + public final void addPatterns(T... patterns) { for (T pattern : patterns) { addPattern(pattern); } @@ -325,7 +338,7 @@ public abstract class TrieSearch { * * @param pattern Pattern to add. Calling this with a zero length pattern does nothing. */ - public void addPattern(@NonNull T pattern) { + public void addPattern(T pattern) { addPattern(pattern, root.getTextLength(pattern), null); } @@ -333,31 +346,31 @@ public abstract class TrieSearch { * @param pattern Pattern to add. Calling this with a zero length pattern does nothing. * @param callback Callback to determine if searching should halt when a match is found. */ - public void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback callback) { + public void addPattern(T pattern, TriePatternMatchedCallback callback) { addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback)); } - void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback callback) { + void addPattern(T pattern, int patternLength, @Nullable TriePatternMatchedCallback callback) { if (patternLength == 0) return; // Nothing to match patterns.add(pattern); root.addPattern(pattern, 0, patternLength, callback); } - public final boolean matches(@NonNull T textToSearch) { + public final boolean matches(T textToSearch) { return matches(textToSearch, 0); } - public boolean matches(@NonNull T textToSearch, @NonNull Object callbackParameter) { + public boolean matches(T textToSearch, Object callbackParameter) { return matches(textToSearch, 0, root.getTextLength(textToSearch), Objects.requireNonNull(callbackParameter)); } - public boolean matches(@NonNull T textToSearch, int startIndex) { + public boolean matches(T textToSearch, int startIndex) { return matches(textToSearch, startIndex, root.getTextLength(textToSearch)); } - public final boolean matches(@NonNull T textToSearch, int startIndex, int endIndex) { + public final boolean matches(T textToSearch, int startIndex, int endIndex) { return matches(textToSearch, startIndex, endIndex, null); } @@ -370,11 +383,11 @@ public abstract class TrieSearch { * @param callbackParameter Optional parameter passed to the callbacks. * @return If any pattern matched, and it's callback halted searching. */ - public boolean matches(@NonNull T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) { + public boolean matches(T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) { return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter); } - private boolean matches(@NonNull T textToSearch, int textToSearchLength, int startIndex, int endIndex, + private boolean matches(T textToSearch, int textToSearchLength, int startIndex, int endIndex, @Nullable Object callbackParameter) { if (endIndex > textToSearchLength) { throw new IllegalArgumentException("endIndex: " + endIndex diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index aed89670ce..cf65db8a4c 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -1,13 +1,21 @@ package app.revanced.extension.shared; import android.annotation.SuppressLint; -import android.app.*; +import android.app.Activity; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.net.ConnectivityManager; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -15,38 +23,70 @@ import android.os.Looper; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.util.Pair; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.Window; +import android.view.WindowManager; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; import android.widget.Toast; -import android.widget.Toolbar; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.text.Bidi; -import java.util.*; -import java.util.regex.Pattern; +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; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import app.revanced.extension.shared.settings.AppLanguage; +import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference; +import app.revanced.extension.shared.ui.Dim; public class Utils { @SuppressLint("StaticFieldLeak") - private static Context context; + static volatile Context context; private static String versionName; + private static String applicationLabel; + + @ColorInt + private static int darkColor = Color.BLACK; + @ColorInt + private static int lightColor = Color.WHITE; + + @Nullable + private static Boolean isDarkModeEnabled; + + private static boolean appIsUsingBoldIcons; + + // Cached Collator instance with its locale. + @Nullable + private static Locale cachedCollatorLocale; + @Nullable + private static Collator cachedCollator; + + private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("\\p{P}+"); + private static final Pattern DIACRITICS_PATTERN = Pattern.compile("\\p{M}"); private Utils() { } // utility class @@ -61,28 +101,30 @@ public class Utils { return ""; // Value is replaced during patching. } + private static PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException { + final var packageName = Objects.requireNonNull(getContext()).getPackageName(); + + PackageManager packageManager = context.getPackageManager(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return packageManager.getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(0) + ); + } + + return packageManager.getPackageInfo( + packageName, + 0 + ); + } + /** - * @return The version name of the app, such as 19.11.43 + * @return The version name of the app, such as 20.13.41 */ public static String getAppVersionName() { if (versionName == null) { try { - final var packageName = Objects.requireNonNull(getContext()).getPackageName(); - - PackageManager packageManager = context.getPackageManager(); - PackageInfo packageInfo; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packageInfo = packageManager.getPackageInfo( - packageName, - PackageManager.PackageInfoFlags.of(0) - ); - } else { - packageInfo = packageManager.getPackageInfo( - packageName, - 0 - ); - } - versionName = packageInfo.versionName; + versionName = getPackageInfo().versionName; } catch (Exception ex) { Logger.printException(() -> "Failed to get package info", ex); versionName = "Unknown"; @@ -92,16 +134,30 @@ public class Utils { return versionName; } + @SuppressWarnings("unused") + public static String getApplicationName() { + if (applicationLabel == null) { + try { + ApplicationInfo applicationInfo = getPackageInfo().applicationInfo; + applicationLabel = (String) applicationInfo.loadLabel(context.getPackageManager()); + } catch (Exception ex) { + Logger.printException(() -> "Failed to get application name", ex); + applicationLabel = "Unknown"; + } + } + + return applicationLabel; + } /** * Hide a view by setting its layout height and width to 1dp. * - * @param condition The setting to check for hiding the view. + * @param setting The setting to check for hiding the view. * @param view The view to hide. */ - public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) { - if (hideViewBy0dpUnderCondition(condition.get(), view)) { - Logger.printDebug(() -> "View hidden by setting: " + condition); + public static void hideViewBy0dpUnderCondition(BooleanSetting setting, View view) { + if (hideViewBy0dpUnderCondition(setting.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + setting); } } @@ -113,7 +169,7 @@ public class Utils { */ public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) { if (condition) { - hideViewByLayoutParams(view); + hideViewBy0dp(view); return true; } @@ -121,19 +177,33 @@ public class Utils { } /** - * Hide a view by setting its visibility to GONE. + * Hide a view by setting its layout params to 0x0 + * @param view The view to hide. + */ + 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. * - * @param condition The setting to check for hiding the view. + * @param setting The setting to check for hiding the view. * @param view The view to hide. */ - public static void hideViewUnderCondition(BooleanSetting condition, View view) { - if (hideViewUnderCondition(condition.get(), view)) { - Logger.printDebug(() -> "View hidden by setting: " + condition); + public static void hideViewUnderCondition(BooleanSetting setting, View view) { + if (hideViewUnderCondition(setting.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + setting); } } /** - * Hide a view by setting its visibility to GONE. + * Hide a view by setting its visibility as GONE. * * @param condition The setting to check for hiding the view. * @param view The view to hide. @@ -147,17 +217,17 @@ public class Utils { return false; } - public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) { - if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) { - Logger.printDebug(() -> "View hidden by setting: " + condition); + public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting setting, View view) { + if (hideViewByRemovingFromParentUnderCondition(setting.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + setting); } } - public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) { - if (setting) { + public static boolean hideViewByRemovingFromParentUnderCondition(boolean condition, View view) { + if (condition) { ViewParent parent = view.getParent(); - if (parent instanceof ViewGroup) { - ((ViewGroup) parent).removeView(view); + if (parent instanceof ViewGroup parentGroup) { + parentGroup.removeView(view); return true; } } @@ -170,23 +240,22 @@ public class Utils { * All tasks run at max thread priority. */ private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor( - 3, // 3 threads always ready to go + 3, // 3 threads always ready to go. Integer.MAX_VALUE, - 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle + 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle. TimeUnit.SECONDS, new SynchronousQueue<>(), r -> { // ThreadFactory Thread t = new Thread(r); - t.setPriority(Thread.MAX_PRIORITY); // run at max priority + t.setPriority(Thread.MAX_PRIORITY); // Run at max priority. return t; }); - public static void runOnBackgroundThread(@NonNull Runnable task) { + public static void runOnBackgroundThread(Runnable task) { backgroundThreadPool.execute(task); } - @NonNull - public static Future submitOnBackgroundThread(@NonNull Callable call) { + public static Future submitOnBackgroundThread(Callable call) { return backgroundThreadPool.submit(call); } @@ -201,64 +270,92 @@ public class Utils { long meaninglessValue = 0; while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) { - // could do a thread sleep, but that will trigger an exception if the thread is interrupted + // Could do a thread sleep, but that will trigger an exception if the thread is interrupted. meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random())); } - // 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 the value, otherwise the compiler or VM might optimize and remove the meaningless time-wasting work, + // leaving an empty loop that hammers on the System.currentTimeMillis native call. return meaninglessValue; } - - public static boolean containsAny(@NonNull String value, @NonNull String... targets) { + public static boolean containsAny(String value, String... targets) { return indexOfFirstFound(value, targets) >= 0; } - public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) { - for (String string : targets) { - if (!string.isEmpty()) { - final int indexOf = value.indexOf(string); - if (indexOf >= 0) return indexOf; + 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; + } } } return -1; } /** - * @return zero, if the resource is not found + * @return zero, if the resource is not found. */ @SuppressLint("DiscouragedApi") - public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) { - return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName()); + 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 getResourceIdentifierOrThrow(Context context, @Nullable ResourceType type, String resourceIdentifierName) { + final int resourceId = getResourceIdentifier(context, type, resourceIdentifierName); + if (resourceId == 0) { + throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName + + " type: " + type); + } + return resourceId; } /** - * @return zero, if the resource is not found + * @return zero, if the resource is not found. + * @see #getResourceIdentifierOrThrow(ResourceType, String) */ - public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) { - return getResourceIdentifier(getContext(), resourceIdentifierName, type); + public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) { + return getResourceIdentifier(getContext(), type, resourceIdentifierName); } - public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer")); + /** + * @return zero, if the resource is not found. + * @see #getResourceIdentifier(ResourceType, String) + */ + public static int getResourceIdentifierOrThrow(@Nullable ResourceType type, String resourceIdentifierName) { + return getResourceIdentifierOrThrow(getContext(), type, resourceIdentifierName); } - @NonNull - public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { - return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim")); + public static String getResourceString(int id) throws Resources.NotFoundException { + return getContext().getResources().getString(id); } - public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName)); + } + + public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException { + return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(ResourceType.ANIM, resourceIdentifierName)); + } + + @ColorInt + public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException { //noinspection deprecation - return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color")); + return getContext().getResources().getColor(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName)); } - public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen")); + public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName)); } - public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { - return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen")); + public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getDimension(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName)); + } + + public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(ResourceType.ARRAY, resourceIdentifierName)); } public interface MatchFilter { @@ -267,16 +364,11 @@ public class Utils { /** * Includes sub children. - * - * @noinspection unchecked */ - public static R getChildViewByResourceName(@NonNull View view, @NonNull String str) { - var child = view.findViewById(Utils.getResourceIdentifier(str, "id")); - if (child != null) { - return (R) child; - } - - throw new IllegalArgumentException("View with resource name '" + str + "' not found"); + public static R getChildViewByResourceName(View view, String str) { + var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str)); + //noinspection unchecked + return (R) child; } /** @@ -285,8 +377,8 @@ public class Utils { * @return The first child view that matches the filter. */ @Nullable - public static T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively, - @NonNull MatchFilter filter) { + public static T getChildView(ViewGroup viewGroup, boolean searchRecursively, + MatchFilter filter) { for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) { View childAt = viewGroup.getChildAt(i); @@ -305,7 +397,7 @@ public class Utils { } @Nullable - public static ViewParent getParentView(@NonNull View view, int nthParent) { + public static ViewParent getParentView(View view, int nthParent) { ViewParent parent = view.getParent(); int currentDepth = 0; @@ -323,9 +415,9 @@ public class Utils { return null; } - public static void restartApp(@NonNull Context context) { + public static void restartApp(Context context) { String packageName = context.getPackageName(); - Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName); + Intent intent = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName)); Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent()); // Required for API 34 and later // Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents @@ -336,31 +428,43 @@ public class Utils { public static Context getContext() { if (context == null) { - Logger.initializationException(Utils.class, "Context is null, returning null!", null); + Logger.printException(() -> "Context is not set by extension hook, returning null", null); } return context; } public static void setContext(Context appContext) { + // Intentionally use logger before context is set, + // to expose any bugs in the 'no context available' logger code. + Logger.printInfo(() -> "Set context: " + appContext); + // Must initially set context to check the app language. context = appContext; - // In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies. - // Calling the regular printDebug method here can cause a Settings context null pointer exception, - // even though the context is already set before the call. - // - // The initialization logger methods do not directly or indirectly - // reference the Context or any Settings and are unaffected by this problem. - // - // Info level also helps debug if a patch hook is called before - // the context is set since debug logging is off by default. - Logger.initializationInfo(Utils.class, "Set context: " + appContext); + + AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get(); + if (language != AppLanguage.DEFAULT) { + // Create a new context with the desired language. + Logger.printDebug(() -> "Using app language: " + language); + Configuration config = new Configuration(appContext.getResources().getConfiguration()); + config.setLocale(language.getLocale()); + context = appContext.createConfigurationContext(config); + } + + setThemeLightColor(getThemeColor(getThemeLightColorResourceName(), Color.WHITE)); + setThemeDarkColor(getThemeColor(getThemeDarkColorResourceName(), Color.BLACK)); } - public static void setClipboard(@NonNull String text) { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); - android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text); + public static void setClipboard(CharSequence text) { + ClipboardManager clipboard = (ClipboardManager) context + .getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("ReVanced", text); 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; } @@ -369,22 +473,54 @@ public class Utils { private static Boolean isRightToLeftTextLayout; /** - * 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 + * {@link BaseSettings#REVANCED_LANGUAGE} is set to a different language. */ - public static boolean isRightToLeftTextLayout() { + public static boolean isRightToLeftLocale() { if (isRightToLeftTextLayout == null) { - String displayLanguage = Locale.getDefault().getDisplayLanguage(); - isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft(); + isRightToLeftTextLayout = isRightToLeftLocale(Locale.getDefault()); } return isRightToLeftTextLayout; } + /** + * @return If the locale uses right to left text layout (Hebrew, Arabic, etc.). + */ + public static boolean isRightToLeftLocale(Locale locale) { + String displayLanguage = locale.getDisplayLanguage(); + return new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft(); + } + + /** + * @return A UTF8 string containing a left-to-right or right-to-left + * character of the device locale. If this should match any ReVanced language + * override then instead use {@link #getTextDirectionString(Locale)} with + * {@link BaseSettings#REVANCED_LANGUAGE}. + */ + public static String getTextDirectionString() { + return getTextDirectionString(isRightToLeftLocale()); + } + + @SuppressWarnings("unused") + public static String getTextDirectionString(Locale locale) { + return getTextDirectionString(isRightToLeftLocale(locale)); + } + + private static String getTextDirectionString(boolean isRightToLeft) { + return isRightToLeft + ? "\u200F" // u200F = right to left character. + : "\u200E"; // u200E = left to right character. + } + /** * @return if the text contains at least 1 number character, - * including any unicode numbers such as Arabic. + * including any Unicode numbers such as Arabic. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public static boolean containsNumber(@NonNull CharSequence text) { + public static boolean containsNumber(CharSequence text) { for (int index = 0, length = text.length(); index < length;) { final int codePoint = Character.codePointAt(text, index); if (Character.isDigit(codePoint)) { @@ -423,7 +559,7 @@ public class Utils { super.onStart(); if (onStartAction != null) { - onStartAction.onStart((AlertDialog) getDialog()); + onStartAction.onStart(dialog); } } catch (Exception ex) { Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex); @@ -432,34 +568,34 @@ public class Utils { } /** - * Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}. + * Interface for {@link #showDialog(Activity, Dialog, boolean, DialogFragmentOnStartAction)}. */ @FunctionalInterface public interface DialogFragmentOnStartAction { - void onStart(AlertDialog dialog); + void onStart(Dialog dialog); } - public static void showDialog(Activity activity, AlertDialog dialog) { + public static void showDialog(Activity activity, Dialog dialog) { showDialog(activity, dialog, true, null); } /** - * Utility method to allow showing an AlertDialog on top of other alert dialogs. + * Utility method to allow showing a Dialog on top of other dialogs. * Calling this will always display the dialog on top of all other dialogs * previously called using this method. - *
+ *

* Be aware the on start action can be called multiple times for some situations, * such as the user switching apps without dismissing the dialog then switching back to this app. - *
+ *

* This method is only useful during app startup and multiple patches may show their own dialog, * and the most important dialog can be called last (using a delay) so it's always on top. - *
+ *

* For all other situations it's better to not use this method and - * call {@link AlertDialog#show()} on the dialog. + * call {@link Dialog#show()} on the dialog. */ @SuppressWarnings("deprecation") public static void showDialog(Activity activity, - AlertDialog dialog, + Dialog dialog, boolean isCancelable, @Nullable DialogFragmentOnStartAction onStartAction) { verifyOnMainThread(); @@ -473,30 +609,65 @@ public class Utils { } /** - * Safe to call from any thread + * Safe to call from any thread. */ - public static void showToastShort(@NonNull String messageToToast) { + public static void showToastShort(String messageToToast) { showToast(messageToToast, Toast.LENGTH_SHORT); } /** - * Safe to call from any thread + * Safe to call from any thread. */ - public static void showToastLong(@NonNull String messageToToast) { + public static void showToastLong(String messageToToast) { showToast(messageToToast, Toast.LENGTH_LONG); } - private static void showToast(@NonNull String messageToToast, int toastDuration) { + /** + * Safe to call from any thread. + * + * @param messageToToast Message to show. + * @param toastDuration Either {@link Toast#LENGTH_SHORT} or {@link Toast#LENGTH_LONG}. + */ + public static void showToast(String messageToToast, int toastDuration) { Objects.requireNonNull(messageToToast); runOnMainThreadNowOrLater(() -> { - if (context == null) { - Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null); - } else { - Logger.printDebug(() -> "Showing toast: " + messageToToast); - Toast.makeText(context, messageToToast, toastDuration).show(); - } - } - ); + Context currentContext = context; + + if (currentContext == null) { + Logger.printException(() -> "Cannot show toast (context is null): " + messageToToast); + } else { + Logger.printDebug(() -> "Showing toast: " + messageToToast); + Toast.makeText(currentContext, messageToToast, toastDuration).show(); + } + }); + } + + /** + * @return The current dark mode as set by any patch. + * Or if none is set, then the system dark mode status is returned. + */ + public static boolean isDarkModeEnabled() { + Boolean isDarkMode = isDarkModeEnabled; + if (isDarkMode != null) { + return isDarkMode; + } + + Configuration config = Resources.getSystem().getConfiguration(); + final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return currentNightMode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * Overrides dark mode status as returned by {@link #isDarkModeEnabled()}. + */ + public static void setIsDarkModeEnabled(boolean isDarkMode) { + isDarkModeEnabled = isDarkMode; + Logger.printDebug(() -> "Dark mode status: " + isDarkMode); + } + + public static boolean isLandscapeOrientation() { + final int orientation = Resources.getSystem().getConfiguration().orientation; + return orientation == Configuration.ORIENTATION_LANDSCAPE; } /** @@ -504,14 +675,14 @@ public class Utils { * * @see #runOnMainThreadNowOrLater(Runnable) */ - public static void runOnMainThread(@NonNull Runnable runnable) { + public static void runOnMainThread(Runnable runnable) { runOnMainThreadDelayed(runnable, 0); } /** - * Automatically logs any exceptions the runnable throws + * Automatically logs any exceptions the runnable throws. */ - public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) { + public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) { Runnable loggingRunnable = () -> { try { runnable.run(); @@ -523,10 +694,10 @@ public class Utils { } /** - * If called from the main thread, the code is run immediately.

+ * If called from the main thread, the code is run immediately. * If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}. */ - public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) { + public static void runOnMainThreadNowOrLater(Runnable runnable) { if (isCurrentlyOnMainThread()) { runnable.run(); } else { @@ -535,14 +706,14 @@ public class Utils { } /** - * @return if the calling thread is on the main thread + * @return if the calling thread is on the main thread. */ public static boolean isCurrentlyOnMainThread() { return Looper.getMainLooper().isCurrentThread(); } /** - * @throws IllegalStateException if the calling thread is _off_ the main thread + * @throws IllegalStateException if the calling thread is _off_ the main thread. */ public static void verifyOnMainThread() throws IllegalStateException { if (!isCurrentlyOnMainThread()) { @@ -551,7 +722,7 @@ public class Utils { } /** - * @throws IllegalStateException if the calling thread is _on_ the main thread + * @throws IllegalStateException if the calling thread is _on_ the main thread. */ public static void verifyOffMainThread() throws IllegalStateException { if (isCurrentlyOnMainThread()) { @@ -559,19 +730,41 @@ public class Utils { } } + public static void openLink(String url) { + try { + Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + Logger.printInfo(() -> "Opening link with external browser: " + intent); + getContext().startActivity(intent); + } catch (Exception ex) { + Logger.printException(() -> "openLink failure", ex); + } + } + public enum NetworkType { NONE, MOBILE, OTHER, } + /** + * Calling extension code must ensure the un-patched app has the permission + * android.permission.ACCESS_NETWORK_STATE, + * otherwise the app will crash if this method is used. + */ public static boolean isNetworkConnected() { NetworkType networkType = getNetworkType(); return networkType == NetworkType.MOBILE || networkType == NetworkType.OTHER; } - @SuppressLint("MissingPermission") // permission already included in YouTube + /** + * Calling extension code must ensure the un-patched app has the permission + * android.permission.ACCESS_NETWORK_STATE, + * otherwise the app will crash if this method is used. + */ + @SuppressWarnings({"MissingPermission", "deprecation"}) public static NetworkType getNetworkType() { Context networkContext = getContext(); if (networkContext == null) { @@ -589,31 +782,188 @@ public class Utils { } /** - * Hide a view by setting its layout params to 0x0 - * @param view The view to hide. + * Hides a view by setting its layout width and height to 0dp. + * Handles null layout params safely. + * + * @param view The view to hide. If null, does nothing. */ - public static void hideViewByLayoutParams(View view) { - if (view instanceof LinearLayout) { - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0); - view.setLayoutParams(layoutParams); - } else if (view instanceof FrameLayout) { - FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0); - view.setLayoutParams(layoutParams2); - } else if (view instanceof RelativeLayout) { - RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0); - view.setLayoutParams(layoutParams3); - } else if (view instanceof Toolbar) { - Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0); - view.setLayoutParams(layoutParams4); - } else if (view instanceof ViewGroup) { - ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0); - view.setLayoutParams(layoutParams5); + public static void hideViewByLayoutParams(@Nullable View view) { + if (view == null) return; + + ViewGroup.LayoutParams params = view.getLayoutParams(); + + if (params == null) { + // Create generic 0x0 layout params accepted by all ViewGroups. + params = new ViewGroup.LayoutParams(0, 0); } else { - ViewGroup.LayoutParams params = view.getLayoutParams(); params.width = 0; params.height = 0; - view.setLayoutParams(params); } + + view.setLayoutParams(params); + } + + /** + * Configures the parameters of a dialog window, including its width, gravity, vertical offset and background dimming. + * The width is calculated as a percentage of the screen's portrait width and the vertical offset is specified in DIP. + * The default dialog background is removed to allow for custom styling. + * + * @param window The {@link Window} object to configure. + * @param gravity The gravity for positioning the dialog (e.g., {@link Gravity#BOTTOM}). + * @param yOffsetDip The vertical offset from the gravity position in DIP. + * @param widthPercentage The width of the dialog as a percentage of the screen's portrait width (0-100). + * @param dimAmount If true, sets the background dim amount to 0 (no dimming); if false, leaves the default dim amount. + */ + public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) { + WindowManager.LayoutParams params = window.getAttributes(); + + params.width = Dim.pctPortraitWidth(widthPercentage); + params.height = WindowManager.LayoutParams.WRAP_CONTENT; + params.gravity = gravity; + params.y = yOffsetDip > 0 ? Dim.dp(yOffsetDip) : 0; + if (dimAmount) { + params.dimAmount = 0f; + } + + window.setAttributes(params); // Apply window attributes. + 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. + */ + public static void setThemeLightColor(@ColorInt int color) { + Logger.printDebug(() -> "Setting theme light color: " + getColorHexString(color)); + lightColor = color; + } + + /** + * Sets the theme dark used by the app. + */ + public static void setThemeDarkColor(@ColorInt int color) { + Logger.printDebug(() -> "Setting theme dark color: " + getColorHexString(color)); + darkColor = color; + } + + /** + * Returns the themed light color, or {@link Color#WHITE} if no theme was set using + * {@link #setThemeLightColor(int). + */ + @ColorInt + public static int getThemeLightColor() { + return lightColor; + } + + /** + * Returns the themed dark color, or {@link Color#BLACK} if no theme was set using + * {@link #setThemeDarkColor(int)}. + */ + @ColorInt + public static int getThemeDarkColor() { + return darkColor; + } + + /** + * Injection point. + */ + @SuppressWarnings("SameReturnValue") + private static String getThemeLightColorResourceName() { + // Value is changed by Settings patch. + return "#FFFFFFFF"; + } + + /** + * Injection point. + */ + @SuppressWarnings("SameReturnValue") + private static String getThemeDarkColorResourceName() { + // Value is changed by Settings patch. + return "#FF000000"; + } + + @ColorInt + private static int getThemeColor(String resourceName, int defaultColor) { + try { + return getColorFromString(resourceName); + } catch (Exception ex) { + // This code can never be reached since a bad custom color will + // fail during resource compilation. So no localized strings are needed here. + Logger.printException(() -> "Invalid custom theme color: " + resourceName, ex); + return defaultColor; + } + } + + + @ColorInt + public static int getDialogBackgroundColor() { + if (isDarkModeEnabled()) { + final int darkColor = getThemeDarkColor(); + return darkColor == Color.BLACK + // Lighten the background a little if using AMOLED dark theme + // as the dialogs are almost invisible. + ? 0xFF080808 // 3% + : darkColor; + } + return getThemeLightColor(); + } + + /** + * @return The current app background color. + */ + @ColorInt + public static int getAppBackgroundColor() { + return isDarkModeEnabled() ? getThemeDarkColor() : getThemeLightColor(); + } + + /** + * @return The current app foreground color. + */ + @ColorInt + public static int getAppForegroundColor() { + return isDarkModeEnabled() + ? getThemeLightColor() + : getThemeDarkColor(); + } + + @ColorInt + public static int getOkButtonBackgroundColor() { + return isDarkModeEnabled() + // Must be inverted color. + ? Color.WHITE + : Color.BLACK; + } + + @ColorInt + public static int getCancelOrNeutralButtonBackgroundColor() { + return isDarkModeEnabled() + ? adjustColorBrightness(getDialogBackgroundColor(), 1.10f) + : adjustColorBrightness(getThemeLightColor(), 0.95f); + } + + @ColorInt + public static int getEditTextBackground() { + return isDarkModeEnabled() + ? adjustColorBrightness(getDialogBackgroundColor(), 1.05f) + : adjustColorBrightness(getThemeLightColor(), 0.97f); + } + + public static String getColorHexString(@ColorInt int color) { + return String.format("#%06X", (0x00FFFFFF & color)); } /** @@ -641,8 +991,7 @@ public class Utils { this.keySuffix = keySuffix; } - @NonNull - static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) { + static Sort fromKey(@Nullable String key, Sort defaultSort) { if (key != null) { for (Sort sort : values()) { if (key.endsWith(sort.keySuffix)) { @@ -654,35 +1003,66 @@ public class Utils { } } - private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+"); - /** - * Strips all punctuation and converts to lower case. A null parameter returns an empty string. + * Removes punctuation and converts text to lowercase. Returns an empty string if input is null. */ - public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) { + public static String removePunctuationToLowercase(@Nullable CharSequence original) { if (original == null) return ""; - return punctuationPattern.matcher(original).replaceAll("").toLowerCase(); + return PUNCTUATION_PATTERN.matcher(original).replaceAll("") + .toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale()); } /** - * Sort a PreferenceGroup and all it's sub groups by title or key. + * Normalizes text for search: applies NFD, removes diacritics, and lowercases (locale-neutral). + * Returns an empty string if input is null. + */ + public static String normalizeTextToLowercase(@Nullable CharSequence original) { + if (original == null) return ""; + return DIACRITICS_PATTERN.matcher(Normalizer.normalize(original, Normalizer.Form.NFD)) + .replaceAll("").toLowerCase(Locale.ROOT); + } + + /** + * Returns a cached Collator for the current locale, or creates a new one if locale changed. + */ + private static Collator getCollator() { + Locale currentLocale = BaseSettings.REVANCED_LANGUAGE.get().getLocale(); + + if (cachedCollator == null || !currentLocale.equals(cachedCollatorLocale)) { + cachedCollatorLocale = currentLocale; + cachedCollator = Collator.getInstance(currentLocale); + cachedCollator.setStrength(Collator.SECONDARY); // Case-insensitive, diacritic-insensitive. + } + + return cachedCollator; + } + + /** + * Sorts a {@link PreferenceGroup} and all nested subgroups by title or key. + *

+ * The sort order is controlled by the {@link Sort} suffix present in the preference key. + * Preferences without a key or without a {@link Sort} suffix remain in their original order. + *

+ * Sorting is performed using {@link Collator} with the current user locale, + * ensuring correct alphabetical ordering for all supported languages + * (e.g., Ukrainian "і", German "ß", French accented characters, etc.). * - * Sort order is determined by the preferences key {@link Sort} suffix. - * - * If a preference has no key or no {@link Sort} suffix, - * then the preferences are left unsorted. + * @param group the {@link PreferenceGroup} to sort */ @SuppressWarnings("deprecation") - public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { + public static void sortPreferenceGroups(PreferenceGroup group) { Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); - SortedMap preferences = new TreeMap<>(); + List> preferences = new ArrayList<>(); + + // Get cached Collator for locale-aware string comparison. + Collator collator = getCollator(); for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { Preference preference = group.getPreference(i); final Sort preferenceSort; - if (preference instanceof PreferenceGroup) { - sortPreferenceGroups((PreferenceGroup) preference); + if (preference instanceof PreferenceGroup subGroup) { + sortPreferenceGroups(subGroup); preferenceSort = groupSort; // Sort value for groups is for it's content, not itself. } else { // Allow individual preferences to set a key sorting. @@ -693,7 +1073,7 @@ public class Utils { final String sortValue; switch (preferenceSort) { case BY_TITLE: - sortValue = removePunctuationConvertToLowercase(preference.getTitle()); + sortValue = removePunctuationToLowercase(preference.getTitle()); break; case BY_KEY: sortValue = preference.getKey(); @@ -704,17 +1084,23 @@ public class Utils { throw new IllegalStateException(); } - preferences.put(sortValue, preference); + preferences.add(new Pair<>(sortValue, preference)); } + // Sort the list using locale-specific collation rules. + Collections.sort(preferences, (pair1, pair2) + -> collator.compare(pair1.first, pair2.first)); + + // Reassign order values to reflect the new sorted sequence int index = 0; - for (Preference pref : preferences.values()) { + for (Pair pair : preferences) { int order = index++; + Preference pref = pair.second; // Move any screens, intents, and the one off About preference to the top. if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference || pref.getIntent() != null) { - // Arbitrary high number. + // Any arbitrary large number. order -= 1000; } @@ -726,7 +1112,7 @@ public class Utils { * Set all preferences to multiline titles if the device is not using an English variant. * The English strings are heavily scrutinized and all titles fit on screen * except 2 or 3 preference strings and those do not affect readability. - * + *

* Allowing multiline for those 2 or 3 English preferences looks weird and out of place, * and visually it looks better to clip the text and keep all titles 1 line. */ @@ -736,8 +1122,8 @@ public class Utils { return; } - String deviceLanguage = Utils.getContext().getResources().getConfiguration().locale.getLanguage(); - if (deviceLanguage.equals("en")) { + String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage(); + if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) { return; } @@ -745,26 +1131,92 @@ public class Utils { Preference pref = group.getPreference(i); pref.setSingleLineTitle(false); - if (pref instanceof PreferenceGroup) { - setPreferenceTitlesToMultiLineIfNeeded((PreferenceGroup) pref); + if (pref instanceof PreferenceGroup subGroup) { + setPreferenceTitlesToMultiLineIfNeeded(subGroup); } } } /** - * If {@link Fragment} uses [Android library] rather than [AndroidX library], - * the Dialog theme corresponding to [Android library] should be used. - *

- * If not, the following issues will occur: - * ReVanced/revanced-patches#3061 - *

- * To prevent these issues, apply the Dialog theme corresponding to [Android library]. + * Parse a color resource or hex code to an int representation of the color. */ - public static void setEditTextDialogTheme(AlertDialog.Builder builder) { - final int editTextDialogStyle = getResourceIdentifier( - "revanced_edit_text_dialog_style", "style"); - if (editTextDialogStyle != 0) { - builder.getContext().setTheme(editTextDialogStyle); + @ColorInt + public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException { + if (colorString.startsWith("#")) { + return Color.parseColor(colorString); } + return getResourceColor(colorString); + } + + /** + * Uses {@link #adjustColorBrightness(int, float)} depending on if light or dark mode is active. + */ + @ColorInt + public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) { + return isDarkModeEnabled() + ? adjustColorBrightness(baseColor, darkThemeFactor) + : adjustColorBrightness(baseColor, lightThemeFactor); + } + + /** + * Adjusts the brightness of a color by lightening or darkening it based on the given factor. + *

+ * If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF). + * If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000). + * The alpha channel remains unchanged. + * + * @param color The input color to adjust, in ARGB format. + * @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening) + * or values <= 1.0f to darken (e.g., 0.95f for slight darkening). + * @return The adjusted color in ARGB format. + */ + @ColorInt + public static int adjustColorBrightness(@ColorInt int color, float factor) { + final int alpha = Color.alpha(color); + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + + if (factor > 1.0f) { + // Lighten: Interpolate toward white (255). + final float t = 1.0f - (1.0f / factor); // Interpolation parameter. + red = Math.round(red + (255 - red) * t); + green = Math.round(green + (255 - green) * t); + blue = Math.round(blue + (255 - blue) * t); + } else { + // Darken or no change: Scale toward black. + red = Math.round(red * factor); + green = Math.round(green * factor); + blue = Math.round(blue * factor); + } + + // Ensure values are within [0, 255]. + red = clamp(red, 0, 255); + green = clamp(green, 0, 255); + blue = clamp(blue, 0, 255); + + return Color.argb(alpha, red, green, blue); + } + + public static int clamp(int value, int lower, int upper) { + return Math.max(lower, Math.min(value, upper)); + } + + 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 Map createSizeRestrictedMap(int maxSize) { + return new LinkedHashMap<>(2 * maxSize) { + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > maxSize; + } + }; } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java index 6d4db14e57..bde66a043c 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java @@ -4,23 +4,32 @@ import static android.text.Html.FROM_HTML_MODE_COMPACT; import static app.revanced.extension.shared.StringRef.str; import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction; -import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; +import android.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; +import android.view.View; import android.widget.Button; +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; @@ -69,7 +78,6 @@ abstract class Check { BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE); } - @SuppressLint("NewApi") static void issueWarning(Activity activity, Collection failedChecks) { final var reasons = new StringBuilder(); @@ -86,38 +94,59 @@ abstract class Check { ); Utils.runOnMainThreadDelayed(() -> { - AlertDialog alert = new AlertDialog.Builder(activity) - .setCancelable(false) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setTitle(str("revanced_check_environment_failed_title")) - .setMessage(message) - .setPositiveButton( - " ", - (dialog, which) -> { - final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(intent); + // Create the custom dialog. + Pair dialogPair = CustomDialog.create( + activity, + str("revanced_check_environment_failed_title"), // Title. + message, // Message. + null, // No EditText. + str("revanced_check_environment_dialog_open_official_source_button"), // OK button text. + () -> { + // Action for the OK (website) button. + final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); - // Shutdown to prevent the user from navigating back to this app, - // which is no longer showing a warning dialog. - activity.finishAffinity(); - System.exit(0); - } - ).setNegativeButton( - " ", - (dialog, which) -> { - // Cleanup data if the user incorrectly imported a huge negative number. - final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()); - BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1); + // Shutdown to prevent the user from navigating back to this app, + // which is no longer showing a warning dialog. + activity.finishAffinity(); + System.exit(0); + }, + null, // No cancel button. + str("revanced_check_environment_dialog_ignore_button"), // Neutral button text. + () -> { + // Neutral button action. + // Cleanup data if the user incorrectly imported a huge negative number. + final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()); + BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1); + }, + true // Dismiss dialog when onNeutralClick. + ); - dialog.dismiss(); - } - ).create(); + // Get the dialog and main layout. + Dialog dialog = dialogPair.first; + LinearLayout mainLayout = dialogPair.second; - Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() { + // Add icon to the dialog. + ImageView iconView = new ImageView(activity); + iconView.setImageResource(Utils.getResourceIdentifierOrThrow( + ResourceType.DRAWABLE, "revanced_ic_dialog_alert")); + iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); + iconView.setPadding(0, 0, 0, 0); + LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + iconParams.gravity = Gravity.CENTER; + mainLayout.addView(iconView, 0); // Add icon at the top. + + dialog.setCancelable(false); + + // Show the dialog. + Utils.showDialog(activity, dialog, false, new DialogFragmentOnStartAction() { boolean hasRun; @Override - public void onStart(AlertDialog dialog) { + public void onStart(Dialog dialog) { // Only run this once, otherwise if the user changes to a different app // then changes back, this handler will run again and disable the buttons. if (hasRun) { @@ -125,19 +154,43 @@ abstract class Check { } hasRun = true; - var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + // Get the button container to access buttons. + LinearLayout buttonContainer = (LinearLayout) mainLayout.getChildAt(mainLayout.getChildCount() - 1); + + Button openWebsiteButton; + Button ignoreButton; + + // Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer). + if (buttonContainer.getChildCount() == 1 + && buttonContainer.getChildAt(0) instanceof LinearLayout rowContainer) { + // Neutral button is the first child (index 0). + ignoreButton = (Button) rowContainer.getChildAt(0); + // OK button is the last child. + openWebsiteButton = (Button) rowContainer.getChildAt(rowContainer.getChildCount() - 1); + } else { + // Multi-row layout: buttons are in separate containers, ordered OK, Cancel, Neutral. + LinearLayout okContainer = + (LinearLayout) buttonContainer.getChildAt(0); // OK is first. + openWebsiteButton = (Button) okContainer.getChildAt(0); + LinearLayout neutralContainer = + (LinearLayout)buttonContainer.getChildAt(buttonContainer.getChildCount() - 1); // Neutral is last. + ignoreButton = (Button) neutralContainer.getChildAt(0); + } + + // Initially set buttons to INVISIBLE and disabled. + openWebsiteButton.setVisibility(View.INVISIBLE); openWebsiteButton.setEnabled(false); + ignoreButton.setVisibility(View.INVISIBLE); + ignoreButton.setEnabled(false); - var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); - dismissButton.setEnabled(false); - - getCountdownRunnable(dismissButton, openWebsiteButton).run(); + // Start the countdown for showing and enabling buttons. + getCountdownRunnable(ignoreButton, openWebsiteButton).run(); } }); }, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs. } - private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) { + private static Runnable getCountdownRunnable(Button ignoreButton, Button openWebsiteButton) { return new Runnable() { private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON; @@ -146,17 +199,15 @@ abstract class Check { Utils.verifyOnMainThread(); if (secondsRemaining > 0) { - if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) { - openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button")); + if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) { + openWebsiteButton.setVisibility(View.VISIBLE); openWebsiteButton.setEnabled(true); } - secondsRemaining--; - Utils.runOnMainThreadDelayed(this, 1000); } else { - dismissButton.setText(str("revanced_check_environment_dialog_ignore_button")); - dismissButton.setEnabled(true); + ignoreButton.setVisibility(View.VISIBLE); + ignoreButton.setEnabled(true); } } }; diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java index d63f8b7e3f..e54ab27f74 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java @@ -7,8 +7,12 @@ 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; @@ -27,6 +31,7 @@ import static app.revanced.extension.shared.checks.PatchInfo.Build.*; *
* 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(); @@ -39,6 +44,7 @@ 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"); @@ -118,7 +124,7 @@ public final class CheckEnvironmentPatch { * If the build properties are different, the app was likely downloaded pre-patched or patched on another device. */ private static class CheckWasPatchedOnSameDevice extends Check { - @SuppressLint({"NewApi", "HardwareIds"}) + @SuppressLint("HardwareIds") @Override protected Boolean check() { if (PATCH_BOARD.isEmpty()) { @@ -192,7 +198,7 @@ public final class CheckEnvironmentPatch { PackageManager packageManager = context.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); - // Duration since initial install or last update, which ever is sooner. + // Duration since initial install or last update, whichever is sooner. durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME; Logger.printInfo(() -> "App was installed/updated: " + (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching")); @@ -288,8 +294,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 install source is not Manager or ADB. + // Allow installing recently patched APKs, + // even if the installation source is not Manager or ADB. Check.disableForever(); return; } else { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/BaseFixRedgifsApiPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/BaseFixRedgifsApiPatch.java new file mode 100644 index 0000000000..00ee6def3b --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/BaseFixRedgifsApiPatch.java @@ -0,0 +1,70 @@ +package app.revanced.extension.shared.fixes.redgifs; + +import androidx.annotation.NonNull; + +import org.json.JSONException; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import app.revanced.extension.shared.Logger; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public abstract class BaseFixRedgifsApiPatch implements Interceptor { + protected static BaseFixRedgifsApiPatch INSTANCE; + public abstract String getDefaultUserAgent(); + + @NonNull + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + Request request = chain.request(); + if (!request.url().host().equals("api.redgifs.com")) { + return chain.proceed(request); + } + + String userAgent = getDefaultUserAgent(); + + if (request.header("Authorization") != null) { + Response response = chain.proceed(request.newBuilder().header("User-Agent", userAgent).build()); + if (response.isSuccessful()) { + return response; + } + // It's possible that the user agent is being overwritten later down in the interceptor + // chain, so make sure we grab the new user agent from the request headers. + userAgent = response.request().header("User-Agent"); + response.close(); + } + + try { + RedgifsTokenManager.RedgifsToken token = RedgifsTokenManager.refreshToken(userAgent); + + // Emulate response for old OAuth endpoint + if (request.url().encodedPath().equals("/v2/oauth/client")) { + String responseBody = RedgifsTokenManager.getEmulatedOAuthResponseBody(token); + return new Response.Builder() + .message("OK") + .code(HttpURLConnection.HTTP_OK) + .protocol(Protocol.HTTP_1_1) + .request(request) + .header("Content-Type", "application/json") + .body(ResponseBody.create( + responseBody, MediaType.get("application/json"))) + .build(); + } + + Request modifiedRequest = request.newBuilder() + .header("Authorization", "Bearer " + token.getAccessToken()) + .header("User-Agent", userAgent) + .build(); + return chain.proceed(modifiedRequest); + } catch (JSONException ex) { + Logger.printException(() -> "Could not parse Redgifs response", ex); + throw new IOException(ex); + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/RedgifsTokenManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/RedgifsTokenManager.java new file mode 100644 index 0000000000..792465a89f --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/redgifs/RedgifsTokenManager.java @@ -0,0 +1,94 @@ +package app.revanced.extension.shared.fixes.redgifs; + +import static app.revanced.extension.shared.requests.Route.Method.GET; + +import androidx.annotation.GuardedBy; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import app.revanced.extension.shared.requests.Requester; + + +/** + * Manages Redgifs token lifecycle. + */ +public class RedgifsTokenManager { + public static class RedgifsToken { + // Expire after 23 hours to provide some breathing room + private static final long EXPIRY_SECONDS = 23 * 60 * 60; + + private final String accessToken; + private final long refreshTimeInSeconds; + + public RedgifsToken(String accessToken, long refreshTime) { + this.accessToken = accessToken; + this.refreshTimeInSeconds = refreshTime; + } + + public String getAccessToken() { + return accessToken; + } + + public long getExpiryTimeInSeconds() { + return refreshTimeInSeconds + EXPIRY_SECONDS; + } + + public boolean isValid() { + if (accessToken == null) return false; + return getExpiryTimeInSeconds() >= System.currentTimeMillis() / 1000; + } + } + public static final String REDGIFS_API_HOST = "https://api.redgifs.com"; + private static final String GET_TEMPORARY_TOKEN = REDGIFS_API_HOST + "/v2/auth/temporary"; + @GuardedBy("itself") + private static final Map tokenMap = new HashMap<>(); + + private static String getToken(String userAgent) throws IOException, JSONException { + HttpURLConnection connection = (HttpURLConnection) new URL(GET_TEMPORARY_TOKEN).openConnection(); + connection.setFixedLengthStreamingMode(0); + connection.setRequestMethod(GET.name()); + connection.setRequestProperty("User-Agent", userAgent); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setUseCaches(false); + + JSONObject responseObject = Requester.parseJSONObject(connection); + return responseObject.getString("token"); + } + + public static RedgifsToken refreshToken(String userAgent) throws IOException, JSONException { + synchronized(tokenMap) { + // Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67 + RedgifsToken token = tokenMap.get(userAgent); + if (token != null && token.isValid()) { + return token; + } + + // Copy user agent from original request if present because Redgifs verifies + // that the user agent in subsequent requests matches the one in the OAuth token. + String accessToken = getToken(userAgent); + long refreshTime = System.currentTimeMillis() / 1000; + token = new RedgifsToken(accessToken, refreshTime); + tokenMap.put(userAgent, token); + return token; + } + } + + public static String getEmulatedOAuthResponseBody(RedgifsToken token) throws JSONException { + // Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67 + JSONObject responseObject = new JSONObject(); + responseObject.put("access_token", token.accessToken); + responseObject.put("expiry_time", token.getExpiryTimeInSeconds() - (System.currentTimeMillis() / 1000)); + responseObject.put("scope", "read"); + responseObject.put("token_type", "Bearer"); + return responseObject.toString(); + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java similarity index 56% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java index ccc2cc8c91..d12eabc0bd 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java @@ -1,16 +1,20 @@ -package app.revanced.extension.youtube.patches; +package app.revanced.extension.shared.patches; import static app.revanced.extension.shared.StringRef.str; import android.app.Activity; +import android.app.Dialog; import android.text.Html; +import android.util.Pair; +import android.widget.LinearLayout; import java.net.InetAddress; import java.net.UnknownHostException; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; -import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.ui.CustomDialog; @SuppressWarnings("unused") public class CheckWatchHistoryDomainNameResolutionPatch { @@ -42,10 +46,10 @@ public class CheckWatchHistoryDomainNameResolutionPatch { /** * Injection point. * - * Checks if s.youtube.com is blacklisted and playback history will fail to work. + * Checks if YouTube watch history endpoint cannot be reached. */ public static void checkDnsResolver(Activity context) { - if (!Utils.isNetworkConnected() || !Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return; + if (!Utils.isNetworkConnected() || !BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return; Utils.runOnBackgroundThread(() -> { try { @@ -57,24 +61,30 @@ public class CheckWatchHistoryDomainNameResolutionPatch { // Prevent this false positive by verify youtube.com resolves. // If youtube.com does not resolve, then it's not a watch history domain resolving error // because the entire app will not work since no domains are resolving. - if (domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT) - || !domainResolvesToValidIP("youtube.com")) { + String domainYouTube = "youtube.com"; + if (!domainResolvesToValidIP(domainYouTube) + || domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT) + // Check multiple times, so a false positive from a flaky connection is almost impossible. + || !domainResolvesToValidIP(domainYouTube) + || domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT)) { return; } Utils.runOnMainThread(() -> { - var alert = new android.app.AlertDialog.Builder(context) - .setTitle(str("revanced_check_watch_history_domain_name_dialog_title")) - .setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message"))) - .setIconAttribute(android.R.attr.alertDialogIcon) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - dialog.dismiss(); - }).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> { - Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false); - dialog.dismiss(); - }).create(); + Pair dialogPair = CustomDialog.create( + context, + str("revanced_check_watch_history_domain_name_dialog_title"), // Title. + Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML). + null, // No EditText. + null, // OK button text. + () -> {}, // OK button action (just dismiss). + null, // No cancel button. + str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text. + () -> BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore). + true // Dismiss dialog on Neutral button click. + ); - Utils.showDialog(context, alert, false, null); + Utils.showDialog(context, dialogPair.first, false, null); }); } catch (Exception ex) { Logger.printException(() -> "checkDnsResolver failure", ex); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java new file mode 100644 index 0000000000..d13513e2df --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CustomBrandingPatch.java @@ -0,0 +1,227 @@ +package app.revanced.extension.shared.patches; + +import android.app.Notification; +import android.content.ComponentName; +import android.content.Context; +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; + +/** + * Patch shared by YouTube and YT Music. + */ +@SuppressWarnings("unused") +public class CustomBrandingPatch { + + // Important: In the future, additional branding themes can be added but all existing and prior + // themes cannot be removed or renamed. + // + // This is because if a user has a branding theme selected, then only that launch alias is enabled. + // If a future update removes or renames that alias, then after updating the app is effectively + // broken and it cannot be opened and not even clearing the app data will fix it. + // In that situation the only fix is to completely uninstall and reinstall again. + // + // The most that can be done is to hide a theme from the UI and keep the alias with dummy data. + public enum BrandingTheme { + /** + * Original unpatched icon. + */ + ORIGINAL, + ROUNDED, + MINIMAL, + SCALED, + /** + * User provided custom icon. + */ + CUSTOM; + + private String packageAndNameIndexToClassAlias(String packageName, int appIndex) { + if (appIndex <= 0) { + throw new IllegalArgumentException("App index starts at index 1"); + } + return packageName + ".revanced_" + name().toLowerCase(Locale.US) + '_' + appIndex; + } + } + + @Nullable + private static Integer 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; + } + + 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"); + } + } + } + return notificationSmallIcon; + } + + /** + * Injection point. + */ + public static View getLottieViewOrNull(View lottieStartupView) { + if (BaseSettings.CUSTOM_BRANDING_ICON.get() == BrandingTheme.ORIGINAL) { + return lottieStartupView; + } + + return null; + } + + /** + * Injection point. + */ + public static void setNotificationIcon(Notification.Builder builder) { + try { + final int smallIcon = getNotificationSmallIcon(); + if (smallIcon != 0) { + builder.setSmallIcon(smallIcon) + .setColor(Color.TRANSPARENT); // Remove YT red tint. + } + } catch (Exception ex) { + Logger.printException(() -> "setNotificationIcon failure", ex); + } + } + + /** + * Injection point. + *

+ * 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. + *

+ * 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. + *

+ * 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; + } + + /** + * Injection point. + */ + @SuppressWarnings("ConstantConditions") + public static void setBranding() { + try { + if (GmsCoreSupport.isPackageNameOriginal()) { + Logger.printInfo(() -> "App is root mounted. Cannot dynamically change app icon"); + return; + } + + Context context = Utils.getContext(); + PackageManager pm = context.getPackageManager(); + String packageName = context.getPackageName(); + + BrandingTheme selectedBranding = BaseSettings.CUSTOM_BRANDING_ICON.get(); + final int selectedNameIndex = BaseSettings.CUSTOM_BRANDING_NAME.get(); + ComponentName componentToEnable = null; + ComponentName defaultComponent = null; + List componentsToDisable = new ArrayList<>(); + + for (BrandingTheme theme : BrandingTheme.values()) { + // Must always update all aliases including custom alias (last index). + final int numberOfPresetAppNames = numberOfPresetAppNames(); + + // App name indices starts at 1. + for (int index = 1; index <= numberOfPresetAppNames; index++) { + String aliasClass = theme.packageAndNameIndexToClassAlias(packageName, index); + ComponentName component = new ComponentName(packageName, aliasClass); + if (defaultComponent == null) { + // Default is always the first alias. + defaultComponent = component; + } + + if (index == selectedNameIndex && theme == selectedBranding) { + componentToEnable = component; + } else { + componentsToDisable.add(component); + } + } + } + + if (componentToEnable == null) { + // User imported a bad app name index value. Either the imported data + // was corrupted, or they previously had custom name enabled and the app + // no longer has a custom name specified. + Utils.showToastLong("Custom branding reset"); + BaseSettings.CUSTOM_BRANDING_ICON.resetToDefault(); + BaseSettings.CUSTOM_BRANDING_NAME.resetToDefault(); + + componentToEnable = defaultComponent; + componentsToDisable.remove(defaultComponent); + } + + for (ComponentName disable : componentsToDisable) { + pm.setComponentEnabledSetting(disable, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); + } + + // Use info logging because if the alias status become corrupt the app cannot launch. + ComponentName componentToEnableFinal = componentToEnable; + Logger.printInfo(() -> "Enabling: " + componentToEnableFinal.getClassName()); + + pm.setComponentEnabledSetting(componentToEnable, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0); + } catch (Exception ex) { + Logger.printException(() -> "setBranding failure", ex); + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java new file mode 100644 index 0000000000..b63f2c6049 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/EnableDebuggingPatch.java @@ -0,0 +1,138 @@ +package app.revanced.extension.shared.patches; + +import static java.lang.Boolean.TRUE; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.BaseSettings; + +@SuppressWarnings("unused") +public final class EnableDebuggingPatch { + + /** + * Only log if debugging is enabled on startup. + * This prevents enabling debugging + * while the app is running then failing to restart + * resulting in an incomplete log. + */ + private static final boolean LOG_FEATURE_FLAGS = BaseSettings.DEBUG.get(); + + private static final ConcurrentMap featureFlags = LOG_FEATURE_FLAGS + ? new ConcurrentHashMap<>(800, 0.5f, 1) + : null; + + private static final Set DISABLED_FEATURE_FLAGS = parseFlags(BaseSettings.DISABLED_FEATURE_FLAGS.get()); + + // Log all disabled flags on app startup. + static { + if (LOG_FEATURE_FLAGS && !DISABLED_FEATURE_FLAGS.isEmpty()) { + StringBuilder sb = new StringBuilder("Disabled feature flags:\n"); + for (Long flag : DISABLED_FEATURE_FLAGS) { + sb.append(" ").append(flag).append('\n'); + } + Logger.printDebug(sb::toString); + } + } + + /** + * Injection point. + */ + public static boolean isBooleanFeatureFlagEnabled(boolean value, long flag) { + if (LOG_FEATURE_FLAGS && value) { + Long flagObj = flag; + if (DISABLED_FEATURE_FLAGS.contains(flagObj)) { + return false; + } + if (featureFlags.putIfAbsent(flagObj, TRUE) == null) { + Logger.printDebug(() -> "boolean feature is enabled: " + flag); + } + } + + return value; + } + + /** + * Injection point. + */ + 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 + + " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue)); + } + } + + return value; + } + + /** + * Injection point. + */ + 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)); + } + } + + return value; + } + + /** + * Injection point. + */ + public static String isStringFeatureFlagEnabled(String value, long flag, String defaultValue) { + if (LOG_FEATURE_FLAGS && !defaultValue.equals(value)) { + if (featureFlags.putIfAbsent(flag, true) == null) { + Logger.printDebug(() -> " string feature is enabled: " + flag + + " value: " + value + (defaultValue.isEmpty() ? "" : " default: " + defaultValue)); + } + } + + return value; + } + + /** + * Get all logged feature flags. + * @return Set of all known flags + */ + public static Set getAllLoggedFlags() { + if (featureFlags != null) { + return new HashSet<>(featureFlags.keySet()); + } + + return new HashSet<>(); + } + + /** + * Public method for parsing flags. + * @param flags String containing newline-separated flag IDs + * @return Set of parsed flag IDs + */ + public static Set parseFlags(String flags) { + Set parsedFlags = new HashSet<>(); + if (!flags.isBlank()) { + for (String flag : flags.split("\n")) { + String trimmedFlag = flag.trim(); + if (trimmedFlag.isEmpty()) continue; // Skip empty lines. + try { + parsedFlags.add(Long.parseLong(trimmedFlag)); + } catch (NumberFormatException e) { + Logger.printException(() -> "Invalid flag ID: " + flag); + } + } + } + + return parsedFlags; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/ForceOriginalAudioPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/ForceOriginalAudioPatch.java new file mode 100644 index 0000000000..8ae454e69a --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/ForceOriginalAudioPatch.java @@ -0,0 +1,71 @@ +package app.revanced.extension.shared.patches; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.AppLanguage; +import app.revanced.extension.shared.spoof.ClientType; +import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch; + +@SuppressWarnings("unused") +public class ForceOriginalAudioPatch { + + private static final String DEFAULT_AUDIO_TRACKS_SUFFIX = ".4"; + + private static volatile boolean enabled; + + public static void setEnabled(boolean isEnabled, ClientType client) { + enabled = isEnabled; + + if (isEnabled && !client.useAuth && !client.supportsMultiAudioTracks) { + // If client spoofing does not use authentication and lacks multi-audio streams, + // then can use any language code for the request and if that requested language is + // not available YT uses the original audio language. Authenticated requests ignore + // the language code and always use the account language. Use a language that is + // not auto-dubbed by YouTube: https://support.google.com/youtube/answer/15569972 + // but the language is also supported natively by the Meta Quest device that + // Android VR is spoofing. + AppLanguage override = AppLanguage.NB; // Norwegian Bokmal. + Logger.printDebug(() -> "Setting language override: " + override); + SpoofVideoStreamsPatch.setLanguageOverride(override); + } + } + + /** + * Injection point. + */ + public static boolean ignoreDefaultAudioStream(boolean original) { + if (enabled) { + return false; + } + return original; + } + + /** + * Injection point. + */ + public static boolean isDefaultAudioStream(boolean isDefault, String audioTrackId, String audioTrackDisplayName) { + try { + if (!enabled) { + return isDefault; + } + + if (audioTrackId.isEmpty()) { + // Older app targets can have empty audio tracks and these might be placeholders. + // The real audio tracks are called after these. + return isDefault; + } + + Logger.printDebug(() -> "default: " + String.format("%-5s", isDefault) + " id: " + + String.format("%-8s", audioTrackId) + " name:" + audioTrackDisplayName); + + final boolean isOriginal = audioTrackId.endsWith(DEFAULT_AUDIO_TRACKS_SUFFIX); + if (isOriginal) { + Logger.printDebug(() -> "Using audio: " + audioTrackId); + } + + return isOriginal; + } catch (Exception ex) { + Logger.printException(() -> "isDefaultAudioStream failure", ex); + return isDefault; + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java new file mode 100644 index 0000000000..b0bcbc6f04 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/SanitizeSharingLinksPatch.java @@ -0,0 +1,31 @@ +package app.revanced.extension.shared.patches; + +import app.revanced.extension.shared.privacy.LinkSanitizer; +import app.revanced.extension.shared.settings.BaseSettings; + +/** + * YouTube and YouTube Music. + */ +@SuppressWarnings("unused") +public final class SanitizeSharingLinksPatch { + + private static final LinkSanitizer sanitizer = new LinkSanitizer( + "si", + "feature" // Old tracking parameter name, and may be obsolete. + ); + + /** + * Injection point. + */ + public static String sanitize(String url) { + if (BaseSettings.SANITIZE_SHARING_LINKS.get()) { + url = sanitizer.sanitizeURLString(url); + } + + if (BaseSettings.REPLACE_MUSIC_LINKS_WITH_YOUTUBE.get()) { + url = url.replace("music.youtube.com", "youtube.com"); + } + + return url; + } +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java similarity index 57% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java index 37062d6e28..beb623a799 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java @@ -1,9 +1,8 @@ -package app.revanced.extension.youtube.patches.components; +package app.revanced.extension.shared.patches.litho; import static app.revanced.extension.shared.StringRef.str; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import java.util.Arrays; import java.util.Collection; @@ -14,17 +13,19 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.StringTrieSearch; import app.revanced.extension.shared.Utils; -import app.revanced.extension.youtube.ByteTrieSearch; -import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.ByteTrieSearch; +import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; +import app.revanced.extension.shared.settings.YouTubeAndMusicSettings; /** * Allows custom filtering using a path and optionally a proto buffer string. */ @SuppressWarnings("unused") -final class CustomFilter extends Filter { +public final class CustomFilter extends Filter { - private static void showInvalidSyntaxToast(@NonNull String expression) { + private static void showInvalidSyntaxToast(String expression) { Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression)); } @@ -36,7 +37,12 @@ final class CustomFilter extends Filter { public static final String SYNTAX_STARTS_WITH = "^"; /** - * Optional character that separates the path from a proto buffer string pattern. + * Optional character that separates the path from an accessibility string pattern. + */ + public static final String SYNTAX_ACCESSIBILITY_SYMBOL = "#"; + + /** + * Optional character that separates the path/accessibility from a proto buffer string pattern. */ public static final String SYNTAX_BUFFER_SYMBOL = "$"; @@ -46,20 +52,26 @@ final class CustomFilter extends Filter { @NonNull @SuppressWarnings("ConstantConditions") static Collection parseCustomFilterGroups() { - String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get(); + String rawCustomFilterText = YouTubeAndMusicSettings.CUSTOM_FILTER_STRINGS.get(); if (rawCustomFilterText.isBlank()) { return Collections.emptyList(); } - // Map key is the path including optional special characters (^ and/or $) + // Map key is the full path including optional special characters (^, #, $), + // and any accessibility pattern, but does not contain any buffer patterns. Map result = new HashMap<>(); + Pattern pattern = Pattern.compile( - "(" // map key group - + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with - + "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path - + "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol - + ")" // end map key group - + "(.*)"); // optional buffer string + "(" // Map key group. + // Optional starts with. + + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" + // Path string. + + "([^\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + SYNTAX_BUFFER_SYMBOL + "\\E]*)" + // Optional accessibility string. + + "(?:\\Q" + SYNTAX_ACCESSIBILITY_SYMBOL + "\\E([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*))?" + // Optional buffer string. + + "(?:\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E(.*))?" + + ")"); // end map key group for (String expression : rawCustomFilterText.split("\n")) { if (expression.isBlank()) continue; @@ -73,10 +85,12 @@ final class CustomFilter extends Filter { final String mapKey = matcher.group(1); final boolean pathStartsWith = !matcher.group(2).isEmpty(); final String path = matcher.group(3); - final boolean hasBufferSymbol = !matcher.group(4).isEmpty(); - final String bufferString = matcher.group(5); + final String accessibility = matcher.group(4); // null if not present + final String buffer = matcher.group(5); // null if not present - if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) { + if (path.isBlank() + || (accessibility != null && accessibility.isEmpty()) + || (buffer != null && buffer.isEmpty())) { showInvalidSyntaxToast(expression); continue; } @@ -89,8 +103,13 @@ final class CustomFilter extends Filter { group = new CustomFilterGroup(pathStartsWith, path); result.put(mapKey, group); } - if (hasBufferSymbol) { - group.addBufferString(bufferString); + + if (accessibility != null) { + group.addAccessibilityString(accessibility); + } + + if (buffer != null) { + group.addBufferString(buffer); } } @@ -98,14 +117,22 @@ final class CustomFilter extends Filter { } final boolean startsWith; + StringTrieSearch accessibilitySearch; ByteTrieSearch bufferSearch; - CustomFilterGroup(boolean startsWith, @NonNull String path) { - super(Settings.CUSTOM_FILTER, path); + CustomFilterGroup(boolean startsWith, String path) { + super(YouTubeAndMusicSettings.CUSTOM_FILTER, path); this.startsWith = startsWith; } - void addBufferString(@NonNull String bufferString) { + void addAccessibilityString(String accessibilityString) { + if (accessibilitySearch == null) { + accessibilitySearch = new StringTrieSearch(); + } + accessibilitySearch.addPattern(accessibilityString); + } + + void addBufferString(String bufferString) { if (bufferSearch == null) { bufferSearch = new ByteTrieSearch(); } @@ -117,6 +144,11 @@ final class CustomFilter extends Filter { public String toString() { StringBuilder builder = new StringBuilder(); builder.append("CustomFilterGroup{"); + if (accessibilitySearch != null) { + builder.append(", accessibility="); + builder.append(accessibilitySearch.getPatterns()); + } + builder.append("path="); if (startsWith) builder.append(SYNTAX_STARTS_WITH); builder.append(filters[0]); @@ -146,16 +178,26 @@ final class CustomFilter extends Filter { } @Override - boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, - StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { // All callbacks are custom filter groups. CustomFilterGroup custom = (CustomFilterGroup) matchedGroup; + + // Check path start requirement. if (custom.startsWith && contentIndex != 0) { return false; } - if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) { + + // Check accessibility string if specified. + if (custom.accessibilitySearch != null && !custom.accessibilitySearch.matches(accessibility)) { return false; } - return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + + // Check buffer if specified. + if (custom.bufferSearch != null && !custom.bufferSearch.matches(buffer)) { + return false; + } + + return true; // All custom filter conditions passed. } -} \ No newline at end of file +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java similarity index 55% rename from extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java rename to extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java index 42b86d589f..b34ca9bdd7 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java @@ -1,33 +1,32 @@ -package app.revanced.extension.youtube.patches.components; +package app.revanced.extension.shared.patches.litho; -import androidx.annotation.Nullable; +import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import app.revanced.extension.shared.Logger; -import app.revanced.extension.shared.settings.BaseSettings; - /** * Filters litho based components. * * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} * and {@link #addPathCallbacks(StringFilterGroup...)}. * - * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to + * To filter {@link FilterContentType#PROTOBUFFER} or {@link FilterContentType#ACCESSIBILITY}, first add a callback to * either an identifier or a path. - * Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) - * or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern). + * or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern). * * All callbacks must be registered before the constructor completes. */ -abstract class Filter { +public abstract class Filter { public enum FilterContentType { IDENTIFIER, PATH, + ACCESSIBILITY, PROTOBUFFER } @@ -35,15 +34,15 @@ abstract class Filter { * Identifier callbacks. Do not add to this instance, * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}. */ - protected final List identifierCallbacks = new ArrayList<>(); + public final List identifierCallbacks = new ArrayList<>(); /** * Path callbacks. Do not add to this instance, * and instead use {@link #addPathCallbacks(StringFilterGroup...)}. */ - protected final List pathCallbacks = new ArrayList<>(); + public final List pathCallbacks = new ArrayList<>(); /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * if any of the groups are found. */ protected final void addIdentifierCallbacks(StringFilterGroup... groups) { @@ -51,7 +50,7 @@ abstract class Filter { } /** - * Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)} + * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} * if any of the groups are found. */ protected final void addPathCallbacks(StringFilterGroup... groups) { @@ -63,27 +62,18 @@ abstract class Filter { * Default implementation is to always filter the matched component and log the action. * Subclasses can perform additional or different checks if needed. *

- * If the content is to be filtered, subclasses should always - * call this method (and never return a plain 'true'). - * That way the logs will always show when a component was filtered and which filter hide it. - *

* Method is called off the main thread. * + * @param identifier Litho identifier. + * @param accessibility Accessibility string, or an empty string if not present for the component. + * @param buffer Protocol buffer. * @param matchedGroup The actual filter that matched. * @param contentType The type of content matched. * @param contentIndex Matched index of the identifier or path. * @return True if the litho component should be filtered out. */ - boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { - if (BaseSettings.DEBUG.get()) { - String filterSimpleName = getClass().getSimpleName(); - if (contentType == FilterContentType.IDENTIFIER) { - Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier); - } else { - Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path); - } - } return true; } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java new file mode 100644 index 0000000000..212787f305 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroup.java @@ -0,0 +1,213 @@ +package app.revanced.extension.shared.patches.litho; + +import androidx.annotation.NonNull; +import app.revanced.extension.shared.ByteTrieSearch; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.BooleanSetting; + +public abstract class FilterGroup { + 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 { + + 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 { + + 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); + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroupList.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroupList.java new file mode 100644 index 0000000000..da22ca9ff7 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/FilterGroupList.java @@ -0,0 +1,72 @@ +package app.revanced.extension.shared.patches.litho; + +import androidx.annotation.NonNull; +import app.revanced.extension.shared.ByteTrieSearch; +import app.revanced.extension.shared.StringTrieSearch; +import app.revanced.extension.shared.TrieSearch; +import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +public abstract class FilterGroupList> implements Iterable { + + private final List filterGroups = new ArrayList<>(); + private final TrieSearch search = createSearchGraph(); + + @SafeVarargs + public final void addAll(final T... groups) { + filterGroups.addAll(Arrays.asList(groups)); + + for (T group : groups) { + if (!group.includeInSearch()) { + continue; + } + for (V pattern : group.filters) { + search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + if (group.isEnabled()) { + FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; + result.setValues(group.setting, matchedStartIndex, matchedLength); + return true; + } + return false; + }); + } + } + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + public FilterGroup.FilterGroupResult check(V stack) { + FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); + search.matches(stack, result); + return result; + + } + + protected abstract TrieSearch createSearchGraph(); + + public static final class StringFilterGroupList extends FilterGroupList { + protected StringTrieSearch createSearchGraph() { + return new StringTrieSearch(); + } + } + + /** + * If searching for a single byte pattern, then it is slightly better to use + * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster + * than a prefix tree to search for only 1 pattern. + */ + public static final class ByteArrayFilterGroupList extends FilterGroupList { + protected ByteTrieSearch createSearchGraph() { + return new ByteTrieSearch(); + } + } +} \ No newline at end of file diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java new file mode 100644 index 0000000000..e1b329ee54 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/LithoFilterPatch.java @@ -0,0 +1,439 @@ +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. + *

+ * Unpatched YouTube uses a layout fixed thread pool between 1 and 3 threads: + *

+     * 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
+     * 
+ * + * 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. + *

+ * This is set during patching, do not change manually. + */ + 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 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> identifierToBufferThread = new ThreadLocal<>(); + + /** + * Global shared buffer. Used only if the buffer is not found in the ThreadLocal. + */ + private static final Map 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 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 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 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; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java new file mode 100644 index 0000000000..421761f7da --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/privacy/LinkSanitizer.java @@ -0,0 +1,68 @@ +package app.revanced.extension.shared.privacy; + +import android.net.Uri; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import app.revanced.extension.shared.Logger; + +/** + * Strips away specific parameters from URLs. + */ +public class LinkSanitizer { + + private final Collection parametersToRemove; + + public LinkSanitizer(String ... parametersToRemove) { + final int parameterCount = parametersToRemove.length; + + // List is faster if only checking a few parameters. + this.parametersToRemove = parameterCount > 4 + ? Set.of(parametersToRemove) + : List.of(parametersToRemove); + } + + public String sanitizeURLString(String url) { + try { + return sanitizeURI(Uri.parse(url)).toString(); + } catch (Exception ex) { + Logger.printException(() -> "sanitizeURLString failure: " + url, ex); + return url; + } + } + + 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); + return uri; + } + + Uri.Builder builder = uri.buildUpon().clearQuery(); + + if (!parametersToRemove.isEmpty()) { + for (String paramName : uri.getQueryParameterNames()) { + if (!parametersToRemove.contains(paramName)) { + for (String value : uri.getQueryParameters(paramName)) { + builder.appendQueryParameter(paramName, value); + } + } + } + } + + Uri sanitizedURL = builder.build(); + Logger.printInfo(() -> "Sanitized URL: " + uri + " to: " + sanitizedURL); + + return sanitizedURL; + } catch (Exception ex) { + Logger.printException(() -> "sanitizeURI failure: " + uri, ex); + return uri; + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java index c25e71d78c..2e5c457f7b 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java @@ -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(); - // Request data is in the URL parameters and no body is sent. - // The calling code must set a length if using a request body. + // 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. connection.setFixedLengthStreamingMode(0); connection.setRequestMethod(route.getMethod().name()); String agentString = System.getProperty("http.agent") diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java index 9e6f2c5a71..74428224a7 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java @@ -52,7 +52,7 @@ public class Route { private int countMatches(CharSequence seq, char c) { int count = 0; - for (int i = 0; i < seq.length(); i++) { + for (int i = 0, length = seq.length(); i < length; i++) { if (seq.charAt(i) == c) count++; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java new file mode 100644 index 0000000000..fbc734a51d --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/AppLanguage.java @@ -0,0 +1,119 @@ +package app.revanced.extension.shared.settings; + +import java.util.Locale; + +public enum AppLanguage { + /** + * The current app language. + */ + DEFAULT, + + // Languages codes not included with YouTube, but are translated on Crowdin + GA, + + // Language codes found in locale_config.xml + // All region specific variants have been removed. + AF, + AM, + AR, + AS, + AZ, + BE, + BG, + BN, + BS, + CA, + CS, + DA, + DE, + EL, + EN, + ES, + ET, + EU, + FA, + FI, + FR, + GL, + GU, + HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code. + HI, + HR, + HU, + HY, + ID, + IS, + IT, + JA, + KA, + KK, + KM, + KN, + KO, + KY, + LO, + LT, + LV, + MK, + ML, + MN, + MR, + MS, + MY, + NB, + NE, + NL, + OR, + PA, + PL, + PT, + RO, + RU, + SI, + SK, + SL, + SQ, + SR, + SV, + SW, + TA, + TE, + TH, + TL, + TR, + UK, + UR, + UZ, + VI, + ZH, + ZU; + + private final String language; + private final Locale locale; + + AppLanguage() { + language = name().toLowerCase(Locale.US); + locale = Locale.forLanguageTag(language); + } + + /** + * @return The 2 letter ISO 639_1 language code. + */ + public String getLanguage() { + // Changing the app language does not force the app to completely restart, + // so the default needs to be the current language and not a static field. + if (this == DEFAULT) { + return Locale.getDefault().getLanguage(); + } + + return language; + } + + public Locale getLocale() { + if (this == DEFAULT) { + return Locale.getDefault(); + } + + return locale; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java new file mode 100644 index 0000000000..1a2bfe9a2b --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseActivityHook.java @@ -0,0 +1,173 @@ +package app.revanced.extension.shared.settings; + +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; + +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; + +/** + * 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) +public abstract class BaseActivityHook extends Activity { + + private static final int ID_REVANCED_SETTINGS_FRAGMENTS = + getResourceIdentifierOrThrow(ResourceType.ID, "revanced_settings_fragments"); + private static final int ID_REVANCED_TOOLBAR_PARENT = + getResourceIdentifierOrThrow(ResourceType.ID, "revanced_toolbar_parent"); + public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR = + getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_settings_with_toolbar"); + private static final int STRING_REVANCED_SETTINGS_TITLE = + getResourceIdentifierOrThrow(ResourceType.STRING, "revanced_settings_title"); + + /** + * Layout parameters for the toolbar, extracted from the dummy toolbar. + */ + protected static ViewGroup.LayoutParams toolbarLayoutParams; + + /** + * Sets the layout parameters for the toolbar. + */ + public static void setToolbarLayoutParams(Toolbar toolbar) { + if (toolbarLayoutParams != null) { + toolbar.setLayoutParams(toolbarLayoutParams); + } + } + + /** + * Initializes the activity by setting the theme, content view and injecting a PreferenceFragment. + */ + public static void initialize(BaseActivityHook hook, Activity activity) { + try { + hook.customizeActivityTheme(activity); + activity.setContentView(hook.getContentViewResourceId()); + + // Sanity check. + String dataString = activity.getIntent().getDataString(); + if (!"revanced_settings_intent".equals(dataString)) { + Logger.printException(() -> "Unknown intent: " + dataString); + return; + } + + PreferenceFragment fragment = hook.createPreferenceFragment(); + hook.createToolbar(activity, fragment); + + activity.getFragmentManager() + .beginTransaction() + .replace(ID_REVANCED_SETTINGS_FRAGMENTS, fragment) + .commit(); + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + * Overrides the ReVanced settings language. + */ + @SuppressWarnings("unused") + public static Context getAttachBaseContext(Context original) { + AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get(); + if (language == AppLanguage.DEFAULT) { + return original; + } + + return Utils.getContext(); + } + + /** + * Creates and configures a toolbar for the activity, replacing a dummy placeholder. + */ + @SuppressLint("UseCompatLoadingForDrawables") + 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"); + toolbarLayoutParams = dummyToolbar.getLayoutParams(); + toolbarParent.removeView(dummyToolbar); + + // Sets appropriate system navigation bar color for the activity. + ToolbarPreferenceFragment.setNavigationBarColor(activity.getWindow()); + + Toolbar toolbar = new Toolbar(toolbarParent.getContext()); + toolbar.setBackgroundColor(getToolbarBackgroundColor()); + toolbar.setNavigationIcon(getNavigationIcon()); + toolbar.setNavigationOnClickListener(getNavigationClickListener(activity)); + toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE); + + toolbar.setTitleMarginStart(Dim.dp16); + toolbar.setTitleMarginEnd(Dim.dp16); + TextView toolbarTextView = Utils.getChildView(toolbar, false, view -> view instanceof TextView); + if (toolbarTextView != null) { + toolbarTextView.setTextColor(Utils.getAppForegroundColor()); + toolbarTextView.setTextSize(20); + } + setToolbarLayoutParams(toolbar); + + 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; + } + + /** + * Customizes the activity's theme. + */ + protected abstract void customizeActivityTheme(Activity activity); + + /** + * Returns the background color for the toolbar. + */ + protected abstract int getToolbarBackgroundColor(); + + /** + * Returns the navigation icon drawable for the toolbar. + */ + protected abstract Drawable getNavigationIcon(); + + /** + * Returns the click listener for the toolbar's navigation icon. + */ + protected abstract View.OnClickListener getNavigationClickListener(Activity activity); + + /** + * Creates the PreferenceFragment to be injected into the activity. + */ + protected PreferenceFragment createPreferenceFragment() { + return new ToolbarPreferenceFragment(); + } + + /** + * Performs additional setup after the toolbar is configured. + * + * @param activity The activity hosting the toolbar. + * @param toolbar The configured toolbar. + * @param fragment The PreferenceFragment associated with the activity. + */ + protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {} +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index 13be9547ca..5fc4418366 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -2,11 +2,15 @@ package app.revanced.extension.shared.settings; import static java.lang.Boolean.FALSE; 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. - * + *

* To ensure this class is loaded when the UI is created, app specific setting bundles should extend * or reference this class. */ @@ -16,4 +20,51 @@ public class BaseSettings { public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message"); public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false); + + public static final EnumSetting REVANCED_LANGUAGE = new EnumSetting<>("revanced_language", AppLanguage.DEFAULT, true, "revanced_language_user_dialog_message"); + + /** + * 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 SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE); + public static final BooleanSetting REPLACE_MUSIC_LINKS_WITH_YOUTUBE = new BooleanSetting("revanced_replace_music_with_youtube", FALSE); + + public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false); + + public static final EnumSetting 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 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); + } + } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java index 7e84034d06..c67ebabf96 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java @@ -43,10 +43,14 @@ public class BooleanSetting extends Setting { * This method is only to be used by the Settings preference code. * * This intentionally is a static method to deter - * accidental usage when {@link #save(Boolean)} was intnded. + * accidental usage when {@link #save(Boolean)} was intended. */ public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) { setting.value = Objects.requireNonNull(newValue); + + if (setting.isSetToDefault()) { + setting.removeFromPreferences(); + } } @Override @@ -65,10 +69,8 @@ public class BooleanSetting extends Setting { } @Override - public void save(@NonNull Boolean newValue) { - // Must set before saving to preferences (otherwise importing fails to update UI correctly). - value = Objects.requireNonNull(newValue); - preferences.saveBoolean(key, newValue); + public void saveToPreferences() { + preferences.saveBoolean(key, value); } @NonNull diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java index a2b82dd215..2c2cb6a3a8 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java @@ -71,15 +71,20 @@ public class EnumSetting> extends Setting { json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH)); } - @NonNull - private T getEnumFromString(String enumName) { + /** + * @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. + */ + protected T getEnumFromString(String enumName) { //noinspection ConstantConditions for (Enum value : defaultValue.getClass().getEnumConstants()) { if (value.name().equalsIgnoreCase(enumName)) { - // noinspection unchecked + //noinspection unchecked return (T) value; } } + throw new IllegalArgumentException("Unknown enum value: " + enumName); } @@ -89,10 +94,8 @@ public class EnumSetting> extends Setting { } @Override - public void save(@NonNull T newValue) { - // Must set before saving to preferences (otherwise importing fails to update UI correctly). - value = Objects.requireNonNull(newValue); - preferences.saveEnumAsString(key, newValue); + public void saveToPreferences() { + preferences.saveEnumAsString(key, value); } @NonNull @@ -105,7 +108,9 @@ public class EnumSetting> extends Setting { * Availability based on if this setting is currently set to any of the provided types. */ @SafeVarargs - public final Setting.Availability availability(@NonNull T... types) { + public final Setting.Availability availability(T... types) { + Objects.requireNonNull(types); + return () -> { T currentEnumType = get(); for (T enumType : types) { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java index 7419741e03..59846e037f 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java @@ -55,10 +55,8 @@ public class FloatSetting extends Setting { } @Override - public void save(@NonNull Float newValue) { - // Must set before saving to preferences (otherwise importing fails to update UI correctly). - value = Objects.requireNonNull(newValue); - preferences.saveFloatString(key, newValue); + public void saveToPreferences() { + preferences.saveFloatString(key, value); } @NonNull diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java index 58f39a9107..ccf128dfdd 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java @@ -55,10 +55,8 @@ public class IntegerSetting extends Setting { } @Override - public void save(@NonNull Integer newValue) { - // Must set before saving to preferences (otherwise importing fails to update UI correctly). - value = Objects.requireNonNull(newValue); - preferences.saveIntegerString(key, newValue); + public void saveToPreferences() { + preferences.saveIntegerString(key, value); } @NonNull diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java index 4d7f8114f2..ea3adcebac 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java @@ -55,10 +55,8 @@ public class LongSetting extends Setting { } @Override - public void save(@NonNull Long newValue) { - // Must set before saving to preferences (otherwise importing fails to update UI correctly). - value = Objects.requireNonNull(newValue); - preferences.saveLongString(key, newValue); + public void saveToPreferences() { + preferences.saveLongString(key, value); } @NonNull diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java index db5ecc844b..53a980e3c2 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -1,21 +1,28 @@ package app.revanced.extension.shared.settings; +import static app.revanced.extension.shared.StringRef.str; + import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.StringRef; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.settings.preference.SharedPrefCategory; -import org.jetbrains.annotations.NotNull; -import org.json.JSONException; -import org.json.JSONObject; -import java.util.*; - -import static app.revanced.extension.shared.StringRef.str; - -@SuppressWarnings("unused") public abstract class Setting { /** @@ -25,39 +32,86 @@ public abstract class Setting { */ public interface Availability { boolean isAvailable(); + + /** + * @return parent settings (dependencies) of this availability. + */ + default List> getParentSettings() { + return Collections.emptyList(); + } } /** * Availability based on a single parent setting being enabled. */ - @NonNull - public static Availability parent(@NonNull BooleanSetting parent) { - return parent::get; + public static Availability parent(BooleanSetting parent) { + return new Availability() { + @Override + public boolean isAvailable() { + return parent.get(); + } + + @Override + public List> getParentSettings() { + return Collections.singletonList(parent); + } + }; + } + + /** + * Availability based on a single parent setting being disabled. + */ + public static Availability parentNot(BooleanSetting parent) { + return new Availability() { + @Override + public boolean isAvailable() { + return !parent.get(); + } + + @Override + public List> getParentSettings() { + return Collections.singletonList(parent); + } + }; } /** * Availability based on all parents being enabled. */ - @NonNull - public static Availability parentsAll(@NonNull BooleanSetting... parents) { - return () -> { - for (BooleanSetting parent : parents) { - if (!parent.get()) return false; + public static Availability parentsAll(BooleanSetting... parents) { + return new Availability() { + @Override + public boolean isAvailable() { + for (BooleanSetting parent : parents) { + if (!parent.get()) return false; + } + return true; + } + + @Override + public List> getParentSettings() { + return Collections.unmodifiableList(Arrays.asList(parents)); } - return true; }; } /** * Availability based on any parent being enabled. */ - @NonNull - public static Availability parentsAny(@NonNull BooleanSetting... parents) { - return () -> { - for (BooleanSetting parent : parents) { - if (parent.get()) return true; + public static Availability parentsAny(BooleanSetting... parents) { + return new Availability() { + @Override + public boolean isAvailable() { + for (BooleanSetting parent : parents) { + if (parent.get()) return true; + } + return false; + } + + @Override + public List> getParentSettings() { + return Collections.unmodifiableList(Arrays.asList(parents)); } - return false; }; } @@ -81,7 +135,7 @@ public abstract class Setting { /** * Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}. */ - public static void addImportExportCallback(@NonNull ImportExportCallback callback) { + public static void addImportExportCallback(ImportExportCallback callback) { importExportCallbacks.add(Objects.requireNonNull(callback)); } @@ -102,14 +156,13 @@ public abstract class Setting { public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs"); @Nullable - public static Setting getSettingFromPath(@NonNull String str) { + public static Setting getSettingFromPath(String str) { return PATH_TO_SETTINGS.get(str); } /** * @return All settings that have been created. */ - @NonNull public static List> allLoadedSettings() { return Collections.unmodifiableList(SETTINGS); } @@ -117,8 +170,8 @@ public abstract class Setting { /** * @return All settings that have been created, sorted by keys. */ - @NonNull private static List> allLoadedSettingsSorted() { + //noinspection ComparatorCombinators Collections.sort(SETTINGS, (Setting o1, Setting o2) -> o1.key.compareTo(o2.key)); return allLoadedSettings(); } @@ -126,13 +179,11 @@ public abstract class Setting { /** * The key used to store the value in the shared preferences. */ - @NonNull public final String key; /** * The default value of the setting. */ - @NonNull public final T defaultValue; /** @@ -154,7 +205,6 @@ public abstract class Setting { /** * Confirmation message to display, if the user tries to change the setting from the default value. - * Currently this works only for Boolean setting types. */ @Nullable public final StringRef userDialogMessage; @@ -164,7 +214,6 @@ public abstract class Setting { /** * The value of the setting. */ - @NonNull protected volatile T value; public Setting(String key, T defaultValue) { @@ -202,8 +251,8 @@ public abstract class Setting { * @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value. * @param availability Condition that must be true, for this setting to be available to configure. */ - public Setting(@NonNull String key, - @NonNull T defaultValue, + public Setting(String key, + T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @@ -218,83 +267,35 @@ public abstract class Setting { SETTINGS.add(this); if (PATH_TO_SETTINGS.put(key, this) != null) { - // Debug setting may not be created yet so using Logger may cause an initialization crash. - // Show a toast instead. - Utils.showToastLong(this.getClass().getSimpleName() + Logger.printException(() -> this.getClass().getSimpleName() + " error: Duplicate Setting key found: " + key); } load(); } - /** - * Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. - */ - public static void migrateOldSettingToNew(@NonNull Setting oldSetting, @NonNull Setting newSetting) { - if (oldSetting == newSetting) throw new IllegalArgumentException(); - - if (!oldSetting.isSetToDefault()) { - Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting); - newSetting.save(oldSetting.value); - oldSetting.resetToDefault(); - } - } - - /** - * Migrate an old Setting value previously stored in a different SharedPreference. - * - * This method will be deleted in the future. - */ - public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) { - if (!oldPrefs.preferences.contains(settingKey)) { - return; // Nothing to do. - } - - Object newValue = setting.get(); - final Object migratedValue; - if (setting instanceof BooleanSetting) { - migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue); - } else if (setting instanceof IntegerSetting) { - migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue); - } else if (setting instanceof LongSetting) { - migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue); - } else if (setting instanceof FloatSetting) { - migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue); - } else if (setting instanceof StringSetting) { - migratedValue = oldPrefs.getString(settingKey, (String) newValue); - } else { - Logger.printException(() -> "Unknown setting: " + setting); - // Remove otherwise it'll show a toast on every launch - oldPrefs.preferences.edit().remove(settingKey).apply(); - return; - } - - oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting. - if (migratedValue.equals(newValue)) { - Logger.printDebug(() -> "Value does not need migrating: " + settingKey); - return; // Old value is already equal to the new setting value. - } - - Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey); - //noinspection unchecked - setting.save(migratedValue); - } - /** * Sets, but does _not_ persistently save the value. * This method is only to be used by the Settings preference code. - * + *

* This intentionally is a static method to deter * accidental usage when {@link #save(Object)} was intended. */ - public static void privateSetValueFromString(@NonNull Setting setting, @NonNull String newValue) { + public static void privateSetValueFromString(Setting setting, String newValue) { setting.setValueFromString(newValue); + + // Clear the preference value since default is used, to allow changing + // the changing the default for a future release. Without this after upgrading + // the saved value will be whatever was the default when the app was first installed. + if (setting.isSetToDefault()) { + setting.removeFromPreferences(); + } } /** * Sets the value of {@link #value}, but do not save to {@link #preferences}. */ - protected abstract void setValueFromString(@NonNull String newValue); + protected abstract void setValueFromString(String newValue); /** * Load and set the value of {@link #value}. @@ -304,16 +305,45 @@ public abstract class Setting { /** * Persistently saves the value. */ - public abstract void save(@NonNull T newValue); + public final void save(T newValue) { + if (value.equals(newValue)) { + return; + } + + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + + if (defaultValue.equals(newValue)) { + removeFromPreferences(); + } else { + saveToPreferences(); + } + } + + /** + * Save {@link #value} to {@link #preferences}. + */ + protected abstract void saveToPreferences(); + + /** + * Remove {@link #value} from {@link #preferences}. + */ + protected final void removeFromPreferences() { + Logger.printDebug(() -> "Clearing stored preference value (reset to default): " + key); + preferences.removeKey(key); + } @NonNull public abstract T get(); /** * Identical to calling {@link #save(Object)} using {@link #defaultValue}. + * + * @return The newly saved default value. */ - public void resetToDefault() { + public T resetToDefault() { save(defaultValue); + return defaultValue; } /** @@ -324,13 +354,24 @@ public abstract class Setting { } /** - * @return if the currently set value is the same as {@link #defaultValue} + * Get the parent Settings that this setting depends on. + * @return List of parent Settings, or empty list if no dependencies exist. + * Defensive: handles null availability or missing getParentSettings() override. + */ + public List> getParentSettings() { + return availability == null + ? Collections.emptyList() + : Objects.requireNonNullElse(availability.getParentSettings(), Collections.emptyList()); + } + + /** + * @return if the currently set value is the same as {@link #defaultValue}. */ public boolean isSetToDefault() { return value.equals(defaultValue); } - @NotNull + @NonNull @Override public String toString() { return key + "=" + get(); @@ -355,7 +396,7 @@ public abstract class Setting { /** * @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; @@ -372,7 +413,6 @@ public abstract class Setting { json.put(importExportKey, value); } - @NonNull public static String exportToJson(@Nullable Context alertDialogContext) { try { JSONObject json = new JSONObject(); @@ -411,7 +451,7 @@ public abstract class Setting { /** * @return if any settings that require a reboot were changed. */ - public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) { + public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) { try { if (!settingsJsonString.matches("[\\s\\S]*\\{")) { settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces @@ -420,6 +460,7 @@ public abstract class Setting { boolean rebootSettingChanged = false; int numberOfSettingsImported = 0; + //noinspection rawtypes for (Setting setting : SETTINGS) { String key = setting.getImportExportKey(); if (json.has(key)) { @@ -441,9 +482,12 @@ public abstract class Setting { callback.settingsImported(alertDialogContext); } - Utils.showToastLong(numberOfSettingsImported == 0 - ? str("revanced_settings_import_reset") - : str("revanced_settings_import_success", numberOfSettingsImported)); + // Use a delay, otherwise the toast can move about on screen from the dismissing dialog. + final int numberOfSettingsImportedFinal = numberOfSettingsImported; + Utils.runOnMainThreadDelayed(() -> Utils.showToastLong(numberOfSettingsImportedFinal == 0 + ? str("revanced_settings_import_reset") + : str("revanced_settings_import_success", numberOfSettingsImportedFinal)), + 150); return rebootSettingChanged; } catch (JSONException | IllegalArgumentException ex) { diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java index 0fa5e03fc1..adb9beaa18 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java @@ -55,10 +55,8 @@ public class StringSetting extends Setting { } @Override - public void save(@NonNull String newValue) { - // Must set before saving to preferences (otherwise importing fails to update UI correctly). - value = Objects.requireNonNull(newValue); - preferences.saveString(key, newValue); + public void saveToPreferences() { + preferences.saveString(key, value); } @NonNull diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java new file mode 100644 index 0000000000..221ce00456 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java @@ -0,0 +1,14 @@ +package app.revanced.extension.shared.settings; + +import static app.revanced.extension.shared.settings.Setting.parent; +import static java.lang.Boolean.FALSE; + +public class YouTubeAndMusicSettings extends BaseSettings { + // Custom filter + public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE); + public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER)); + + // Miscellaneous + public static final BooleanSetting DEBUG_PROTOCOLBUFFER = new BooleanSetting("revanced_debug_protocolbuffer", FALSE, false, + "revanced_debug_protocolbuffer_user_dialog_message", parent(BaseSettings.DEBUG)); +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java index 902b95897e..a515471a00 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -3,25 +3,36 @@ package app.revanced.extension.shared.settings.preference; import static app.revanced.extension.shared.StringRef.str; import android.annotation.SuppressLint; -import android.app.AlertDialog; +import android.app.Dialog; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.*; - +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.util.Pair; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; 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; import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.ui.CustomDialog; @SuppressWarnings("deprecation") public abstract class AbstractPreferenceFragment extends PreferenceFragment { + /** * Indicates that if a preference changes, * to apply the change from the Setting to the UI component. @@ -29,20 +40,30 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { public static boolean settingImportInProgress; /** - * Confirm and restart dialog button text and title. - * Set by subclasses if Strings cannot be added as a resource. + * Prevents recursive calls during preference <-> UI syncing from showing extra dialogs. */ - @Nullable - protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle; + private static boolean updatingPreference; /** * Used to prevent showing reboot dialog, if user cancels a setting user dialog. */ - private boolean showingUserDialogMessage; + private static boolean showingUserDialogMessage; + + /** + * Confirm and restart dialog button text and title. + * Set by subclasses if Strings cannot be added as a resource. + */ + @Nullable + protected static CharSequence restartDialogTitle, restartDialogMessage, restartDialogButtonText, confirmDialogTitle; private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { - Setting setting = Setting.getSettingFromPath(str); + if (updatingPreference) { + Logger.printDebug(() -> "Ignoring preference change as sync is in progress"); + return; + } + + Setting setting = Setting.getSettingFromPath(Objects.requireNonNull(str)); if (setting == null) { return; } @@ -52,29 +73,29 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { } Logger.printDebug(() -> "Preference changed: " + setting.key); - // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. - updatePreference(pref, setting, true, settingImportInProgress); - // Update any other preference availability that may now be different. - updateUIAvailability(); - - if (settingImportInProgress) { - return; - } - - if (!showingUserDialogMessage) { - if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { - showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting); + if (!settingImportInProgress && !showingUserDialogMessage) { + if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) { + // Do not change the setting yet, to allow preserving whatever + // list/text value was previously set if it needs to be reverted. + showSettingUserDialogConfirmation(pref, setting); + return; } else if (setting.rebootApp) { showRestartDialog(getContext()); } } + updatingPreference = true; + // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. + // Updating here can cause a recursive call back into this same method. + updatePreference(pref, setting, true, settingImportInProgress); + // Update any other preference availability that may now be different. + updateUIAvailability(); + updatingPreference = false; } catch (Exception ex) { Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); } }; - /** * Initialize this instance, and do any custom behavior. *

@@ -83,7 +104,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { * so all app specific {@link Setting} instances are loaded before this method returns. */ protected void initialize() { - final var identifier = Utils.getResourceIdentifier("revanced_prefs", "xml"); + 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); if (identifier == 0) return; addPreferencesFromResource(identifier); @@ -92,37 +122,57 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { Utils.setPreferenceTitlesToMultiLineIfNeeded(screen); } - private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { + private void showSettingUserDialogConfirmation(Preference pref, Setting setting) { Utils.verifyOnMainThread(); final var context = getContext(); if (confirmDialogTitle == null) { confirmDialogTitle = str("revanced_settings_confirm_user_dialog_title"); } + showingUserDialogMessage = true; - new AlertDialog.Builder(context) - .setTitle(confirmDialogTitle) - .setMessage(Objects.requireNonNull(setting.userDialogMessage).toString()) - .setPositiveButton(android.R.string.ok, (dialog, id) -> { + + CharSequence message = BulletPointPreference.formatIntoBulletPoints( + Objects.requireNonNull(setting.userDialogMessage).toString()); + + Pair dialogPair = CustomDialog.create( + context, + confirmDialogTitle, // Title. + message, + null, // No EditText. + null, // OK button text. + () -> { + // OK button action. User confirmed, save to the Setting. + updatePreference(pref, setting, true, false); + + // Update availability of other preferences that may be changed. + updateUIAvailability(); + if (setting.rebootApp) { showRestartDialog(context); } - }) - .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value. - }) - .setOnDismissListener(dialog -> { - showingUserDialogMessage = false; - }) - .setCancelable(false) - .show(); + }, + () -> { + // Cancel button action. Restore whatever the setting was before the change. + updatePreference(pref, setting, true, true); + }, + null, // No Neutral button. + null, // No Neutral button action. + true // Dismiss dialog when onNeutralClick. + ); + + dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false); + dialogPair.first.setCancelable(false); + + // Show the dialog. + dialogPair.first.show(); } /** * Updates all Preferences values and their availability using the current values in {@link Setting}. */ protected void updateUIToSettingValues() { - updatePreferenceScreen(getPreferenceScreen(), true,true); + updatePreferenceScreen(getPreferenceScreen(), true, true); } /** @@ -132,19 +182,39 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { updatePreferenceScreen(getPreferenceScreen(), false, false); } + /** + * @return If the preference is currently set to the default value of the Setting. + */ + protected boolean prefIsSetToDefault(Preference pref, Setting setting) { + Object defaultValue = setting.defaultValue; + if (pref instanceof SwitchPreference switchPref) { + return switchPref.isChecked() == (Boolean) defaultValue; + } + String defaultValueString = defaultValue.toString(); + if (pref instanceof EditTextPreference editPreference) { + return editPreference.getText().equals(defaultValueString); + } + if (pref instanceof ListPreference listPref) { + return listPref.getValue().equals(defaultValueString); + } + + throw new IllegalStateException("Must override method to handle " + + "preference type: " + pref.getClass()); + } + /** * Syncs all UI Preferences to any {@link Setting} they represent. */ - private void updatePreferenceScreen(@NonNull PreferenceScreen screen, + private void updatePreferenceScreen(@NonNull PreferenceGroup group, boolean syncSettingValue, boolean applySettingToPreference) { - // Alternatively this could iterate thru all Settings and check for any matching Preferences, + // Alternatively this could iterate through all Settings and check for any matching Preferences, // but there are many more Settings than UI preferences so it's more efficient to only check // the Preferences. - for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) { - Preference pref = screen.getPreference(i); - if (pref instanceof PreferenceScreen) { - updatePreferenceScreen((PreferenceScreen) pref, syncSettingValue, applySettingToPreference); + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference pref = group.getPreference(i); + if (pref instanceof PreferenceGroup subGroup) { + updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference); } else if (pref.hasKey()) { String key = pref.getKey(); Setting setting = Setting.getSettingFromPath(key); @@ -170,30 +240,28 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { protected void syncSettingWithPreference(@NonNull Preference pref, @NonNull Setting setting, boolean applySettingToPreference) { - if (pref instanceof SwitchPreference) { - SwitchPreference switchPref = (SwitchPreference) pref; + if (pref instanceof SwitchPreference switchPref) { BooleanSetting boolSetting = (BooleanSetting) setting; if (applySettingToPreference) { switchPref.setChecked(boolSetting.get()); } else { BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked()); } - } else if (pref instanceof EditTextPreference) { - EditTextPreference editPreference = (EditTextPreference) pref; + } else if (pref instanceof EditTextPreference editPreference) { if (applySettingToPreference) { editPreference.setText(setting.get().toString()); } else { Setting.privateSetValueFromString(setting, editPreference.getText()); } - } else if (pref instanceof ListPreference) { - ListPreference listPref = (ListPreference) pref; + } else if (pref instanceof ListPreference listPref) { if (applySettingToPreference) { listPref.setValue(setting.get().toString()); } else { Setting.privateSetValueFromString(setting, listPref.getValue()); } updateListPreferenceSummary(listPref, setting); - } else { + } else if (!pref.getClass().equals(Preference.class)) { + // Ignore root preference class because there is no data to sync. Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref); } } @@ -235,21 +303,33 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { } } - public static void showRestartDialog(@NonNull final Context context) { + public static void showRestartDialog(Context context) { Utils.verifyOnMainThread(); if (restartDialogTitle == null) { restartDialogTitle = str("revanced_settings_restart_title"); } + if (restartDialogMessage == null) { + restartDialogMessage = str("revanced_settings_restart_dialog_message"); + } if (restartDialogButtonText == null) { restartDialogButtonText = str("revanced_settings_restart"); } - new AlertDialog.Builder(context) - .setMessage(restartDialogTitle) - .setPositiveButton(restartDialogButtonText, (dialog, id) - -> Utils.restartApp(context)) - .setNegativeButton(android.R.string.cancel, null) - .setCancelable(false) - .show(); + + Pair dialogPair = CustomDialog.create( + context, + restartDialogTitle, // Title. + restartDialogMessage, // Message. + null, // No EditText. + restartDialogButtonText, // OK button text. + () -> Utils.restartApp(context), // OK button action. + () -> {}, // Cancel button action (dismiss only). + null, // No Neutral button text. + null, // No Neutral button action. + true // Dismiss dialog when onNeutralClick. + ); + + // Show the dialog. + dialogPair.first.show(); } @SuppressLint("ResourceType") diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java new file mode 100644 index 0000000000..ee3f02fc8c --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointPreference.java @@ -0,0 +1,86 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.preference.Preference; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.TextUtils; +import android.text.style.BulletSpan; +import android.util.AttributeSet; + +/** + * Formats the summary text bullet points into Spanned text for better presentation. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class BulletPointPreference extends Preference { + + /** + * Replaces bullet points with styled spans. + */ + public static CharSequence formatIntoBulletPoints(CharSequence source) { + final char bulletPoint = '•'; + if (TextUtils.indexOf(source, bulletPoint) < 0) { + return source; // Nothing to do. + } + + SpannableStringBuilder builder = new SpannableStringBuilder(source); + + int lineStart = 0; + int length = builder.length(); + + while (lineStart < length) { + int lineEnd = TextUtils.indexOf(builder, '\n', lineStart); + if (lineEnd < 0) lineEnd = length; + + // Apply BulletSpan only if the line starts with the '•' character. + if (lineEnd > lineStart && builder.charAt(lineStart) == bulletPoint) { + int deleteEnd = lineStart + 1; // remove the bullet itself + + // If there's a single space right after the bullet, remove that too. + if (deleteEnd < builder.length() && builder.charAt(deleteEnd) == ' ') { + deleteEnd++; + } + + builder.delete(lineStart, deleteEnd); + + // Apply the BulletSpan to the remainder of that line. + builder.setSpan(new BulletSpan(20), + lineStart, + lineEnd - (deleteEnd - lineStart), // adjust for deleted chars. + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); + + // Update total length and lineEnd after deletion. + length = builder.length(); + final int removed = deleteEnd - lineStart; + lineEnd -= removed; + } + + lineStart = lineEnd + 1; + } + + return new SpannedString(builder); + } + + public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public BulletPointPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BulletPointPreference(Context context) { + super(context); + } + + @Override + public void setSummary(CharSequence summary) { + super.setSummary(formatIntoBulletPoints(summary)); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java new file mode 100644 index 0000000000..ccbbf1eef9 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/BulletPointSwitchPreference.java @@ -0,0 +1,45 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.settings.preference.BulletPointPreference.formatIntoBulletPoints; + +import android.content.Context; +import android.preference.SwitchPreference; +import android.util.AttributeSet; + +/** + * Formats the summary text bullet points into Spanned text for better presentation. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class BulletPointSwitchPreference extends SwitchPreference { + + public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public BulletPointSwitchPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public BulletPointSwitchPreference(Context context) { + super(context); + } + + @Override + public void setSummary(CharSequence summary) { + super.setSummary(formatIntoBulletPoints(summary)); + } + + @Override + public void setSummaryOn(CharSequence summaryOn) { + super.setSummaryOn(formatIntoBulletPoints(summaryOn)); + } + + @Override + public void setSummaryOff(CharSequence summaryOff) { + super.setSummaryOff(formatIntoBulletPoints(summaryOff)); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java new file mode 100644 index 0000000000..7dbf0dd387 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ClearLogBufferPreference.java @@ -0,0 +1,33 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; +import android.preference.Preference; + +/** + * A custom preference that clears the ReVanced debug log buffer when clicked. + * Invokes the {@link LogBufferManager#clearLogBuffer} method. + */ +@SuppressWarnings("unused") +public class ClearLogBufferPreference extends Preference { + + { + setOnPreferenceClickListener(pref -> { + LogBufferManager.clearLogBuffer(); + return true; + }); + } + + public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ClearLogBufferPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ClearLogBufferPreference(Context context) { + super(context); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java new file mode 100644 index 0000000000..c9fc7b6da9 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java @@ -0,0 +1,476 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; + +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; +import app.revanced.extension.shared.ui.ColorDot; +import app.revanced.extension.shared.ui.CustomDialog; +import app.revanced.extension.shared.ui.Dim; + +/** + * A custom preference for selecting a color via a hexadecimal code or a color picker dialog. + * Extends {@link EditTextPreference} to display a colored dot in the widget area, + * reflecting the currently selected color. The dot is dimmed when the preference is disabled. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class ColorPickerPreference extends EditTextPreference { + /** Length of a valid color string of format #RRGGBB (without alpha) or #AARRGGBB (with alpha). */ + public static final int COLOR_STRING_LENGTH_WITHOUT_ALPHA = 7; + public static final int COLOR_STRING_LENGTH_WITH_ALPHA = 9; + + /** Matches everything that is not a hex number/letter. */ + private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]"); + + /** Alpha for dimming when the preference is disabled. */ + public static final float DISABLED_ALPHA = 0.5f; // 50% + + /** View displaying a colored dot in the widget area. */ + private View widgetColorDot; + + /** Dialog View displaying a colored dot for the selected color preview in the dialog. */ + private View dialogColorDot; + + /** Current color, including alpha channel if opacity slider is enabled. */ + @ColorInt + private int currentColor; + + /** Associated setting for storing the color value. */ + private StringSetting colorSetting; + + /** Dialog TextWatcher for the EditText to monitor color input changes. */ + private TextWatcher colorTextWatcher; + + /** Dialog color picker view. */ + protected ColorPickerView dialogColorPickerView; + + /** Listener for color changes. */ + protected OnColorChangeListener colorChangeListener; + + /** Whether the opacity slider is enabled. */ + private boolean opacitySliderEnabled = false; + + public static final int ID_REVANCED_COLOR_PICKER_VIEW = + getResourceIdentifierOrThrow(ResourceType.ID, "revanced_color_picker_view"); + public static final int ID_PREFERENCE_COLOR_DOT = + getResourceIdentifierOrThrow(ResourceType.ID, "preference_color_dot"); + public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET = + getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_dot_widget"); + public static final int LAYOUT_REVANCED_COLOR_PICKER = + getResourceIdentifierOrThrow(ResourceType.LAYOUT, "revanced_color_picker"); + + /** + * Removes non valid hex characters, converts to all uppercase, + * and adds # character to the start if not present. + */ + public static String cleanupColorCodeString(String colorString, boolean includeAlpha) { + String result = "#" + PATTERN_NOT_HEX.matcher(colorString) + .replaceAll("").toUpperCase(Locale.ROOT); + + int maxLength = includeAlpha ? COLOR_STRING_LENGTH_WITH_ALPHA : COLOR_STRING_LENGTH_WITHOUT_ALPHA; + if (result.length() < maxLength) { + return result; + } + + return result.substring(0, maxLength); + } + + /** + * @param color Color, with or without alpha channel. + * @param includeAlpha Whether to include the alpha channel in the output string. + * @return #RRGGBB or #AARRGGBB hex color string + */ + public static String getColorString(@ColorInt int color, boolean includeAlpha) { + if (includeAlpha) { + return String.format("#%08X", color); + } + color = color & 0x00FFFFFF; // Mask to strip alpha. + return String.format("#%06X", color); + } + + /** + * Interface for notifying color changes. + */ + public interface OnColorChangeListener { + void onColorChanged(String key, int newColor); + } + + /** + * Sets the listener for color changes. + */ + public void setOnColorChangeListener(OnColorChangeListener listener) { + this.colorChangeListener = listener; + } + + /** + * Enables or disables the opacity slider in the color picker dialog. + */ + public void setOpacitySliderEnabled(boolean enabled) { + this.opacitySliderEnabled = enabled; + } + + public ColorPickerPreference(Context context) { + super(context); + init(); + } + + public ColorPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + /** + * Initializes the preference by setting up the EditText, loading the color, and set the widget layout. + */ + private void init() { + if (getKey() != null) { + colorSetting = (StringSetting) Setting.getSettingFromPath(getKey()); + if (colorSetting == null) { + Logger.printException(() -> "Could not find color setting for: " + getKey()); + } + } else { + Logger.printDebug(() -> "initialized without key, settings will be loaded later"); + } + + EditText editText = getEditText(); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + editText.setAutofillHints((String) null); + } + + // Set the widget layout to a custom layout containing the colored dot. + setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET); + } + + /** + * Sets the selected color and updates the UI and settings. + */ + @Override + public void setText(String colorString) { + try { + Logger.printDebug(() -> "setText: " + colorString); + super.setText(colorString); + + currentColor = Color.parseColor(colorString); + if (colorSetting != null) { + colorSetting.save(getColorString(currentColor, opacitySliderEnabled)); + } + updateDialogColorDot(); + updateWidgetColorDot(); + + // Notify the listener about the color change. + if (colorChangeListener != null) { + colorChangeListener.onColorChanged(getKey(), currentColor); + } + } catch (IllegalArgumentException ex) { + // This code is reached if the user pastes settings json with an invalid color + // since this preference is updated with the new setting text. + Logger.printDebug(() -> "Parse color error: " + colorString, ex); + Utils.showToastShort(str("revanced_settings_color_invalid")); + setText(colorSetting.resetToDefault()); + } catch (Exception ex) { + Logger.printException(() -> "setText failure: " + colorString, ex); + } + } + + /** + * Creates a TextWatcher to monitor changes in the EditText for color input. + */ + private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) { + return new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable edit) { + try { + String colorString = edit.toString(); + String sanitizedColorString = cleanupColorCodeString(colorString, opacitySliderEnabled); + if (!sanitizedColorString.equals(colorString)) { + edit.replace(0, colorString.length(), sanitizedColorString); + return; + } + + int expectedLength = opacitySliderEnabled + ? COLOR_STRING_LENGTH_WITH_ALPHA + : COLOR_STRING_LENGTH_WITHOUT_ALPHA; + if (sanitizedColorString.length() != expectedLength) { + return; + } + + final int newColor = Color.parseColor(colorString); + if (currentColor != newColor) { + Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString); + currentColor = newColor; + updateDialogColorDot(); + updateWidgetColorDot(); + colorPickerView.setColor(newColor); + } + } catch (Exception ex) { + // Should never be reached since input is validated before using. + Logger.printException(() -> "afterTextChanged failure", ex); + } + } + }; + } + + /** + * Hook for subclasses to add a custom view to the top of the dialog. + */ + @Nullable + protected View createExtraDialogContentView(Context context) { + return null; // Default implementation returns no extra view. + } + + /** + * Hook for subclasses to handle the OK button click. + */ + protected void onDialogOkClicked() { + // Default implementation does nothing. + } + + /** + * Hook for subclasses to handle the Neutral button click. + */ + protected void onDialogNeutralClicked() { + // Default implementation. + try { + final int defaultColor = Color.parseColor(colorSetting.defaultValue); + dialogColorPickerView.setColor(defaultColor); + } catch (Exception ex) { + Logger.printException(() -> "Reset button failure", ex); + } + } + + @Override + protected void showDialog(Bundle state) { + Context context = getContext(); + + // Create content container for all dialog views. + LinearLayout contentContainer = new LinearLayout(context); + contentContainer.setOrientation(LinearLayout.VERTICAL); + + // Add extra view from subclass if it exists. + View extraView = createExtraDialogContentView(context); + if (extraView != null) { + contentContainer.addView(extraView); + } + + // Inflate color picker view. + View colorPicker = LayoutInflater.from(context).inflate(LAYOUT_REVANCED_COLOR_PICKER, null); + dialogColorPickerView = colorPicker.findViewById(ID_REVANCED_COLOR_PICKER_VIEW); + dialogColorPickerView.setOpacitySliderEnabled(opacitySliderEnabled); + dialogColorPickerView.setColor(currentColor); + contentContainer.addView(colorPicker); + + // Horizontal layout for preview and EditText. + LinearLayout inputLayout = new LinearLayout(context); + inputLayout.setOrientation(LinearLayout.HORIZONTAL); + inputLayout.setGravity(Gravity.CENTER_VERTICAL); + + dialogColorDot = new View(context); + LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(Dim.dp20,Dim.dp20); + previewParams.setMargins(Dim.dp16, 0, Dim.dp10, 0); + dialogColorDot.setLayoutParams(previewParams); + inputLayout.addView(dialogColorDot); + updateDialogColorDot(); + + EditText editText = getEditText(); + ViewParent parent = editText.getParent(); + if (parent instanceof ViewGroup parentViewGroup) { + parentViewGroup.removeView(editText); + } + editText.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + String currentColorString = getColorString(currentColor, opacitySliderEnabled); + editText.setText(currentColorString); + editText.setSelection(currentColorString.length()); + editText.setTypeface(Typeface.MONOSPACE); + colorTextWatcher = createColorTextWatcher(dialogColorPickerView); + editText.addTextChangedListener(colorTextWatcher); + inputLayout.addView(editText); + + // Add a dummy view to take up remaining horizontal space, + // otherwise it will show an oversize underlined text view. + View paddingView = new View(context); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + 0, + LinearLayout.LayoutParams.MATCH_PARENT, + 1f + ); + paddingView.setLayoutParams(params); + inputLayout.addView(paddingView); + + contentContainer.addView(inputLayout); + + // Create ScrollView to wrap the content container. + ScrollView contentScrollView = new ScrollView(context); + contentScrollView.setVerticalScrollBarEnabled(false); + contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); + LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 0, + 1.0f + ); + contentScrollView.setLayoutParams(scrollViewParams); + contentScrollView.addView(contentContainer); + + final int originalColor = currentColor; + Pair dialogPair = CustomDialog.create( + context, + getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"), + null, + null, + null, + () -> { // OK button action. + try { + String colorString = editText.getText().toString(); + int expectedLength = opacitySliderEnabled + ? COLOR_STRING_LENGTH_WITH_ALPHA + : COLOR_STRING_LENGTH_WITHOUT_ALPHA; + if (colorString.length() != expectedLength) { + Utils.showToastShort(str("revanced_settings_color_invalid")); + setText(getColorString(originalColor, opacitySliderEnabled)); + return; + } + setText(colorString); + + onDialogOkClicked(); + } catch (Exception ex) { + // Should never happen due to a bad color string, + // since the text is validated and fixed while the user types. + Logger.printException(() -> "OK button failure", ex); + } + }, + () -> { // Cancel button action. + try { + setText(getColorString(originalColor, opacitySliderEnabled)); + } catch (Exception ex) { + Logger.printException(() -> "Cancel button failure", ex); + } + }, + str("revanced_settings_reset_color"), // Neutral button text. + this::onDialogNeutralClicked, // Neutral button action. + false // Do not dismiss dialog. + ); + + // Add the ScrollView to the dialog's main layout. + LinearLayout dialogMainLayout = dialogPair.second; + dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1); + + // Set up color picker listener with debouncing. + // Add listener last to prevent callbacks from set calls above. + dialogColorPickerView.setOnColorChangedListener(color -> { + // Check if it actually changed, since this callback + // can be caused by updates in afterTextChanged(). + if (currentColor == color) { + return; + } + + String updatedColorString = getColorString(color, opacitySliderEnabled); + Logger.printDebug(() -> "onColorChanged: " + updatedColorString); + currentColor = color; + editText.setText(updatedColorString); + editText.setSelection(updatedColorString.length()); + + updateDialogColorDot(); + updateWidgetColorDot(); + }); + + // Configure and show the dialog. + Dialog dialog = dialogPair.first; + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (colorTextWatcher != null) { + getEditText().removeTextChangedListener(colorTextWatcher); + colorTextWatcher = null; + } + + dialogColorDot = null; + dialogColorPickerView = null; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + updateWidgetColorDot(); + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + + widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT); + updateWidgetColorDot(); + } + + private void updateWidgetColorDot() { + if (widgetColorDot == null) return; + + ColorDot.applyColorDot( + widgetColorDot, + currentColor, + widgetColorDot.isEnabled() + ); + } + + private void updateDialogColorDot() { + if (dialogColorDot == null) return; + + ColorDot.applyColorDot( + dialogColorDot, + currentColor, + true + ); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java new file mode 100644 index 0000000000..b8c9577112 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java @@ -0,0 +1,639 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ComposeShader; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.RectF; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.ColorInt; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.ui.Dim; + +/** + * A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector + * and an optional opacity slider. + * This implementation is density-independent and responsive across different screen sizes and DPIs. + *

+ * This view displays three main components for color selection: + *

    + *
  • Hue Bar: A horizontal bar at the bottom that allows the user to select the hue component of the color. + *
  • Saturation-Value Selector: A rectangular area above the hue bar that allows the user to select the + * saturation and value (brightness) components of the color based on the selected hue. + *
  • Opacity Slider: An optional horizontal bar below the hue bar that allows the user to adjust + * the opacity (alpha channel) of the color. + *
+ *

+ * The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar, + * opacity slider, and the saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles). + *

+ * The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}. + * An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes. + */ +public class ColorPickerView extends View { + /** + * Interface definition for a callback to be invoked when the selected color changes. + */ + public interface OnColorChangedListener { + /** + * Called when the selected color has changed. + */ + void onColorChanged(@ColorInt int color); + } + + /** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */ + public static final float TOUCH_EXPANSION = Dim.dp20; + + /** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */ + private static final float MARGIN_BETWEEN_AREAS = Dim.dp24; + + /** Padding around the view. */ + private static final float VIEW_PADDING = Dim.dp16; + + /** Height of the hue bar. */ + private static final float HUE_BAR_HEIGHT = Dim.dp12; + + /** Height of the opacity slider. */ + private static final float OPACITY_BAR_HEIGHT = Dim.dp12; + + /** Corner radius for the hue bar. */ + private static final float HUE_CORNER_RADIUS = Dim.dp6; + + /** Corner radius for the opacity slider. */ + private static final float OPACITY_CORNER_RADIUS = Dim.dp6; + + /** Radius of the selector handles. */ + private static final float SELECTOR_RADIUS = Dim.dp12; + + /** Stroke width for the selector handle outlines. */ + private static final float SELECTOR_STROKE_WIDTH = 8; + + /** + * Hue and opacity fill radius. Use slightly smaller radius for the selector handle fill, + * otherwise the anti-aliasing causes the fill color to bleed past the selector outline. + */ + private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2; + + /** Thin dark outline stroke width for the selector rings. */ + private static final float SELECTOR_EDGE_STROKE_WIDTH = 1; + + /** Radius for the outer edge of the selector rings, including stroke width. */ + public static final float SELECTOR_EDGE_RADIUS = + SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2; + + /** Selector outline inner color. */ + @ColorInt + private static final int SELECTOR_OUTLINE_COLOR = Color.WHITE; + + /** Dark edge color for the selector rings. */ + @ColorInt + private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF"); + + /** Precomputed array of hue colors for the hue bar (0-360 degrees). */ + private static final int[] HUE_COLORS = new int[361]; + static { + for (int i = 0; i < 361; i++) { + HUE_COLORS[i] = Color.HSVToColor(new float[]{i, 1, 1}); + } + } + + /** Paint for the hue bar. */ + private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + /** Paint for the opacity slider. */ + private final Paint opacityPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + /** Paint for the saturation-value selector. */ + private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + /** Paint for the draggable selector handles. */ + private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + { + selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH); + } + + /** Bounds of the hue bar. */ + private final RectF hueRect = new RectF(); + + /** Bounds of the opacity slider. */ + private final RectF opacityRect = new RectF(); + + /** Bounds of the saturation-value selector. */ + private final RectF saturationValueRect = new RectF(); + + /** HSV color calculations to avoid allocations during drawing. */ + private final float[] hsvArray = {1, 1, 1}; + + /** Current hue value (0-360). */ + private float hue = 0f; + + /** Current saturation value (0-1). */ + private float saturation = 1f; + + /** Current value (brightness) value (0-1). */ + private float value = 1f; + + /** Current opacity value (0-1). */ + private float opacity = 1f; + + /** The currently selected color, including alpha channel if opacity slider is enabled. */ + @ColorInt + private int selectedColor; + + /** Listener for color change events. */ + private OnColorChangedListener colorChangedListener; + + /** Tracks if the hue selector is being dragged. */ + private boolean isDraggingHue; + + /** Tracks if the saturation-value selector is being dragged. */ + private boolean isDraggingSaturation; + + /** Tracks if the opacity selector is being dragged. */ + private boolean isDraggingOpacity; + + /** Flag to enable/disable the opacity slider. */ + private boolean opacitySliderEnabled = false; + + public ColorPickerView(Context context) { + super(context); + } + + public ColorPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * Enables or disables the opacity slider. + */ + public void setOpacitySliderEnabled(boolean enabled) { + if (opacitySliderEnabled != enabled) { + opacitySliderEnabled = enabled; + if (!enabled) { + opacity = 1f; // Reset to fully opaque when disabled. + updateSelectedColor(); + } + updateOpacityShader(); + requestLayout(); // Trigger re-measure to account for opacity slider. + invalidate(); + } + } + + /** + * Measures the view, ensuring a consistent aspect ratio and minimum dimensions. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8 + + final int minWidth = Dim.dp(250); + final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) + + (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0); + + int width = resolveSize(minWidth, widthMeasureSpec); + int height = resolveSize(minHeight, heightMeasureSpec); + + // Ensure minimum dimensions for usability. + width = Math.max(width, minWidth); + height = Math.max(height, minHeight); + + // Adjust height to maintain desired aspect ratio if possible. + final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) + + (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0); + if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) { + height = desiredHeight; + } + + setMeasuredDimension(width, height); + } + + /** + * Updates the view's layout when its size changes, recalculating bounds and shaders. + */ + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + + // Calculate bounds with hue bar and optional opacity bar at the bottom. + final float effectiveWidth = width - (2 * VIEW_PADDING); + final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS + - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0); + + // Adjust rectangles to account for padding and density-independent dimensions. + saturationValueRect.set( + VIEW_PADDING, + VIEW_PADDING, + VIEW_PADDING + effectiveWidth, + VIEW_PADDING + effectiveHeight + ); + + hueRect.set( + VIEW_PADDING, + height - VIEW_PADDING - HUE_BAR_HEIGHT - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0), + VIEW_PADDING + effectiveWidth, + height - VIEW_PADDING - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0) + ); + + if (opacitySliderEnabled) { + opacityRect.set( + VIEW_PADDING, + height - VIEW_PADDING - OPACITY_BAR_HEIGHT, + VIEW_PADDING + effectiveWidth, + height - VIEW_PADDING + ); + } + + // Update the shaders. + updateHueShader(); + updateSaturationValueShader(); + updateOpacityShader(); + } + + /** + * Updates the shader for the hue bar to reflect the color gradient. + */ + private void updateHueShader() { + LinearGradient hueShader = new LinearGradient( + hueRect.left, hueRect.top, + hueRect.right, hueRect.top, + HUE_COLORS, + null, + Shader.TileMode.CLAMP + ); + + huePaint.setShader(hueShader); + } + + /** + * Updates the shader for the opacity slider to reflect the current RGB color with varying opacity. + */ + private void updateOpacityShader() { + if (!opacitySliderEnabled) { + opacityPaint.setShader(null); + return; + } + + // Create a linear gradient for opacity from transparent to opaque, using the current RGB color. + int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value}); + LinearGradient opacityShader = new LinearGradient( + opacityRect.left, opacityRect.top, + opacityRect.right, opacityRect.top, + rgbColor & 0x00FFFFFF, // Fully transparent + rgbColor | 0xFF000000, // Fully opaque + Shader.TileMode.CLAMP + ); + + opacityPaint.setShader(opacityShader); + } + + /** + * Updates the shader for the saturation-value selector to reflect the current hue. + */ + private void updateSaturationValueShader() { + // Create a saturation-value gradient based on the current hue. + // Calculate the start color (white with the selected hue) for the saturation gradient. + final int startColor = Color.HSVToColor(new float[]{hue, 0f, 1f}); + + // Calculate the middle color (fully saturated color with the selected hue) for the saturation gradient. + final int midColor = Color.HSVToColor(new float[]{hue, 1f, 1f}); + + // Create a linear gradient for the saturation from startColor to midColor (horizontal). + LinearGradient satShader = new LinearGradient( + saturationValueRect.left, saturationValueRect.top, + saturationValueRect.right, saturationValueRect.top, + startColor, + midColor, + Shader.TileMode.CLAMP + ); + + // Create a linear gradient for the value (brightness) from white to black (vertical). + LinearGradient valShader = new LinearGradient( + saturationValueRect.left, saturationValueRect.top, + saturationValueRect.left, saturationValueRect.bottom, + Color.WHITE, + Color.BLACK, + Shader.TileMode.CLAMP + ); + + // Combine the saturation and value shaders using PorterDuff.Mode.MULTIPLY to create the final color. + ComposeShader combinedShader = new ComposeShader(satShader, valShader, PorterDuff.Mode.MULTIPLY); + + // Set the combined shader for the saturation-value paint. + saturationValuePaint.setShader(combinedShader); + } + + /** + * Draws the color picker components, including the saturation-value selector, hue bar, opacity slider, and their respective handles. + */ + @Override + protected void onDraw(Canvas canvas) { + // Draw the saturation-value selector rectangle. + canvas.drawRect(saturationValueRect, saturationValuePaint); + + // Draw the hue bar. + canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint); + + // Draw the opacity bar if enabled. + if (opacitySliderEnabled) { + canvas.drawRoundRect(opacityRect, OPACITY_CORNER_RADIUS, OPACITY_CORNER_RADIUS, opacityPaint); + } + + final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width(); + final float hueSelectorY = hueRect.centerY(); + + final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width(); + final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height(); + + // Draw the saturation and hue selector handles filled with their respective colors (fully opaque). + hsvArray[0] = hue; + final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray); // Force opaque for hue handle. + final int satHandleColor = Color.HSVToColor(0xFF, new float[]{hue, saturation, value}); // Force opaque for sat-val handle. + selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE); + + selectorPaint.setColor(hueHandleColor); + canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint); + + selectorPaint.setColor(satHandleColor); + canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint); + + if (opacitySliderEnabled) { + final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width(); + final float opacitySelectorY = opacityRect.centerY(); + selectorPaint.setColor(selectedColor); // Use full ARGB color to show opacity. + canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_FILL_RADIUS, selectorPaint); + } + + // Draw white outlines for the handles. + selectorPaint.setColor(SELECTOR_OUTLINE_COLOR); + selectorPaint.setStyle(Paint.Style.STROKE); + selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH); + canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint); + canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint); + if (opacitySliderEnabled) { + final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width(); + final float opacitySelectorY = opacityRect.centerY(); + canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_RADIUS, selectorPaint); + } + + // Draw thin dark outlines for the handles at the outer edge of the white outline. + selectorPaint.setColor(SELECTOR_EDGE_COLOR); + selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH); + canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint); + canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint); + if (opacitySliderEnabled) { + final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width(); + final float opacitySelectorY = opacityRect.centerY(); + canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_EDGE_RADIUS, selectorPaint); + } + } + + /** + * Handles touch events to allow dragging of the hue, saturation-value, and opacity selectors. + * + * @param event The motion event. + * @return True if the event was handled, false otherwise. + */ + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent event) { + try { + final float x = event.getX(); + final float y = event.getY(); + final int action = event.getAction(); + Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y); + + // Define touch expansion for the hue and opacity bars. + RectF expandedHueRect = new RectF( + hueRect.left, + hueRect.top - TOUCH_EXPANSION, + hueRect.right, + hueRect.bottom + TOUCH_EXPANSION + ); + RectF expandedOpacityRect = opacitySliderEnabled ? new RectF( + opacityRect.left, + opacityRect.top - TOUCH_EXPANSION, + opacityRect.right, + opacityRect.bottom + TOUCH_EXPANSION + ) : new RectF(); + + switch (action) { + case MotionEvent.ACTION_DOWN: + // Calculate current handle positions. + final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width(); + final float hueSelectorY = hueRect.centerY(); + + final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width(); + final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height(); + + final float opacitySelectorX = opacitySliderEnabled ? opacityRect.left + opacity * opacityRect.width() : 0; + final float opacitySelectorY = opacitySliderEnabled ? opacityRect.centerY() : 0; + + // Create hit areas for all handles. + RectF hueHitRect = new RectF( + hueSelectorX - SELECTOR_RADIUS, + hueSelectorY - SELECTOR_RADIUS, + hueSelectorX + SELECTOR_RADIUS, + hueSelectorY + SELECTOR_RADIUS + ); + RectF satValHitRect = new RectF( + satSelectorX - SELECTOR_RADIUS, + valSelectorY - SELECTOR_RADIUS, + satSelectorX + SELECTOR_RADIUS, + valSelectorY + SELECTOR_RADIUS + ); + RectF opacityHitRect = opacitySliderEnabled ? new RectF( + opacitySelectorX - SELECTOR_RADIUS, + opacitySelectorY - SELECTOR_RADIUS, + opacitySelectorX + SELECTOR_RADIUS, + opacitySelectorY + SELECTOR_RADIUS + ) : new RectF(); + + // Check if the touch started on a handle or within the expanded bar areas. + if (hueHitRect.contains(x, y)) { + isDraggingHue = true; + updateHueFromTouch(x); + } else if (satValHitRect.contains(x, y)) { + isDraggingSaturation = true; + updateSaturationValueFromTouch(x, y); + } else if (opacitySliderEnabled && opacityHitRect.contains(x, y)) { + isDraggingOpacity = true; + updateOpacityFromTouch(x); + } else if (expandedHueRect.contains(x, y)) { + // Handle touch within the expanded hue bar area. + isDraggingHue = true; + updateHueFromTouch(x); + } else if (saturationValueRect.contains(x, y)) { + isDraggingSaturation = true; + updateSaturationValueFromTouch(x, y); + } else if (opacitySliderEnabled && expandedOpacityRect.contains(x, y)) { + isDraggingOpacity = true; + updateOpacityFromTouch(x); + } + break; + + case MotionEvent.ACTION_MOVE: + // Continue updating values even if touch moves outside the view. + if (isDraggingHue) { + updateHueFromTouch(x); + } else if (isDraggingSaturation) { + updateSaturationValueFromTouch(x, y); + } else if (isDraggingOpacity) { + updateOpacityFromTouch(x); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + isDraggingHue = false; + isDraggingSaturation = false; + isDraggingOpacity = false; + break; + } + } catch (Exception ex) { + Logger.printException(() -> "onTouchEvent failure", ex); + } + + return true; + } + + /** + * Updates the hue value based on a touch event. + */ + private void updateHueFromTouch(float x) { + // Clamp x to the hue rectangle bounds. + final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right); + final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f; + if (hue == updatedHue) { + return; + } + + hue = updatedHue; + updateSaturationValueShader(); + updateOpacityShader(); + updateSelectedColor(); + } + + /** + * Updates the saturation and value based on a touch event. + */ + private void updateSaturationValueFromTouch(float x, float y) { + // Clamp x and y to the saturation-value rectangle bounds. + final float clampedX = Utils.clamp(x, saturationValueRect.left, saturationValueRect.right); + final float clampedY = Utils.clamp(y, saturationValueRect.top, saturationValueRect.bottom); + + final float updatedSaturation = (clampedX - saturationValueRect.left) / saturationValueRect.width(); + final float updatedValue = 1 - ((clampedY - saturationValueRect.top) / saturationValueRect.height()); + + if (saturation == updatedSaturation && value == updatedValue) { + return; + } + saturation = updatedSaturation; + value = updatedValue; + updateOpacityShader(); + updateSelectedColor(); + } + + /** + * Updates the opacity value based on a touch event. + */ + private void updateOpacityFromTouch(float x) { + if (!opacitySliderEnabled) { + return; + } + final float clampedX = Utils.clamp(x, opacityRect.left, opacityRect.right); + final float updatedOpacity = (clampedX - opacityRect.left) / opacityRect.width(); + if (opacity == updatedOpacity) { + return; + } + opacity = updatedOpacity; + updateSelectedColor(); + } + + /** + * Updates the selected color based on the current hue, saturation, value, and opacity. + */ + private void updateSelectedColor() { + final int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value}); + final int updatedColor = opacitySliderEnabled + ? (rgbColor & 0x00FFFFFF) | (((int) (opacity * 255)) << 24) + : (rgbColor & 0x00FFFFFF) | 0xFF000000; + + if (selectedColor != updatedColor) { + selectedColor = updatedColor; + + if (colorChangedListener != null) { + colorChangedListener.onColorChanged(updatedColor); + } + } + + // Must always redraw, otherwise if saturation is pure grey or black + // then the hue slider cannot be changed. + invalidate(); + } + + /** + * Sets the selected color, updating the hue, saturation, value and opacity sliders accordingly. + */ + public void setColor(@ColorInt int color) { + if (selectedColor == color) { + return; + } + + // Update the selected color. + selectedColor = color; + Logger.printDebug(() -> "setColor: " + getColorString(selectedColor, opacitySliderEnabled)); + + // Convert the ARGB color to HSV values. + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + + // Update the hue, saturation, and value. + hue = hsv[0]; + saturation = hsv[1]; + value = hsv[2]; + opacity = opacitySliderEnabled ? ((color >> 24) & 0xFF) / 255f : 1f; + + // Update the saturation-value shader based on the new hue. + updateSaturationValueShader(); + updateOpacityShader(); + + // Notify the listener if it's set. + if (colorChangedListener != null) { + colorChangedListener.onColorChanged(selectedColor); + } + + // Invalidate the view to trigger a redraw. + invalidate(); + } + + /** + * Gets the currently selected color. + */ + @ColorInt + public int getColor() { + return selectedColor; + } + + /** + * Sets a listener to be notified when the selected color changes. + */ + public void setOnColorChangedListener(OnColorChangedListener listener) { + colorChangedListener = listener; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java new file mode 100644 index 0000000000..5e24f7bf36 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerWithOpacitySliderPreference.java @@ -0,0 +1,34 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; + +/** + * Extended ColorPickerPreference that enables the opacity slider for color selection. + */ +@SuppressWarnings("unused") +public class ColorPickerWithOpacitySliderPreference extends ColorPickerPreference { + + public ColorPickerWithOpacitySliderPreference(Context context) { + super(context); + init(); + } + + public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + /** + * Initialize the preference with opacity slider enabled. + */ + private void init() { + // Enable the opacity slider for alpha channel support. + setOpacitySliderEnabled(true); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java new file mode 100644 index 0000000000..48c50c1f33 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/CustomDialogListPreference.java @@ -0,0 +1,267 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +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; + +/** + * A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator, + * supports a static summary and highlighted entries for search functionality. + */ +@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"); + + private String staticSummary = null; + private CharSequence[] highlightedEntriesForDialog = null; + + /** + * Set a static summary that will not be overwritten by value changes. + */ + public void setStaticSummary(String summary) { + this.staticSummary = summary; + } + + /** + * Returns the static summary if set, otherwise null. + */ + @Nullable + public String getStaticSummary() { + return staticSummary; + } + + /** + * Always return static summary if set. + */ + @Override + public CharSequence getSummary() { + if (staticSummary != null) { + return staticSummary; + } + return super.getSummary(); + } + + /** + * Sets highlighted entries for display in the dialog. + * These entries are used only for the current dialog and are automatically cleared. + */ + public void setHighlightedEntriesForDialog(CharSequence[] highlightedEntries) { + this.highlightedEntriesForDialog = highlightedEntries; + } + + /** + * Clears highlighted entries after the dialog is closed. + */ + public void clearHighlightedEntriesForDialog() { + this.highlightedEntriesForDialog = null; + } + + /** + * Returns entries for display in the dialog. + * If highlighted entries exist, they are used; otherwise, the original entries are returned. + */ + private CharSequence[] getEntriesForDialog() { + return highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries(); + } + + /** + * Custom ArrayAdapter to handle checkmark visibility. + */ + public static class ListPreferenceArrayAdapter extends ArrayAdapter { + private static class SubViewDataContainer { + ImageView checkIcon; + View placeholder; + TextView itemText; + } + + final int layoutResourceId; + final CharSequence[] entryValues; + String selectedValue; + + public ListPreferenceArrayAdapter(Context context, int resource, + CharSequence[] entries, + CharSequence[] entryValues, + String selectedValue) { + super(context, resource, entries); + this.layoutResourceId = resource; + this.entryValues = entryValues; + this.selectedValue = selectedValue; + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + View view = convertView; + SubViewDataContainer holder; + + if (view == null) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + view = inflater.inflate(layoutResourceId, parent, false); + holder = new SubViewDataContainer(); + holder.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(); + } + + CharSequence itemText = getItem(position); + holder.itemText.setText(itemText); + holder.itemText.setTextColor(Utils.getAppForegroundColor()); + + // Show or hide checkmark and placeholder. + String currentValue = entryValues[position].toString(); + boolean isSelected = currentValue.equals(selectedValue); + holder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE); + holder.checkIcon.setColorFilter(Utils.getAppForegroundColor()); + holder.placeholder.setVisibility(isSelected ? View.GONE : View.VISIBLE); + + return view; + } + + public void setSelectedValue(String value) { + this.selectedValue = value; + } + } + + public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CustomDialogListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomDialogListPreference(Context context) { + super(context); + } + + @Override + protected void showDialog(Bundle state) { + Context context = getContext(); + + CharSequence[] entriesToShow = getEntriesForDialog(); + CharSequence[] entryValues = getEntryValues(); + + // Create ListView. + ListView listView = new ListView(context); + listView.setId(android.R.id.list); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + + // Create custom adapter for the ListView. + ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter( + context, + LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED, + entriesToShow, + entryValues, + getValue() + ); + listView.setAdapter(adapter); + + // Set checked item. + String currentValue = getValue(); + if (currentValue != null) { + for (int i = 0, length = entryValues.length; i < length; i++) { + if (currentValue.equals(entryValues[i].toString())) { + listView.setItemChecked(i, true); + listView.setSelection(i); + break; + } + } + } + + // Create the custom dialog without OK button. + Pair dialogPair = CustomDialog.create( + context, + getTitle() != null ? getTitle().toString() : "", + null, + null, + null, + null, + this::clearHighlightedEntriesForDialog, // Cancel button action. + null, + null, + true + ); + + Dialog dialog = dialogPair.first; + // Add a listener to clear when the dialog is closed in any way. + dialog.setOnDismissListener(dialogInterface -> clearHighlightedEntriesForDialog()); + + // Add the ListView to the main layout. + LinearLayout mainLayout = dialogPair.second; + LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + 0, + 1.0f + ); + mainLayout.addView(listView, mainLayout.getChildCount() - 1, listViewParams); + + // Handle item click to select value and dismiss dialog. + listView.setOnItemClickListener((parent, view, position, id) -> { + String selectedValue = entryValues[position].toString(); + if (callChangeListener(selectedValue)) { + setValue(selectedValue); + + // Update summaries from the original entries (without highlighting). + if (staticSummary == null) { + CharSequence[] originalEntries = getEntries(); + if (originalEntries != null && position < originalEntries.length) { + setSummary(originalEntries[position]); + } + } + + adapter.setSelectedValue(selectedValue); + adapter.notifyDataSetChanged(); + } + + // Clear highlighted entries before closing. + clearHighlightedEntriesForDialog(); + dialog.dismiss(); + }); + + // Show the dialog. + dialog.show(); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java new file mode 100644 index 0000000000..57fb128232 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ExportLogToClipboardPreference.java @@ -0,0 +1,33 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; +import android.preference.Preference; + +/** + * A custom preference that triggers exporting ReVanced debug logs to the clipboard when clicked. + * Invokes the {@link LogBufferManager#exportToClipboard} method. + */ +@SuppressWarnings({"deprecation", "unused"}) +public class ExportLogToClipboardPreference extends Preference { + + { + setOnPreferenceClickListener(pref -> { + LogBufferManager.exportToClipboard(); + return true; + }); + } + + public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ExportLogToClipboardPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ExportLogToClipboardPreference(Context context) { + super(context); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java new file mode 100644 index 0000000000..4e6d2e5cdd --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/FeatureFlagsManagerPreference.java @@ -0,0 +1,626 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.preference.Preference; +import android.text.Editable; +import android.text.InputType; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.Pair; +import android.util.SparseBooleanArray; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.Space; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; +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; +import app.revanced.extension.shared.ui.CustomDialog; +import app.revanced.extension.shared.ui.Dim; + +/** + * A custom preference that opens a dialog for managing feature flags. + * Allows moving boolean flags between active and blocked states with advanced selection. + */ +@SuppressWarnings({"deprecation", "unused"}) +public class FeatureFlagsManagerPreference extends Preference { + + private static final int DRAWABLE_REVANCED_SETTINGS_SELECT_ALL = + getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_select_all"); + private static final int DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL = + getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_deselect_all"); + private static final int DRAWABLE_REVANCED_SETTINGS_COPY_ALL = + getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_copy_all"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE = + getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_one"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE = + getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_right_double"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE = + getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_one"); + private static final int DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE = + getResourceIdentifierOrThrow(ResourceType.DRAWABLE, "revanced_settings_arrow_left_double"); + + /** + * Flags to hide from the UI. + */ + private static final Set 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. + ); + + /** + * Tracks state for range selection in ListView. + */ + private static class ListViewSelectionState { + int lastClickedPosition = -1; // Position of the last clicked item. + boolean isRangeSelecting = false; // True while a range is being selected. + } + + /** + * Helper class to pass ListView and Adapter together. + */ + private record ColumnViews(ListView listView, FlagAdapter adapter) {} + + { + setOnPreferenceClickListener(pref -> { + showFlagsManagerDialog(); + return true; + }); + } + + public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public FeatureFlagsManagerPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public FeatureFlagsManagerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public FeatureFlagsManagerPreference(Context context) { + super(context); + } + + /** + * Shows the main dialog for managing feature flags. + */ + private void showFlagsManagerDialog() { + if (!BaseSettings.DEBUG.get()) { + Utils.showToastShort(str("revanced_debug_logs_disabled")); + return; + } + + Context context = getContext(); + + // Load all known and disabled flags. + TreeSet allKnownFlags = new TreeSet<>(EnableDebuggingPatch.getAllLoggedFlags()); + allKnownFlags.removeAll(FLAGS_TO_IGNORE); + + TreeSet disabledFlags = new TreeSet<>(EnableDebuggingPatch.parseFlags( + BaseSettings.DISABLED_FEATURE_FLAGS.get())); + 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")); + return; + } + + TreeSet availableFlags = new TreeSet<>(allKnownFlags); + availableFlags.removeAll(disabledFlags); + TreeSet blockedFlags = new TreeSet<>(disabledFlags); + + Pair dialogPair = CustomDialog.create( + context, + getTitle() != null ? getTitle().toString() : "", + null, + null, + str("revanced_settings_save"), + () -> saveFlags(blockedFlags), + () -> {}, + str("revanced_settings_reset"), + this::resetFlags, + true + ); + + LinearLayout mainLayout = dialogPair.second; + LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0f); + + // Insert content before the dialog button row. + View contentView = createContentView(context, availableFlags, blockedFlags); + mainLayout.addView(contentView, mainLayout.getChildCount() - 1, contentParams); + + Dialog dialog = dialogPair.first; + dialog.show(); + + Window window = dialog.getWindow(); + if (window != null) { + Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 100, false); + } + } + + /** + * Creates the main content view with two columns. + */ + private View createContentView(Context context, TreeSet availableFlags, TreeSet blockedFlags) { + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + + // Headers. + TextView availableHeader = createHeader(context, "revanced_debug_feature_flags_manager_active_header"); + TextView blockedHeader = createHeader(context, "revanced_debug_feature_flags_manager_blocked_header"); + + LinearLayout headersLayout = new LinearLayout(context); + headersLayout.setOrientation(LinearLayout.HORIZONTAL); + headersLayout.addView(availableHeader, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + headersLayout.addView(blockedHeader, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + // Columns. + View leftColumn = createColumn(context, availableFlags, availableHeader); + View rightColumn = createColumn(context, blockedFlags, blockedHeader); + + ColumnViews leftViews = (ColumnViews) leftColumn.getTag(); + ColumnViews rightViews = (ColumnViews) rightColumn.getTag(); + + updateHeaderCount(availableHeader, leftViews.adapter); + updateHeaderCount(blockedHeader, rightViews.adapter); + + // Main columns layout. + LinearLayout columnsLayout = new LinearLayout(context); + columnsLayout.setOrientation(LinearLayout.HORIZONTAL); + columnsLayout.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)); + columnsLayout.addView(leftColumn, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); + + Space spaceBetweenColumns = new Space(context); + spaceBetweenColumns.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.MATCH_PARENT)); + columnsLayout.addView(spaceBetweenColumns); + + columnsLayout.addView(rightColumn, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)); + + // Move buttons below columns. + Pair moveButtons = createMoveButtons(context, + leftViews.listView, rightViews.listView, + availableFlags, blockedFlags, availableHeader, blockedHeader); + + // Layout for buttons row. + LinearLayout buttonsRow = new LinearLayout(context); + buttonsRow.setOrientation(LinearLayout.HORIZONTAL); + buttonsRow.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + buttonsRow.addView(moveButtons.first, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + Space spaceBetweenButtons = new Space(context); + spaceBetweenButtons.setLayoutParams(new LinearLayout.LayoutParams(Dim.dp8, ViewGroup.LayoutParams.WRAP_CONTENT)); + buttonsRow.addView(spaceBetweenButtons); + + buttonsRow.addView(moveButtons.second, new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + contentLayout.addView(headersLayout); + contentLayout.addView(columnsLayout); + contentLayout.addView(buttonsRow); + + return contentLayout; + } + + /** + * Creates a header TextView. + */ + private TextView createHeader(Context context, String tag) { + TextView textview = new TextView(context); + textview.setTag(tag); + textview.setTextSize(16); + textview.setTextColor(Utils.getAppForegroundColor()); + textview.setGravity(Gravity.CENTER); + + return textview; + } + + /** + * Creates a single column (search + buttons + list). + */ + private View createColumn(Context context, TreeSet flags, TextView countText) { + LinearLayout wrapper = new LinearLayout(context); + wrapper.setOrientation(LinearLayout.VERTICAL); + + Pair pair = createListView(context, flags, countText); + ListView listView = pair.first; + FlagAdapter adapter = pair.second; + + EditText search = createSearchBox(context, adapter, listView, countText); + LinearLayout buttons = createActionButtons(context, listView, adapter); + + listView.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)); + ShapeDrawable background = new ShapeDrawable(new RoundRectShape( + Dim.roundedCorners(10), null, null)); + background.getPaint().setColor(Utils.getEditTextBackground()); + listView.setPadding(0, Dim.dp4, 0, Dim.dp4); + listView.setBackground(background); + listView.setOverScrollMode(View.OVER_SCROLL_NEVER); + + wrapper.addView(search); + wrapper.addView(buttons); + wrapper.addView(listView); + + // Save references for move buttons. + wrapper.setTag(new ColumnViews(listView, adapter)); + + return wrapper; + } + + /** + * Updates the header text with the current count. + */ + private void updateHeaderCount(TextView header, FlagAdapter adapter) { + header.setText(str((String) header.getTag(), adapter.getCount())); + } + + /** + * Creates a search box that filters the list. + */ + @SuppressLint("ClickableViewAccessibility") + private EditText createSearchBox(Context context, FlagAdapter adapter, ListView listView, TextView countText) { + EditText search = new EditText(context); + search.setInputType(InputType.TYPE_CLASS_NUMBER); + search.setTextSize(16); + search.setHint(str("revanced_debug_feature_flags_manager_search_hint")); + search.setHapticFeedbackEnabled(false); + search.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + search.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) { + adapter.setSearchQuery(s.toString()); + listView.clearChoices(); + updateHeaderCount(countText, adapter); + Drawable clearIcon = context.getResources().getDrawable(android.R.drawable.ic_menu_close_clear_cancel); + clearIcon.setBounds(0, 0, Dim.dp20, Dim.dp20); + search.setCompoundDrawables(null, null, TextUtils.isEmpty(s) ? null : clearIcon, null); + } + @Override public void afterTextChanged(Editable s) {} + }); + + search.setOnTouchListener((v, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP) { + Drawable[] compoundDrawables = search.getCompoundDrawables(); + if (compoundDrawables[2] != null && + event.getRawX() >= (search.getRight() - compoundDrawables[2].getBounds().width())) { + search.setText(""); + return true; + } + } + return false; + }); + + return search; + } + + /** + * Creates action buttons. + */ + private LinearLayout createActionButtons(Context context, ListView listView, FlagAdapter adapter) { + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER); + row.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + ImageButton selectAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_SELECT_ALL, + () -> { + for (int i = 0, count = adapter.getCount(); i < count; i++) { + listView.setItemChecked(i, true); + } + }); + + ImageButton clearAll = createButton(context, DRAWABLE_REVANCED_SETTINGS_DESELECT_ALL, + () -> { + listView.clearChoices(); + adapter.notifyDataSetChanged(); + }); + + ImageButton copy = createButton(context, DRAWABLE_REVANCED_SETTINGS_COPY_ALL, + () -> { + List items = new ArrayList<>(); + SparseBooleanArray checked = listView.getCheckedItemPositions(); + + if (checked.size() > 0) { + for (int i = 0, count = adapter.getCount(); i < count; i++) { + if (checked.get(i)) { + items.add(adapter.getItem(i)); + } + } + } else { + for (Long flag : adapter.getFullFlags()) { + items.add(String.valueOf(flag)); + } + } + + Utils.setClipboard(TextUtils.join("\n", items)); + + Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_copied")); + }); + + row.addView(selectAll); + row.addView(clearAll); + row.addView(copy); + + return row; + } + + /** + * Creates the move buttons (left and right groups). + */ + private Pair createMoveButtons(Context context, + ListView availableListView, ListView blockedListView, + TreeSet availableFlags, TreeSet blockedFlags, + TextView availableCountText, TextView blockedCountText) { + // Left group: >> > + LinearLayout leftButtons = new LinearLayout(context); + leftButtons.setOrientation(LinearLayout.HORIZONTAL); + leftButtons.setGravity(Gravity.CENTER); + + ImageButton moveAllRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_DOUBLE, + () -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags, + availableCountText, blockedCountText, true)); + + ImageButton moveOneRight = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_RIGHT_ONE, + () -> moveFlags(availableListView, blockedListView, availableFlags, blockedFlags, + availableCountText, blockedCountText, false)); + + leftButtons.addView(moveAllRight); + leftButtons.addView(moveOneRight); + + // Right group: < << + LinearLayout rightButtons = new LinearLayout(context); + rightButtons.setOrientation(LinearLayout.HORIZONTAL); + rightButtons.setGravity(Gravity.CENTER); + + ImageButton moveOneLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_ONE, + () -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags, + blockedCountText, availableCountText, false)); + + ImageButton moveAllLeft = createButton(context, DRAWABLE_REVANCED_SETTINGS_ARROW_LEFT_DOUBLE, + () -> moveFlags(blockedListView, availableListView, blockedFlags, availableFlags, + blockedCountText, availableCountText, true)); + + rightButtons.addView(moveOneLeft); + rightButtons.addView(moveAllLeft); + + return new Pair<>(leftButtons, rightButtons); + } + + /** + * Creates a styled ImageButton. + */ + @SuppressLint("ResourceType") + private ImageButton createButton(Context context, int drawableResId, Runnable action) { + ImageButton button = new ImageButton(context); + + button.setImageResource(drawableResId); + button.setScaleType(ImageView.ScaleType.CENTER); + int[] attrs = {android.R.attr.selectableItemBackgroundBorderless}; + //noinspection Recycle + TypedArray ripple = context.obtainStyledAttributes(attrs); + button.setBackgroundDrawable(ripple.getDrawable(0)); + ripple.close(); + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(Dim.dp32, Dim.dp32); + params.setMargins(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8); + button.setLayoutParams(params); + + button.setOnClickListener(v -> action.run()); + + return button; + } + + /** + * Custom adapter with search filtering. + */ + private static class FlagAdapter extends ArrayAdapter { + private final TreeSet fullFlags; + private String searchQuery = ""; + + public FlagAdapter(Context context, TreeSet fullFlags) { + super(context, android.R.layout.simple_list_item_multiple_choice, new ArrayList<>()); + this.fullFlags = fullFlags; + updateFiltered(); + } + + public void setSearchQuery(String query) { + searchQuery = query == null ? "" : query.trim(); + updateFiltered(); + } + + private void updateFiltered() { + clear(); + for (Long flag : fullFlags) { + String flagString = String.valueOf(flag); + if (searchQuery.isEmpty() || flagString.contains(searchQuery)) { + add(flagString); + } + } + notifyDataSetChanged(); + } + + public void refresh() { + updateFiltered(); + } + + public List getFullFlags() { + return new ArrayList<>(fullFlags); + } + } + + /** + * Creates a ListView with filtering, multi-select, and range selection. + */ + @SuppressLint("ClickableViewAccessibility") + private Pair createListView(Context context, + TreeSet flags, TextView countText) { + ListView listView = new ListView(context); + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + listView.setDividerHeight(0); + + FlagAdapter adapter = new FlagAdapter(context, flags); + listView.setAdapter(adapter); + + final ListViewSelectionState state = new ListViewSelectionState(); + + listView.setOnItemClickListener((parent, view, position, id) -> { + if (!state.isRangeSelecting) { + state.lastClickedPosition = position; + } else { + state.isRangeSelecting = false; + } + }); + + listView.setOnItemLongClickListener((parent, view, position, id) -> { + if (state.lastClickedPosition == -1) { + listView.setItemChecked(position, true); + state.lastClickedPosition = position; + } else { + int start = Math.min(state.lastClickedPosition, position); + int end = Math.max(state.lastClickedPosition, position); + for (int i = start; i <= end; i++) { + listView.setItemChecked(i, true); + } + state.isRangeSelecting = true; + } + return true; + }); + + listView.setOnTouchListener((view, event) -> { + if (event.getAction() == MotionEvent.ACTION_UP && state.isRangeSelecting) { + state.isRangeSelecting = false; + } + return false; + }); + + return new Pair<>(listView, adapter); + } + + /** + * Moves selected or all flags from one list to another. + * + * @param fromListView Source ListView. + * @param toListView Destination ListView. + * @param fromFlags Source flag set. + * @param toFlags Destination flag set. + * @param fromCountText Header showing count of source items. + * @param toCountText Header showing count of destination items. + * @param moveAll If true, move all items; if false, move only selected. + */ + private void moveFlags(ListView fromListView, ListView toListView, + TreeSet fromFlags, TreeSet toFlags, + TextView fromCountText, TextView toCountText, + boolean moveAll) { + if (fromListView == null || toListView == null) return; + + List flagsToMove = new ArrayList<>(); + FlagAdapter fromAdapter = (FlagAdapter) fromListView.getAdapter(); + + if (moveAll) { + flagsToMove.addAll(fromFlags); + } else { + SparseBooleanArray checked = fromListView.getCheckedItemPositions(); + for (int i = 0, count = fromAdapter.getCount(); i < count; i++) { + if (checked.get(i)) { + String item = fromAdapter.getItem(i); + if (item != null) { + flagsToMove.add(Long.parseLong(item)); + } + } + } + } + + if (flagsToMove.isEmpty()) return; + + for (Long flag : flagsToMove) { + fromFlags.remove(flag); + toFlags.add(flag); + } + + // Clear selections before refreshing. + fromListView.clearChoices(); + toListView.clearChoices(); + + // Refresh both adapters. + fromAdapter.refresh(); + ((FlagAdapter) toListView.getAdapter()).refresh(); + + // Update headers. + updateHeaderCount(fromCountText, fromAdapter); + updateHeaderCount(toCountText, (FlagAdapter) toListView.getAdapter()); + } + + /** + * Saves blocked flags to settings. + */ + private void saveFlags(TreeSet blockedFlags) { + StringBuilder flagsString = new StringBuilder(); + for (Long flag : blockedFlags) { + if (flagsString.length() > 0) { + flagsString.append("\n"); + } + flagsString.append(flag); + } + + BaseSettings.DISABLED_FEATURE_FLAGS.save(flagsString.toString()); + Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_saved")); + Logger.printDebug(() -> "Feature flags saved. Blocked: " + blockedFlags.size()); + + AbstractPreferenceFragment.showRestartDialog(getContext()); + } + + /** + * Resets all blocked flags. + */ + private void resetFlags() { + BaseSettings.DISABLED_FEATURE_FLAGS.save(""); + Utils.showToastShort(str("revanced_debug_feature_flags_manager_toast_reset")); + + AbstractPreferenceFragment.showRestartDialog(getContext()); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ForceOriginalAudioSwitchPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ForceOriginalAudioSwitchPreference.java new file mode 100644 index 0000000000..fdcde3668d --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ForceOriginalAudioSwitchPreference.java @@ -0,0 +1,63 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; + +import android.content.Context; +import android.preference.SwitchPreference; +import android.util.AttributeSet; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.spoof.ClientType; +import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch; + +@SuppressWarnings({"deprecation", "unused"}) +public class ForceOriginalAudioSwitchPreference extends SwitchPreference { + + // Spoof stream patch is not included, or is not currently spoofing to Android Studio. + private static final boolean available = !SpoofVideoStreamsPatch.isPatchIncluded() + || !(BaseSettings.SPOOF_VIDEO_STREAMS.get() + && SpoofVideoStreamsPatch.getPreferredClient() == ClientType.ANDROID_CREATOR); + + { + if (!available) { + // Show why force audio is not available. + String summary = str("revanced_force_original_audio_not_available"); + super.setSummary(summary); + super.setSummaryOn(summary); + super.setSummaryOff(summary); + super.setEnabled(false); + } + } + + public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ForceOriginalAudioSwitchPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ForceOriginalAudioSwitchPreference(Context context) { + super(context); + } + + @Override + public void setEnabled(boolean enabled) { + if (!available) { + return; + } + + super.setEnabled(enabled); + } + + @Override + public void setSummary(CharSequence summary) { + if (!available) { + return; + } + + super.setSummary(summary); + } +} + diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java index a1d051c2b8..1044ba424e 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java @@ -1,19 +1,24 @@ package app.revanced.extension.shared.settings.preference; -import android.app.AlertDialog; +import static app.revanced.extension.shared.StringRef.str; + +import android.app.Dialog; import android.content.Context; import android.os.Build; +import android.os.Bundle; import android.preference.EditTextPreference; import android.preference.Preference; import android.text.InputType; import android.util.AttributeSet; -import android.util.TypedValue; +import android.util.Pair; +import android.view.inputmethod.InputMethodManager; import android.widget.EditText; -import app.revanced.extension.shared.settings.Setting; +import android.widget.LinearLayout; + import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; - -import static app.revanced.extension.shared.StringRef.str; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.ui.CustomDialog; @SuppressWarnings({"unused", "deprecation"}) public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { @@ -29,7 +34,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer editText.setAutofillHints((String) null); } editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); - editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap. + editText.setTextSize(14); setOnPreferenceClickListener(this); } @@ -54,7 +59,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer @Override public boolean onPreferenceClick(Preference preference) { try { - // Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened. + // Must set text before showing dialog, + // otherwise text is non-selectable if this preference is later reopened. existingSettings = Setting.exportToJson(getContext()); getEditText().setText(existingSettings); } catch (Exception ex) { @@ -64,18 +70,46 @@ public class ImportExportPreference extends EditTextPreference implements Prefer } @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + protected void showDialog(Bundle state) { try { - Utils.setEditTextDialogTheme(builder); + Context context = getContext(); + EditText editText = getEditText(); - // Show the user the settings in JSON format. - builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { - Utils.setClipboard(getEditText().getText().toString()); - }).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> { - importSettings(builder.getContext(), getEditText().getText().toString()); - }); + // Create a custom dialog with the EditText. + Pair dialogPair = CustomDialog.create( + context, + str("revanced_pref_import_export_title"), // Title. + null, // No message (EditText replaces it). + editText, // Pass the EditText. + str("revanced_settings_import"), // OK button text. + () -> importSettings(context, editText.getText().toString()), // OK button action. + () -> {}, // Cancel button action (dismiss only). + str("revanced_settings_import_copy"), // Neutral button (Copy) text. + () -> { + // Neutral button (Copy) action. Show the user the settings in JSON format. + Utils.setClipboard(editText.getText()); + }, + true // Dismiss dialog when onNeutralClick. + ); + + // If there are no settings yet, then show the on screen keyboard and bring focus to + // the edit text. This makes it easier to paste saved settings after a reinstall. + dialogPair.first.setOnShowListener(dialogInterface -> { + if (existingSettings.isEmpty()) { + editText.postDelayed(() -> { + editText.requestFocus(); + + InputMethodManager inputMethodManager = (InputMethodManager) + editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); + }, 100); + } + }); + + // Show the dialog. + dialogPair.first.show(); } catch (Exception ex) { - Logger.printException(() -> "onPrepareDialogBuilder failure", ex); + Logger.printException(() -> "showDialog failure", ex); } } @@ -88,7 +122,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings); if (rebootNeeded) { - AbstractPreferenceFragment.showRestartDialog(getContext()); + AbstractPreferenceFragment.showRestartDialog(context); } } catch (Exception ex) { Logger.printException(() -> "importSettings failure", ex); @@ -96,5 +130,4 @@ public class ImportExportPreference extends EditTextPreference implements Prefer AbstractPreferenceFragment.settingImportInProgress = false; } } - -} \ No newline at end of file +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java new file mode 100644 index 0000000000..4bd54c65be --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/LogBufferManager.java @@ -0,0 +1,113 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; + +import java.util.Deque; +import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BaseSettings; + +/** + * Manages a buffer for storing debug logs from {@link Logger}. + * Stores just under 1MB of the most recent log data. + * + * All methods are thread-safe. + */ +public final class LogBufferManager { + /** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */ + private static final int BUFFER_MAX_BYTES = 900_000; + /** Limit number of log lines. */ + private static final int BUFFER_MAX_SIZE = 10_000; + + private static final Deque logBuffer = new ConcurrentLinkedDeque<>(); + private static final AtomicInteger logBufferByteSize = new AtomicInteger(); + + /** + * Appends a log message to the internal buffer if debugging is enabled. + * The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE} + * to prevent excessive memory usage. + * + * @param message The log message to append. + */ + public static void appendToLogBuffer(String message) { + Objects.requireNonNull(message); + + // It's very important that no Settings are used in this method, + // as this code is used when a context is not set and thus referencing + // a setting will crash the app. + logBuffer.addLast(message); + int newSize = logBufferByteSize.addAndGet(message.length()); + + // Remove oldest entries if over the log size limits. + while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) { + String removed = logBuffer.pollFirst(); + if (removed == null) { + // Thread race of two different calls to this method, and the other thread won. + return; + } + + newSize = logBufferByteSize.addAndGet(-removed.length()); + } + } + + /** + * Exports all logs from the internal buffer to the clipboard. + * Displays a toast with the result. + */ + public static void exportToClipboard() { + try { + if (!BaseSettings.DEBUG.get()) { + Utils.showToastShort(str("revanced_debug_logs_disabled")); + return; + } + + if (logBuffer.isEmpty()) { + Utils.showToastShort(str("revanced_debug_logs_none_found")); + clearLogBufferData(); // Clear toast log entry that was just created. + return; + } + + // Most (but not all) Android 13+ devices always show a "copied to clipboard" toast + // and there is no way to programmatically detect if a toast will show or not. + // Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard). + Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard")); + + Utils.setClipboard(String.join("\n", logBuffer)); + } catch (Exception ex) { + // Handle security exception if clipboard access is denied. + String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage()); + Utils.showToastLong(errorMessage); + Logger.printDebug(() -> errorMessage, ex); + } + } + + private static void clearLogBufferData() { + // Cannot simply clear the log buffer because there is no + // write lock for both the deque and the atomic int. + // Instead pop off log entries and decrement the size one by one. + while (!logBuffer.isEmpty()) { + String removed = logBuffer.pollFirst(); + if (removed != null) { + logBufferByteSize.addAndGet(-removed.length()); + } + } + } + + /** + * Clears the internal log buffer and displays a toast with the result. + */ + public static void clearLogBuffer() { + if (!BaseSettings.DEBUG.get()) { + Utils.showToastShort(str("revanced_debug_logs_disabled")); + return; + } + + // Show toast before clearing, otherwise toast log will still remain. + Utils.showToastShort(str("revanced_debug_logs_clear_toast")); + clearLogBufferData(); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java new file mode 100644 index 0000000000..d6b895f22a --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/NoTitlePreferenceCategory.java @@ -0,0 +1,58 @@ +package app.revanced.extension.shared.settings.preference; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * Empty preference category with no title, used to organize and group related preferences together. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class NoTitlePreferenceCategory extends PreferenceCategory { + + public NoTitlePreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public NoTitlePreferenceCategory(Context context) { + super(context); + } + + @Override + @SuppressLint("MissingSuperCall") + protected View onCreateView(ViewGroup parent) { + // Return an zero-height view to eliminate empty title space. + return new View(getContext()); + } + + @Override + public CharSequence getTitle() { + // Title can be used for sorting. Return the first sub preference title. + if (getPreferenceCount() > 0) { + return getPreference(0).getTitle(); + } + + return super.getTitle(); + } + + @Override + public int getTitleRes() { + if (getPreferenceCount() > 0) { + return getPreference(0).getTitleRes(); + } + + return super.getTitleRes(); + } +} + diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java index cd62235fc7..0d4003b913 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java @@ -4,21 +4,25 @@ 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; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; -import android.content.res.Configuration; -import android.graphics.Color; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.Preference; import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; import android.view.Window; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -36,6 +40,7 @@ import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; import app.revanced.extension.shared.requests.Requester; import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.ui.Dim; /** * Opens a dialog showing official links. @@ -49,30 +54,6 @@ public class ReVancedAboutPreference extends Preference { return text.replace("-", "‑"); // #8209 = non breaking hyphen. } - private static String getColorHexString(int color) { - return String.format("#%06X", (0x00FFFFFF & color)); - } - - protected boolean isDarkModeEnabled() { - Configuration config = getContext().getResources().getConfiguration(); - final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; - return currentNightMode == Configuration.UI_MODE_NIGHT_YES; - } - - /** - * Subclasses can override this and provide a themed color. - */ - protected int getLightColor() { - return Color.WHITE; - } - - /** - * Subclasses can override this and provide a themed color. - */ - protected int getDarkColor() { - return Color.BLACK; - } - /** * Apps that do not support bundling resources must override this. * @@ -89,9 +70,8 @@ public class ReVancedAboutPreference extends Preference { builder.append(""); builder.append(""); - final boolean isDarkMode = isDarkModeEnabled(); - String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor()); - String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor()); + String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor()); + String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor()); // Apply light/dark mode colors. builder.append(String.format( "", @@ -146,6 +126,8 @@ public class ReVancedAboutPreference extends Preference { { setOnPreferenceClickListener(pref -> { + Context context = pref.getContext(); + // Show a progress spinner if the social links are not fetched yet. if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) { // Show a progress spinner, but only if the api fetch takes more than a half a second. @@ -158,17 +140,18 @@ public class ReVancedAboutPreference extends Preference { handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner); Utils.runOnBackgroundThread(() -> - fetchLinksAndShowDialog(handler, showDialogRunnable, progress)); + fetchLinksAndShowDialog(context, handler, showDialogRunnable, progress)); } else { // No network call required and can run now. - fetchLinksAndShowDialog(null, null, null); + fetchLinksAndShowDialog(context, null, null, null); } return false; }); } - private void fetchLinksAndShowDialog(@Nullable Handler handler, + private void fetchLinksAndShowDialog(Context context, + @Nullable Handler handler, Runnable showDialogRunnable, @Nullable ProgressDialog progress) { WebLink[] links = AboutLinksRoutes.fetchAboutLinks(); @@ -185,7 +168,17 @@ public class ReVancedAboutPreference extends Preference { if (handler != null) { handler.removeCallbacks(showDialogRunnable); } - if (progress != null) { + + // Don't continue if the activity is done. To test this tap the + // about dialog and immediately press back before the dialog can show. + if (context instanceof Activity activity) { + if (activity.isFinishing() || activity.isDestroyed()) { + Logger.printDebug(() -> "Not showing about dialog, activity is closed"); + return; + } + } + + if (progress != null && progress.isShowing()) { progress.dismiss(); } new WebViewDialog(getContext(), htmlDialog).show(); @@ -223,14 +216,37 @@ class WebViewDialog extends Dialog { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); + requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar. + // Create main layout. + LinearLayout mainLayout = new LinearLayout(getContext()); + mainLayout.setOrientation(LinearLayout.VERTICAL); + + mainLayout.setPadding(Dim.dp10, Dim.dp10, Dim.dp10, Dim.dp10); + // Set rounded rectangle background. + ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape( + Dim.roundedCorners(28), null, null)); + mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor()); + mainLayout.setBackground(mainBackground); + + // Create WebView. WebView webView = new WebView(getContext()); + webView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar. + webView.setOverScrollMode(View.OVER_SCROLL_NEVER); webView.getSettings().setJavaScriptEnabled(true); webView.setWebViewClient(new OpenLinksExternallyWebClient()); webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null); - setContentView(webView); + // Add WebView to layout. + mainLayout.addView(webView); + + setContentView(mainLayout); + + // Set dialog window attributes. + Window window = getWindow(); + if (window != null) { + Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false); + } } private class OpenLinksExternallyWebClient extends WebViewClient { @@ -318,7 +334,7 @@ class AboutLinksRoutes { // Do not show an exception toast if the server is down final int responseCode = connection.getResponseCode(); if (responseCode != 200) { - Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode); + Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode); return NO_CONNECTION_STATIC_LINKS; } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java index 3e9a969611..c6f323ceb4 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java @@ -1,24 +1,33 @@ package app.revanced.extension.shared.settings.preference; -import android.app.AlertDialog; +import static app.revanced.extension.shared.StringRef.str; + +import android.app.Dialog; import android.content.Context; import android.os.Bundle; import android.preference.EditTextPreference; import android.util.AttributeSet; -import android.widget.Button; +import android.util.Pair; import android.widget.EditText; +import android.widget.LinearLayout; -import app.revanced.extension.shared.Utils; -import app.revanced.extension.shared.settings.Setting; -import app.revanced.extension.shared.Logger; +import androidx.annotation.Nullable; import java.util.Objects; -import static app.revanced.extension.shared.StringRef.str; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.ui.CustomDialog; @SuppressWarnings({"unused", "deprecation"}) public class ResettableEditTextPreference extends EditTextPreference { + /** + * Setting to reset. + */ + @Nullable + private Setting setting; + public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @@ -32,36 +41,65 @@ public class ResettableEditTextPreference extends EditTextPreference { super(context); } - @Override - protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { - super.onPrepareDialogBuilder(builder); - Utils.setEditTextDialogTheme(builder); - - Setting setting = Setting.getSettingFromPath(getKey()); - if (setting != null) { - builder.setNeutralButton(str("revanced_settings_reset"), null); - } + public void setSetting(@Nullable Setting setting) { + this.setting = setting; } @Override protected void showDialog(Bundle state) { - super.showDialog(state); + try { + Context context = getContext(); + EditText editText = getEditText(); - // Override the button click listener to prevent dismissing the dialog. - Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL); - if (button == null) { - return; - } - button.setOnClickListener(v -> { - try { - Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); - String defaultStringValue = setting.defaultValue.toString(); - EditText editText = getEditText(); - editText.setText(defaultStringValue); - editText.setSelection(defaultStringValue.length()); // move cursor to end of text - } catch (Exception ex) { - Logger.printException(() -> "reset failure", ex); + // Resolve setting if not already set. + if (setting == null) { + String key = getKey(); + if (key != null) { + setting = Setting.getSettingFromPath(key); + } } - }); + + // Set initial EditText value to the current persisted value or empty string. + String initialValue = getText() != null ? getText() : ""; + editText.setText(initialValue); + editText.setSelection(initialValue.length()); // Move cursor to end. + + // Create custom dialog. + String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null; + Pair dialogPair = CustomDialog.create( + context, + getTitle() != null ? getTitle().toString() : "", // Title. + null, // Message is replaced by EditText. + editText, // Pass the EditText. + null, // OK button text. + () -> { + // OK button action. Persist the EditText value when OK is clicked. + String newValue = editText.getText().toString(); + if (callChangeListener(newValue)) { + setText(newValue); + } + }, + () -> {}, // Cancel button action (dismiss only). + neutralButtonText, // Neutral button text (Reset). + () -> { + // Neutral button action. + if (setting != null) { + try { + String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString(); + editText.setText(defaultStringValue); + editText.setSelection(defaultStringValue.length()); // Move cursor to end of text. + } catch (Exception ex) { + Logger.printException(() -> "reset failure", ex); + } + } + }, + false // Do not dismiss dialog when onNeutralClick. + ); + + // Show the dialog. + dialogPair.first.show(); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } } } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java index 4e9c1f2e0b..ed5db6b235 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java @@ -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())).apply(); + preferences.edit().putString(key, (value == null ? null : value.toString())).commit(); } /** * Removes any preference data type that has the specified key. */ public void removeKey(@NonNull String key) { - preferences.edit().remove(Objects.requireNonNull(key)).apply(); + preferences.edit().remove(Objects.requireNonNull(key)).commit(); } public void saveBoolean(@NonNull String key, boolean value) { - preferences.edit().putBoolean(key, value).apply(); + preferences.edit().putBoolean(key, value).commit(); } /** diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java new file mode 100644 index 0000000000..fb32e7bc07 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SortedListPreference.java @@ -0,0 +1,124 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import app.revanced.extension.shared.Utils; + +/** + * PreferenceList that sorts itself. + * By default the first entry is preserved in its original position, + * and all other entries are sorted alphabetically. + * + * Ideally the 'keep first entries to preserve' is an xml parameter, + * but currently that's not so simple since Extensions code cannot use + * generated code from the Patches repo (which is required for custom xml parameters). + * + * If any class wants to use a different getFirstEntriesToPreserve value, + * it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class SortedListPreference extends CustomDialogListPreference { + + /** + * Sorts the current list entries. + * + * @param firstEntriesToPreserve The number of entries to preserve in their original position, + * or a negative value to not sort and leave entries + * as they current are. + */ + public void sortEntryAndValues(int firstEntriesToPreserve) { + CharSequence[] entries = getEntries(); + CharSequence[] entryValues = getEntryValues(); + if (entries == null || entryValues == null) { + return; + } + + final int entrySize = entries.length; + if (entrySize != entryValues.length) { + // Xml array declaration has a missing/extra entry. + throw new IllegalStateException(); + } + + if (firstEntriesToPreserve < 0) { + return; // Nothing to do. + } + + List> firstEntries = new ArrayList<>(firstEntriesToPreserve); + + // Android does not have a triple class like Kotlin, So instead use a nested pair. + // Cannot easily use a SortedMap, because if two entries incorrectly have + // identical names then the duplicates entries are not preserved. + List>> lastEntries = new ArrayList<>(); + + for (int i = 0; i < entrySize; i++) { + Pair pair = new Pair<>(entries[i], entryValues[i]); + if (i < firstEntriesToPreserve) { + firstEntries.add(pair); + } else { + lastEntries.add(new Pair<>(Utils.removePunctuationToLowercase(pair.first), pair)); + } + } + + //noinspection ComparatorCombinators + Collections.sort(lastEntries, (pair1, pair2) + -> pair1.first.compareTo(pair2.first)); + + CharSequence[] sortedEntries = new CharSequence[entrySize]; + CharSequence[] sortedEntryValues = new CharSequence[entrySize]; + + int i = 0; + for (Pair pair : firstEntries) { + sortedEntries[i] = pair.first; + sortedEntryValues[i] = pair.second; + i++; + } + + for (Pair> outer : lastEntries) { + Pair inner = outer.second; + sortedEntries[i] = inner.first; + sortedEntryValues[i] = inner.second; + i++; + } + + super.setEntries(sortedEntries); + super.setEntryValues(sortedEntryValues); + } + + public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } + + public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } + + public SortedListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } + + public SortedListPreference(Context context) { + super(context); + + sortEntryAndValues(getFirstEntriesToPreserve()); + } + + /** + * @return The number of first entries to leave exactly where they are, and do not sort them. + * A negative value indicates do not sort any entries. + */ + protected int getFirstEntriesToPreserve() { + return 1; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java new file mode 100644 index 0000000000..8b1d8b882d --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ToolbarPreferenceFragment.java @@ -0,0 +1,173 @@ +package app.revanced.extension.shared.settings.preference; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.graphics.Insets; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowInsets; +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) +public class ToolbarPreferenceFragment extends AbstractPreferenceFragment { + + /** + * Removes the list of preferences from this fragment, if they exist. + * @param keys Preference keys. + */ + protected void removePreferences(String ... keys) { + for (String key : keys) { + Preference pref = findPreference(key); + if (pref != null) { + PreferenceGroup parent = pref.getParent(); + if (parent != null) { + Logger.printDebug(() -> "Removing preference: " + key); + parent.removePreference(pref); + } + } + } + } + + /** + * Sets toolbar for all nested preference screens. + */ + protected void setPreferenceScreenToolbar(PreferenceScreen parentScreen) { + for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) { + Preference childPreference = parentScreen.getPreference(i); + if (childPreference instanceof PreferenceScreen) { + // Recursively set sub preferences. + setPreferenceScreenToolbar((PreferenceScreen) childPreference); + + childPreference.setOnPreferenceClickListener( + childScreen -> { + Dialog preferenceScreenDialog = ((PreferenceScreen) childScreen).getDialog(); + ViewGroup rootView = (ViewGroup) preferenceScreenDialog + .findViewById(android.R.id.content) + .getParent(); + + // Allow package-specific background customization. + customizeDialogBackground(rootView); + + // Fix the system navigation bar color for submenus. + setNavigationBarColor(preferenceScreenDialog.getWindow()); + + // Fix edge-to-edge screen with Android 15 and YT 19.45+ + // https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + rootView.setOnApplyWindowInsetsListener((v, insets) -> { + Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars()); + Insets navInsets = insets.getInsets(WindowInsets.Type.navigationBars()); + Insets cutoutInsets = insets.getInsets(WindowInsets.Type.displayCutout()); + + // Apply padding for display cutout in landscape. + int leftPadding = cutoutInsets.left; + int rightPadding = cutoutInsets.right; + int topPadding = statusInsets.top; + int bottomPadding = navInsets.bottom; + + v.setPadding(leftPadding, topPadding, rightPadding, bottomPadding); + return insets; + }); + } + + Toolbar toolbar = new Toolbar(childScreen.getContext()); + toolbar.setTitle(childScreen.getTitle()); + toolbar.setNavigationIcon(getBackButtonDrawable()); + toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss()); + + toolbar.setTitleMargin(Dim.dp16, 0, Dim.dp16, 0); + + TextView toolbarTextView = Utils.getChildView(toolbar, + true, TextView.class::isInstance); + if (toolbarTextView != null) { + toolbarTextView.setTextColor(Utils.getAppForegroundColor()); + toolbarTextView.setTextSize(20); + } + + // Allow package-specific toolbar customization. + customizeToolbar(toolbar); + + // Allow package-specific post-toolbar setup. + onPostToolbarSetup(toolbar, preferenceScreenDialog); + + rootView.addView(toolbar, 0); + return false; + } + ); + } + } + } + + /** + * Sets the system navigation bar color for the activity. + * Applies the background color obtained from {@link Utils#getAppBackgroundColor()} to the navigation bar. + * For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility. + */ + public static void setNavigationBarColor(@Nullable Window window) { + if (window == null) { + Logger.printDebug(() -> "Cannot set navigation bar color, window is null"); + return; + } + + window.setNavigationBarColor(Utils.getAppBackgroundColor()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.setNavigationBarContrastEnforced(true); + } + } + + /** + * Returns the drawable for the back button. + */ + @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"); + Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource); + customizeBackButtonDrawable(drawable); + return drawable; + } + + /** + * Customizes the back button drawable. + */ + protected static void customizeBackButtonDrawable(Drawable drawable) { + drawable.setTint(Utils.getAppForegroundColor()); + } + + /** + * Allows subclasses to customize the dialog's root view background. + */ + protected void customizeDialogBackground(ViewGroup rootView) { + rootView.setBackgroundColor(Utils.getAppBackgroundColor()); + } + + /** + * Allows subclasses to customize the toolbar. + */ + protected void customizeToolbar(Toolbar toolbar) { + BaseActivityHook.setToolbarLayoutParams(toolbar); + } + + /** + * Allows subclasses to perform actions after toolbar setup. + */ + protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {} +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java new file mode 100644 index 0000000000..59f3077ceb --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/URLLinkPreference.java @@ -0,0 +1,44 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.Preference; +import android.util.AttributeSet; + +import app.revanced.extension.shared.Logger; + +/** + * Simple preference that opens a URL when clicked. + */ +@SuppressWarnings("deprecation") +public class URLLinkPreference extends Preference { + + protected String externalURL; + + { + setOnPreferenceClickListener(pref -> { + if (externalURL == null) { + Logger.printException(() -> "URL not set " + getClass().getSimpleName()); + return false; + } + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse(externalURL)); + pref.getContext().startActivity(i); + return true; + }); + } + + public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public URLLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public URLLinkPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public URLLinkPreference(Context context) { + super(context); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java new file mode 100644 index 0000000000..95731418d2 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultItem.java @@ -0,0 +1,373 @@ +package app.revanced.extension.shared.settings.search; + +import android.graphics.Color; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.SwitchPreference; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; + +import androidx.annotation.ColorInt; + +import java.util.ArrayList; +import java.util.Collections; +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; + +/** + * Abstract base class for search result items, defining common fields and behavior. + */ +public abstract class BaseSearchResultItem { + // Enum to represent view types. + public enum ViewType { + REGULAR, + SWITCH, + LIST, + COLOR_PICKER, + GROUP_HEADER, + NO_RESULTS, + URL_LINK; + + // 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"); + }; + } + + private static int getResourceIdentifier(String name) { + // Placeholder for actual resource identifier retrieval. + return Utils.getResourceIdentifierOrThrow(ResourceType.LAYOUT, name); + } + } + + final String navigationPath; + final List navigationKeys; + final ViewType preferenceType; + CharSequence highlightedTitle; + CharSequence highlightedSummary; + boolean highlightingApplied; + + BaseSearchResultItem(String navPath, List navKeys, ViewType type) { + this.navigationPath = navPath; + this.navigationKeys = new ArrayList<>(navKeys != null ? navKeys : Collections.emptyList()); + this.preferenceType = type; + this.highlightedTitle = ""; + this.highlightedSummary = ""; + this.highlightingApplied = false; + } + + abstract boolean matchesQuery(String query); + abstract void applyHighlighting(Pattern queryPattern); + abstract void clearHighlighting(); + + // Shared method for highlighting text with search query. + protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) { + if (TextUtils.isEmpty(text) || queryPattern == null) return text; + + final int adjustedColor = Utils.adjustColorBrightness( + Utils.getAppBackgroundColor(), 0.95f, 1.20f); + BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor); + SpannableStringBuilder spannable = new SpannableStringBuilder(text); + + Matcher matcher = queryPattern.matcher(text); + while (matcher.find()) { + int start = matcher.start(); + int end = matcher.end(); + if (start == end) continue; // Skip zero matches. + spannable.setSpan(highlightSpan, start, end, + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + return spannable; + } + + /** + * Search result item for group headers (navigation path only). + */ + public static class GroupHeaderItem extends BaseSearchResultItem { + GroupHeaderItem(String navPath, List navKeys) { + super(navPath, navKeys, ViewType.GROUP_HEADER); + this.highlightedTitle = navPath; + } + + @Override + boolean matchesQuery(String query) { + return false; // Headers are not directly searchable. + } + + @Override + void applyHighlighting(Pattern queryPattern) {} + + @Override + void clearHighlighting() {} + } + + /** + * Search result item for preferences, handling type-specific data and search text. + */ + @SuppressWarnings("deprecation") + public static class PreferenceSearchItem extends BaseSearchResultItem { + public final Preference preference; + final String searchableText; + final CharSequence originalTitle; + final CharSequence originalSummary; + final CharSequence originalSummaryOn; + final CharSequence originalSummaryOff; + final CharSequence[] originalEntries; + private CharSequence[] highlightedEntries; + private boolean entriesHighlightingApplied; + + @ColorInt + private int color; + + // Store last applied highlighting pattern to reapply when needed. + Pattern lastQueryPattern; + + PreferenceSearchItem(Preference pref, String navPath, List navKeys) { + super(navPath, navKeys, determineType(pref)); + this.preference = pref; + this.originalTitle = pref.getTitle() != null ? pref.getTitle() : ""; + this.originalSummary = pref.getSummary(); + this.highlightedTitle = this.originalTitle; + this.highlightedSummary = this.originalSummary != null ? this.originalSummary : ""; + this.color = 0; + this.lastQueryPattern = null; + + // Initialize type-specific fields. + FieldInitializationResult result = initTypeSpecificFields(pref); + this.originalSummaryOn = result.summaryOn; + this.originalSummaryOff = result.summaryOff; + this.originalEntries = result.entries; + + // Build searchable text. + this.searchableText = buildSearchableText(pref); + } + + private static class FieldInitializationResult { + CharSequence summaryOn = null; + CharSequence summaryOff = null; + CharSequence[] entries = null; + } + + private static ViewType determineType(Preference pref) { + 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 ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS; + return ViewType.REGULAR; + } + + private FieldInitializationResult initTypeSpecificFields(Preference pref) { + FieldInitializationResult result = new FieldInitializationResult(); + + if (pref instanceof SwitchPreference switchPref) { + result.summaryOn = switchPref.getSummaryOn(); + result.summaryOff = switchPref.getSummaryOff(); + } else if (pref instanceof ColorPickerPreference colorPref) { + String colorString = colorPref.getText(); + this.color = TextUtils.isEmpty(colorString) ? 0 : Color.parseColor(colorString); + } else if (pref instanceof ListPreference listPref) { + result.entries = listPref.getEntries(); + if (result.entries != null) { + this.highlightedEntries = new CharSequence[result.entries.length]; + System.arraycopy(result.entries, 0, this.highlightedEntries, 0, result.entries.length); + } + } + + this.entriesHighlightingApplied = false; + return result; + } + + private String buildSearchableText(Preference pref) { + StringBuilder searchBuilder = new StringBuilder(); + String key = pref.getKey(); + String normalizedKey = ""; + if (key != null) { + // Normalize preference key by removing the common "revanced_" prefix + // so that users can search by the meaningful part only. + normalizedKey = key.startsWith("revanced_") + ? key.substring("revanced_".length()) + : key; + } + appendText(searchBuilder, normalizedKey); + appendText(searchBuilder, originalTitle); + appendText(searchBuilder, originalSummary); + + // Add type-specific searchable content. + if (pref instanceof ListPreference) { + if (originalEntries != null) { + for (CharSequence entry : originalEntries) { + appendText(searchBuilder, entry); + } + } + } else if (pref instanceof SwitchPreference) { + appendText(searchBuilder, originalSummaryOn); + appendText(searchBuilder, originalSummaryOff); + } else if (pref instanceof ColorPickerPreference) { + appendText(searchBuilder, ColorPickerPreference.getColorString(color, false)); + } + + // Include navigation path in searchable text. + appendText(searchBuilder, navigationPath); + + return searchBuilder.toString(); + } + + /** + * Appends normalized searchable text to the builder. + * Uses full Unicode normalization for accurate search across all languages. + */ + private void appendText(StringBuilder builder, CharSequence text) { + if (!TextUtils.isEmpty(text)) { + if (builder.length() > 0) builder.append(" "); + builder.append(Utils.normalizeTextToLowercase(text)); + } + } + + /** + * Gets the current effective summary for this preference, considering state-dependent summaries. + */ + public CharSequence getCurrentEffectiveSummary() { + if (preference instanceof CustomDialogListPreference customPref) { + String staticSum = customPref.getStaticSummary(); + if (staticSum != null) { + return staticSum; + } + } + if (preference instanceof SwitchPreference switchPref) { + boolean currentState = switchPref.isChecked(); + return currentState + ? (originalSummaryOn != null ? originalSummaryOn : + originalSummary != null ? originalSummary : "") + : (originalSummaryOff != null ? originalSummaryOff : + originalSummary != null ? originalSummary : ""); + } else if (preference instanceof ListPreference listPref) { + String value = listPref.getValue(); + CharSequence[] entries = listPref.getEntries(); + CharSequence[] entryValues = listPref.getEntryValues(); + if (value != null && entries != null && entryValues != null) { + for (int i = 0, length = entries.length; i < length; i++) { + if (value.equals(entryValues[i].toString())) { + return originalEntries != null && i < originalEntries.length && originalEntries[i] != null + ? originalEntries[i] + : originalSummary != null ? originalSummary : ""; + } + } + } + return originalSummary != null ? originalSummary : ""; + } + return originalSummary != null ? originalSummary : ""; + } + + /** + * Checks if this search result item matches the provided query. + * Uses case-insensitive matching against the searchable text. + */ + @Override + boolean matchesQuery(String query) { + return searchableText.contains(Utils.normalizeTextToLowercase(query)); + } + + /** + * Get highlighted entries to show in dialog. + */ + public CharSequence[] getHighlightedEntries() { + return highlightedEntries; + } + + /** + * Whether highlighting is applied to entries. + */ + public boolean isEntriesHighlightingApplied() { + return entriesHighlightingApplied; + } + + /** + * Highlights the search query in the title and summary. + */ + @Override + void applyHighlighting(Pattern queryPattern) { + this.lastQueryPattern = queryPattern; + // Highlight the title. + highlightedTitle = highlightSearchQuery(originalTitle, queryPattern); + + // Get the current effective summary and highlight it. + CharSequence currentSummary = getCurrentEffectiveSummary(); + highlightedSummary = highlightSearchQuery(currentSummary, queryPattern); + + // Highlight the entries. + if (preference instanceof ListPreference && originalEntries != null) { + highlightedEntries = new CharSequence[originalEntries.length]; + for (int i = 0, length = originalEntries.length; i < length; i++) { + if (originalEntries[i] != null) { + highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern); + } else { + highlightedEntries[i] = null; + } + } + entriesHighlightingApplied = true; + } + + highlightingApplied = true; + } + + /** + * Clears all search query highlighting and restores original state completely. + */ + @Override + void clearHighlighting() { + if (!highlightingApplied) return; + + // Restore original title. + highlightedTitle = originalTitle; + + // Restore current effective summary without highlighting. + highlightedSummary = getCurrentEffectiveSummary(); + + // Restore original entries. + if (originalEntries != null && highlightedEntries != null) { + System.arraycopy(originalEntries, 0, highlightedEntries, 0, + Math.min(originalEntries.length, highlightedEntries.length)); + } + + entriesHighlightingApplied = false; + highlightingApplied = false; + lastQueryPattern = null; + } + + /** + * Refreshes highlighting for dynamic summaries (like switch preferences). + * Should be called when the preference state changes. + */ + public void refreshHighlighting() { + if (highlightingApplied && lastQueryPattern != null) { + CharSequence currentSummary = getCurrentEffectiveSummary(); + highlightedSummary = highlightSearchQuery(currentSummary, lastQueryPattern); + } + } + + public void setColor(int newColor) { + this.color = newColor; + } + + @ColorInt + public int getColor() { + return color; + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java new file mode 100644 index 0000000000..04d69c6b6b --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchResultsAdapter.java @@ -0,0 +1,621 @@ +package app.revanced.extension.shared.settings.search; + +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; + +import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.Switch; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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.ui.ColorDot; + +/** + * Abstract adapter for displaying search results in overlay ListView with ViewHolder pattern. + */ +@SuppressWarnings("deprecation") +public abstract class BaseSearchResultsAdapter extends ArrayAdapter { + protected final LayoutInflater inflater; + protected final BaseSearchViewController.BasePreferenceFragment fragment; + protected final BaseSearchViewController searchViewController; + protected AnimatorSet currentAnimator; + protected abstract PreferenceScreen getMainPreferenceScreen(); + + protected static final int BLINK_DURATION = 400; + protected static final int PAUSE_BETWEEN_BLINKS = 100; + + protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow( + ResourceType.ID, "preference_title"); + protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow( + ResourceType.ID, "preference_summary"); + protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow( + ResourceType.ID, "preference_path"); + protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow( + ResourceType.ID, "preference_switch"); + protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow( + ResourceType.ID, "preference_color_dot"); + + protected static class RegularViewHolder { + TextView titleView; + TextView summaryView; + } + + protected static class SwitchViewHolder { + TextView titleView; + TextView summaryView; + Switch switchWidget; + } + + protected static class ColorViewHolder { + TextView titleView; + TextView summaryView; + View colorDot; + } + + protected static class GroupHeaderViewHolder { + TextView pathView; + } + + protected static class NoResultsViewHolder { + TextView titleView; + TextView summaryView; + ImageView iconView; + } + + public BaseSearchResultsAdapter(Context context, List items, + BaseSearchViewController.BasePreferenceFragment fragment, + BaseSearchViewController searchViewController) { + super(context, 0, items); + this.inflater = LayoutInflater.from(context); + this.fragment = fragment; + this.searchViewController = searchViewController; + } + + @Override + public int getItemViewType(int position) { + BaseSearchResultItem item = getItem(position); + return item == null ? 0 : item.preferenceType.ordinal(); + } + + @Override + public int getViewTypeCount() { + return BaseSearchResultItem.ViewType.values().length; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + BaseSearchResultItem item = getItem(position); + if (item == null) return new View(getContext()); + // Use the ViewType enum. + BaseSearchResultItem.ViewType viewType = item.preferenceType; + // Create or reuse preference view based on type. + return createPreferenceView(item, convertView, viewType, parent); + } + + @Override + public boolean isEnabled(int position) { + BaseSearchResultItem item = getItem(position); + // Disable for NO_RESULTS items to prevent ripple/selection. + return item != null && item.preferenceType != BaseSearchResultItem.ViewType.NO_RESULTS; + } + + /** + * Creates or reuses a view for the given SearchResultItem. + *

+ * Thanks to {@link #getItemViewType(int)} and {@link #getViewTypeCount()}, ListView knows + * how many different row types exist and keeps a separate "recycling pool" for each. + * That means convertView passed here is ALWAYS of the correct type for this position. + * So only need to check if (view == null), and if so – inflate a new layout and create the proper ViewHolder. + */ + protected View createPreferenceView(BaseSearchResultItem item, View convertView, + BaseSearchResultItem.ViewType viewType, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = inflateViewForType(viewType, parent); + createViewHolderForType(view, viewType); + } + + // Retrieve the cached ViewHolder. + Object holder = view.getTag(); + bindDataToViewHolder(item, holder, viewType, view); + return view; + } + + protected View inflateViewForType(BaseSearchResultItem.ViewType viewType, ViewGroup parent) { + return inflater.inflate(viewType.getLayoutResourceId(), parent, false); + } + + protected void createViewHolderForType(View view, BaseSearchResultItem.ViewType viewType) { + switch (viewType) { + case REGULAR, LIST, URL_LINK -> { + RegularViewHolder regularHolder = new RegularViewHolder(); + regularHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE); + regularHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY); + view.setTag(regularHolder); + } + case SWITCH -> { + SwitchViewHolder switchHolder = new SwitchViewHolder(); + switchHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE); + switchHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY); + switchHolder.switchWidget = view.findViewById(ID_PREFERENCE_SWITCH); + view.setTag(switchHolder); + } + case COLOR_PICKER -> { + ColorViewHolder colorHolder = new ColorViewHolder(); + colorHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE); + colorHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY); + colorHolder.colorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT); + view.setTag(colorHolder); + } + case GROUP_HEADER -> { + GroupHeaderViewHolder groupHolder = new GroupHeaderViewHolder(); + groupHolder.pathView = view.findViewById(ID_PREFERENCE_PATH); + view.setTag(groupHolder); + } + case NO_RESULTS -> { + NoResultsViewHolder noResultsHolder = new NoResultsViewHolder(); + noResultsHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE); + noResultsHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY); + noResultsHolder.iconView = view.findViewById(android.R.id.icon); + view.setTag(noResultsHolder); + } + default -> throw new IllegalStateException("Unknown viewType: " + viewType); + } + } + + protected void bindDataToViewHolder(BaseSearchResultItem item, Object holder, + BaseSearchResultItem.ViewType viewType, View view) { + switch (viewType) { + case REGULAR, URL_LINK, LIST -> bindRegularViewHolder(item, (RegularViewHolder) holder, view); + case SWITCH -> bindSwitchViewHolder(item, (SwitchViewHolder) holder, view); + case COLOR_PICKER -> bindColorViewHolder(item, (ColorViewHolder) holder, view); + case GROUP_HEADER -> bindGroupHeaderViewHolder(item, (GroupHeaderViewHolder) holder, view); + case NO_RESULTS -> bindNoResultsViewHolder(item, (NoResultsViewHolder) holder); + default -> throw new IllegalStateException("Unknown viewType: " + viewType); + } + } + + protected void bindRegularViewHolder(BaseSearchResultItem item, RegularViewHolder holder, View view) { + BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item; + prefItem.refreshHighlighting(); + holder.titleView.setText(item.highlightedTitle); + holder.summaryView.setText(item.highlightedSummary); + holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE); + setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference, + () -> { + handlePreferenceClick(prefItem.preference); + if (prefItem.preference instanceof ListPreference) { + prefItem.refreshHighlighting(); + holder.summaryView.setText(prefItem.getCurrentEffectiveSummary()); + holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE); + notifyDataSetChanged(); + } + }, + () -> navigateAndScrollToPreference(item)); + } + + protected void bindSwitchViewHolder(BaseSearchResultItem item, SwitchViewHolder holder, View view) { + BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item; + SwitchPreference switchPref = (SwitchPreference) prefItem.preference; + holder.titleView.setText(item.highlightedTitle); + holder.switchWidget.setBackground(null); // Remove ripple/highlight. + // Sync switch state with preference without animation. + boolean currentState = switchPref.isChecked(); + if (holder.switchWidget.isChecked() != currentState) { + holder.switchWidget.setChecked(currentState); + holder.switchWidget.jumpDrawablesToCurrentState(); + } + prefItem.refreshHighlighting(); + holder.summaryView.setText(prefItem.highlightedSummary); + holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE); + setupPreferenceView(view, holder.titleView, holder.summaryView, switchPref, + () -> { + boolean newState = !switchPref.isChecked(); + switchPref.setChecked(newState); + holder.switchWidget.setChecked(newState); + prefItem.refreshHighlighting(); + holder.summaryView.setText(prefItem.getCurrentEffectiveSummary()); + holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE); + if (switchPref.getOnPreferenceChangeListener() != null) { + switchPref.getOnPreferenceChangeListener().onPreferenceChange(switchPref, newState); + } + notifyDataSetChanged(); + }, + () -> navigateAndScrollToPreference(item)); + holder.switchWidget.setEnabled(switchPref.isEnabled()); + } + + protected void bindColorViewHolder(BaseSearchResultItem item, ColorViewHolder holder, View view) { + BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item; + holder.titleView.setText(item.highlightedTitle); + holder.summaryView.setText(item.highlightedSummary); + holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE); + ColorDot.applyColorDot(holder.colorDot, prefItem.getColor(), prefItem.preference.isEnabled()); + setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference, + () -> handlePreferenceClick(prefItem.preference), + () -> navigateAndScrollToPreference(item)); + } + + protected void bindGroupHeaderViewHolder(BaseSearchResultItem item, GroupHeaderViewHolder holder, View view) { + holder.pathView.setText(item.highlightedTitle); + view.setOnClickListener(v -> navigateToTargetScreen(item)); + } + + protected void bindNoResultsViewHolder(BaseSearchResultItem item, NoResultsViewHolder holder) { + 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()); + } + + /** + * Sets up a preference view with click listeners and proper enabled state handling. + */ + protected void setupPreferenceView(View view, TextView titleView, TextView summaryView, Preference preference, + Runnable onClickAction, Runnable onLongClickAction) { + boolean enabled = preference.isEnabled(); + + // To enable long-click navigation for disabled settings, manually control the enabled state of the title + // and summary and disable the ripple effect instead of using 'view.setEnabled(enabled)'. + + titleView.setEnabled(enabled); + summaryView.setEnabled(enabled); + + if (!enabled) view.setBackground(null); // Disable ripple effect. + + // In light mode, alpha 0.5 is applied to a disabled title automatically, + // but in dark mode it needs to be applied manually. + if (Utils.isDarkModeEnabled()) { + titleView.setAlpha(enabled ? 1.0f : ColorPickerPreference.DISABLED_ALPHA); + } + // Set up click and long-click listeners. + view.setOnClickListener(enabled ? v -> onClickAction.run() : null); + view.setOnLongClickListener(v -> { + onLongClickAction.run(); + return true; + }); + } + + /** + * Navigates to the settings screen containing the given search result item and triggers scrolling. + */ + protected void navigateAndScrollToPreference(BaseSearchResultItem item) { + // No navigation for URL_LINK items. + if (item.preferenceType == BaseSearchResultItem.ViewType.URL_LINK) return; + + PreferenceScreen targetScreen = navigateToTargetScreen(item); + if (targetScreen == null) return; + if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) return; + + Preference targetPreference = prefItem.preference; + + fragment.getView().post(() -> { + ListView listView = targetScreen == getMainPreferenceScreen() + ? getPreferenceListView() + : targetScreen.getDialog().findViewById(android.R.id.list); + + if (listView == null) return; + + int targetPosition = findPreferencePosition(targetPreference, listView); + if (targetPosition == -1) return; + + int firstVisible = listView.getFirstVisiblePosition(); + int lastVisible = listView.getLastVisiblePosition(); + + if (targetPosition >= firstVisible && targetPosition <= lastVisible) { + // The preference is already visible, but still scroll it to the bottom of the list for consistency. + View child = listView.getChildAt(targetPosition - firstVisible); + if (child != null) { + // Calculate how much to scroll so the item is aligned at the bottom. + int scrollAmount = child.getBottom() - listView.getHeight(); + if (scrollAmount > 0) { + // Perform smooth scroll animation for better user experience. + listView.smoothScrollBy(scrollAmount, 300); + } + } + // Highlight the preference once it is positioned. + highlightPreferenceAtPosition(listView, targetPosition); + } else { + // The preference is outside of the current visible range, scroll to it from the top. + listView.smoothScrollToPositionFromTop(targetPosition, 0); + + Handler handler = new Handler(Looper.getMainLooper()); + // Fallback runnable in case the OnScrollListener does not trigger. + Runnable fallback = () -> { + listView.setOnScrollListener(null); + highlightPreferenceAtPosition(listView, targetPosition); + }; + // Post fallback with a small delay. + handler.postDelayed(fallback, 350); + + listView.setOnScrollListener(new AbsListView.OnScrollListener() { + private boolean isScrolling = false; + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == SCROLL_STATE_TOUCH_SCROLL || scrollState == SCROLL_STATE_FLING) { + // Mark that scrolling has started. + isScrolling = true; + } + if (scrollState == SCROLL_STATE_IDLE && isScrolling) { + // Scrolling is finished, cleanup listener and cancel fallback. + isScrolling = false; + listView.setOnScrollListener(null); + handler.removeCallbacks(fallback); + // Highlight the target preference when scrolling is done. + highlightPreferenceAtPosition(listView, targetPosition); + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} + }); + } + }); + } + + /** + * Navigates to the final PreferenceScreen using preference keys or titles as fallback. + */ + protected PreferenceScreen navigateToTargetScreen(BaseSearchResultItem item) { + PreferenceScreen currentScreen = getMainPreferenceScreen(); + Preference targetPref = null; + + // Try key-based navigation first. + if (item.navigationKeys != null && !item.navigationKeys.isEmpty()) { + String finalKey = item.navigationKeys.get(item.navigationKeys.size() - 1); + targetPref = findPreferenceByKey(currentScreen, finalKey); + } + + // Fallback to title-based navigation. + if (targetPref == null && !TextUtils.isEmpty(item.navigationPath)) { + String[] pathSegments = item.navigationPath.split(" > "); + String finalSegment = pathSegments[pathSegments.length - 1].trim(); + if (!TextUtils.isEmpty(finalSegment)) { + targetPref = findPreferenceByTitle(currentScreen, finalSegment); + } + } + + if (targetPref instanceof PreferenceScreen targetScreen) { + handlePreferenceClick(targetScreen); + return targetScreen; + } + + return currentScreen; + } + + /** + * Recursively searches for a preference by title in a preference group. + */ + protected Preference findPreferenceByTitle(PreferenceGroup group, String title) { + for (int i = 0; i < group.getPreferenceCount(); i++) { + Preference pref = group.getPreference(i); + CharSequence prefTitle = pref.getTitle(); + if (prefTitle != null && (prefTitle.toString().trim().equalsIgnoreCase(title) + || normalizeString(prefTitle.toString()).equals(normalizeString(title)))) { + return pref; + } + if (pref instanceof PreferenceGroup) { + Preference found = findPreferenceByTitle((PreferenceGroup) pref, title); + if (found != null) { + return found; + } + } + } + return null; + } + + /** + * Normalizes string for comparison (removes extra characters, spaces etc.). + */ + protected String normalizeString(String input) { + if (TextUtils.isEmpty(input)) return ""; + return input.trim().toLowerCase().replaceAll("\\s+", " ").replaceAll("[^\\w\\s]", ""); + } + + /** + * Gets the ListView from the PreferenceFragment. + */ + protected ListView getPreferenceListView() { + View fragmentView = fragment.getView(); + if (fragmentView != null) { + ListView listView = findListViewInViewGroup(fragmentView); + if (listView != null) { + return listView; + } + } + return fragment.getActivity().findViewById(android.R.id.list); + } + + /** + * Recursively searches for a ListView in a ViewGroup. + */ + protected ListView findListViewInViewGroup(View view) { + if (view instanceof ListView) { + return (ListView) view; + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + ListView result = findListViewInViewGroup(group.getChildAt(i)); + if (result != null) { + return result; + } + } + } + return null; + } + + /** + * Finds the position of a preference in the ListView adapter. + */ + protected int findPreferencePosition(Preference targetPreference, ListView listView) { + ListAdapter adapter = listView.getAdapter(); + if (adapter == null) { + return -1; + } + + for (int i = 0, count = adapter.getCount(); i < count; i++) { + Object item = adapter.getItem(i); + if (item == targetPreference) { + return i; + } + if (item instanceof Preference pref && targetPreference.getKey() != null) { + if (targetPreference.getKey().equals(pref.getKey())) { + return i; + } + } + } + return -1; + } + + /** + * Highlights a preference at the specified position with a blink effect. + */ + protected void highlightPreferenceAtPosition(ListView listView, int position) { + int firstVisible = listView.getFirstVisiblePosition(); + if (position < firstVisible || position > listView.getLastVisiblePosition()) { + return; + } + + View itemView = listView.getChildAt(position - firstVisible); + if (itemView != null) { + blinkView(itemView); + } + } + + /** + * Creates a smooth double-blink effect on a view's background without affecting the text. + * @param view The View to apply the animation to. + */ + protected void blinkView(View view) { + // If a previous animation is still running, cancel it to prevent conflicts. + if (currentAnimator != null && currentAnimator.isRunning()) { + currentAnimator.cancel(); + } + final int startColor = Utils.getAppBackgroundColor(); + final int highlightColor = Utils.adjustColorBrightness( + startColor, + Utils.isDarkModeEnabled() ? 1.25f : 0.8f + ); + // Animator for transitioning from the start color to the highlight color. + ObjectAnimator fadeIn = ObjectAnimator.ofObject( + view, + "backgroundColor", + new ArgbEvaluator(), + startColor, + highlightColor + ); + fadeIn.setDuration(BLINK_DURATION); + // Animator to return to the start color. + ObjectAnimator fadeOut = ObjectAnimator.ofObject( + view, + "backgroundColor", + new ArgbEvaluator(), + highlightColor, + startColor + ); + fadeOut.setDuration(BLINK_DURATION); + + currentAnimator = new AnimatorSet(); + // Create the sequence: fadeIn -> fadeOut -> (pause) -> fadeIn -> fadeOut. + AnimatorSet firstBlink = new AnimatorSet(); + firstBlink.playSequentially(fadeIn, fadeOut); + AnimatorSet secondBlink = new AnimatorSet(); + secondBlink.playSequentially(fadeIn.clone(), fadeOut.clone()); // Use clones for the second blink. + + currentAnimator.play(secondBlink).after(firstBlink).after(PAUSE_BETWEEN_BLINKS); + currentAnimator.start(); + } + + /** + * Recursively finds a preference by key in a preference group. + */ + protected Preference findPreferenceByKey(PreferenceGroup group, String key) { + if (group == null || TextUtils.isEmpty(key)) { + return null; + } + + // First search on current level. + for (int i = 0, count = group.getPreferenceCount(); i < count; i++) { + Preference pref = group.getPreference(i); + if (key.equals(pref.getKey())) { + return pref; + } + if (pref instanceof PreferenceGroup) { + Preference found = findPreferenceByKey((PreferenceGroup) pref, key); + if (found != null) { + return found; + } + } + } + return null; + } + + /** + * Handles preference click actions by invoking the preference's performClick method via reflection. + */ + @SuppressWarnings("all") + private void handlePreferenceClick(Preference preference) { + try { + if (preference instanceof CustomDialogListPreference listPref) { + BaseSearchResultItem.PreferenceSearchItem searchItem = + searchViewController.findSearchItemByPreference(preference); + if (searchItem != null && searchItem.isEntriesHighlightingApplied()) { + listPref.setHighlightedEntriesForDialog(searchItem.getHighlightedEntries()); + } + } + + Method m = Preference.class.getDeclaredMethod("performClick", PreferenceScreen.class); + m.setAccessible(true); + m.invoke(preference, fragment.getPreferenceScreenForSearch()); + } catch (Exception e) { + Logger.printException(() -> "Failed to invoke performClick()", e); + } + } + + /** + * Checks if a preference has navigation capability (can open a new screen). + */ + 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; + // Other group types that might have their own screens. + if (preference instanceof PreferenceGroup) { + // Check if it has its own fragment or intent. + return preference.getIntent() != null || preference.getFragment() != null; + } + return false; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java new file mode 100644 index 0000000000..3be942f6f6 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/BaseSearchViewController.java @@ -0,0 +1,704 @@ +package app.revanced.extension.shared.settings.search; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.os.Build; +import android.preference.Preference; +import android.preference.PreferenceCategory; +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; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.Toolbar; + +import androidx.annotation.ColorInt; +import androidx.annotation.RequiresApi; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +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; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.preference.ColorPickerPreference; +import app.revanced.extension.shared.settings.preference.CustomDialogListPreference; +import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory; +import app.revanced.extension.shared.ui.Dim; + +/** + * Abstract controller for managing the overlay search view in ReVanced settings. + * Subclasses must implement app-specific preference handling. + */ +@SuppressWarnings("deprecation") +public abstract class BaseSearchViewController { + protected SearchView searchView; + protected FrameLayout searchContainer; + protected FrameLayout overlayContainer; + protected final Toolbar toolbar; + protected final Activity activity; + protected final BasePreferenceFragment fragment; + protected final CharSequence originalTitle; + protected BaseSearchResultsAdapter searchResultsAdapter; + protected final List allSearchItems; + protected final List filteredSearchItems; + protected final Map keyToSearchItem; + protected final InputMethodManager inputMethodManager; + protected SearchHistoryManager searchHistoryManager; + protected boolean isSearchActive; + protected boolean isShowingSearchHistory; + + 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; + } + + /** + * Constructs a new BaseSearchViewController instance. + * + * @param activity The activity hosting the search view. + * @param toolbar The toolbar containing the search action. + * @param fragment The preference fragment to manage search preferences. + */ + protected BaseSearchViewController(Activity activity, Toolbar toolbar, BasePreferenceFragment fragment) { + this.activity = activity; + this.toolbar = toolbar; + this.fragment = fragment; + this.originalTitle = toolbar.getTitle(); + this.allSearchItems = new ArrayList<>(); + this.filteredSearchItems = new ArrayList<>(); + this.keyToSearchItem = new HashMap<>(); + this.inputMethodManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + this.isShowingSearchHistory = false; + + // Initialize components + initializeSearchView(); + initializeOverlayContainer(); + initializeSearchHistoryManager(); + setupToolbarMenu(); + setupListeners(); + } + + /** + * Initializes the search view with proper configurations, such as background, query hint, and RTL support. + */ + private void initializeSearchView() { + // 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")); + // Disable fullscreen keyboard mode. + searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI); + + searchContainer = activity.findViewById(ID_REVANCED_SEARCH_VIEW_CONTAINER); + + // Set background and query hint. + searchView.setBackground(createBackgroundDrawable()); + searchView.setQueryHint(str("revanced_settings_search_hint")); + + // Set text size. + searchEditText.setTextSize(16); + + // Set cursor color. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + setCursorColor(searchEditText); + } + + // Configure RTL support based on app language. + AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get(); + if (Utils.isRightToLeftLocale(appLanguage.getLocale())) { + searchView.setTextDirection(View.TEXT_DIRECTION_RTL); + searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); + } + } + + /** + * Sets the cursor color (for Android 10+ devices). + */ + @RequiresApi(api = Build.VERSION_CODES.Q) + private void setCursorColor(EditText editText) { + // Get the cursor color based on the current theme. + final int cursorColor = Utils.isDarkModeEnabled() ? Color.WHITE : Color.BLACK; + + // Create cursor drawable. + GradientDrawable cursorDrawable = new GradientDrawable(); + cursorDrawable.setShape(GradientDrawable.RECTANGLE); + cursorDrawable.setSize(Dim.dp2, -1); // Width: 2dp, Height: match text height. + cursorDrawable.setColor(cursorColor); + + // Set cursor drawable. + editText.setTextCursorDrawable(cursorDrawable); + } + + /** + * Initializes the overlay container for displaying search results and history. + */ + private void initializeOverlayContainer() { + // Create overlay container for search results and history. + overlayContainer = new FrameLayout(activity); + overlayContainer.setVisibility(View.GONE); + overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor()); + overlayContainer.setElevation(Dim.dp8); + + // Container for search results. + FrameLayout searchResultsContainer = new FrameLayout(activity); + searchResultsContainer.setVisibility(View.VISIBLE); + + // Create a ListView for the results. + ListView searchResultsListView = new ListView(activity); + searchResultsListView.setDivider(null); + searchResultsListView.setDividerHeight(0); + searchResultsAdapter = createSearchResultsAdapter(); + searchResultsListView.setAdapter(searchResultsAdapter); + + // Add results list into container. + searchResultsContainer.addView(searchResultsListView, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // Add results container into overlay. + overlayContainer.addView(searchResultsContainer, new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + + // Add overlay to the main content container. + FrameLayout mainContainer = activity.findViewById(ID_REVANCED_SETTINGS_FRAGMENTS); + if (mainContainer != null) { + FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT); + overlayParams.gravity = Gravity.TOP; + mainContainer.addView(overlayContainer, overlayParams); + } + } + + /** + * Initializes the search history manager with the specified overlay container and listener. + */ + private void initializeSearchHistoryManager() { + searchHistoryManager = new SearchHistoryManager(activity, overlayContainer, query -> { + searchView.setQuery(query, true); + hideSearchHistory(); + }); + } + + // Abstract methods that subclasses must implement. + protected abstract BaseSearchResultsAdapter createSearchResultsAdapter(); + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + protected abstract boolean isSpecialPreferenceGroup(Preference preference); + protected abstract void setupSpecialPreferenceListeners(BaseSearchResultItem item); + + // Abstract interface for preference fragments. + public interface BasePreferenceFragment { + PreferenceScreen getPreferenceScreenForSearch(); + android.view.View getView(); + Activity getActivity(); + } + + /** + * Determines whether a preference should be included in the search index. + * + * @param preference The preference to evaluate. + * @param currentDepth The current depth in the preference hierarchy. + * @param includeDepth The maximum depth to include in the search index. + * @return True if the preference should be included, false otherwise. + */ + protected boolean shouldIncludePreference(Preference preference, int currentDepth, int includeDepth) { + return includeDepth <= currentDepth + && !(preference instanceof PreferenceCategory) + && !isSpecialPreferenceGroup(preference) + && !(preference instanceof PreferenceScreen); + } + + /** + * Sets up the toolbar menu for the search action. + */ + protected void setupToolbarMenu() { + toolbar.inflateMenu(MENU_REVANCED_SEARCH_MENU); + toolbar.setOnMenuItemClickListener(item -> { + if (item.getItemId() == ID_ACTION_SEARCH && !isSearchActive) { + openSearch(); + return true; + } + return false; + }); + + // Set bold icon if needed. + MenuItem search = toolbar.getMenu().findItem(ID_ACTION_SEARCH); + search.setIcon(getSearchIcon()); + } + + /** + * Configures listeners for the search view and toolbar navigation. + */ + protected void setupListeners() { + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + try { + String queryTrimmed = query.trim(); + if (!queryTrimmed.isEmpty()) { + searchHistoryManager.saveSearchQuery(queryTrimmed); + } + } catch (Exception ex) { + Logger.printException(() -> "onQueryTextSubmit failure", ex); + } + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + try { + Logger.printDebug(() -> "Search query: " + newText); + + String trimmedText = newText.trim(); + if (!isSearchActive) { + Logger.printDebug(() -> "Search is not active, skipping query processing"); + return true; + } + + if (trimmedText.isEmpty()) { + // If empty query: show history. + hideSearchResults(); + showSearchHistory(); + } else { + // If has search text: hide history and show search results. + hideSearchHistory(); + filterAndShowResults(newText); + } + } catch (Exception ex) { + Logger.printException(() -> "onQueryTextChange failure", ex); + } + return true; + } + }); + // Set navigation click listener. + toolbar.setNavigationOnClickListener(view -> { + if (isSearchActive) { + closeSearch(); + } else { + activity.finish(); + } + }); + } + + /** + * Initializes search data by collecting all searchable preferences from the fragment. + * This method should be called after the preference fragment is fully loaded. + * Runs on the UI thread to ensure proper access to preference components. + */ + public void initializeSearchData() { + allSearchItems.clear(); + keyToSearchItem.clear(); + // Wait until fragment is properly initialized. + activity.runOnUiThread(() -> { + try { + PreferenceScreen screen = fragment.getPreferenceScreenForSearch(); + if (screen != null) { + collectSearchablePreferences(screen); + for (BaseSearchResultItem item : allSearchItems) { + if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) { + String key = prefItem.preference.getKey(); + if (key != null) { + keyToSearchItem.put(key, item); + } + } + } + setupPreferenceListeners(); + Logger.printDebug(() -> "Collected " + allSearchItems.size() + " searchable preferences"); + } + } catch (Exception ex) { + Logger.printException(() -> "Failed to initialize search data", ex); + } + }); + } + + /** + * Sets up listeners for preferences to keep search results in sync when preference values change. + */ + protected void setupPreferenceListeners() { + for (BaseSearchResultItem item : allSearchItems) { + // Skip non-preference items. + if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) continue; + Preference pref = prefItem.preference; + + if (pref instanceof ColorPickerPreference colorPref) { + colorPref.setOnColorChangeListener((prefKey, newColor) -> { + BaseSearchResultItem.PreferenceSearchItem searchItem = + (BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(prefKey); + if (searchItem != null) { + searchItem.setColor(newColor); + refreshSearchResults(); + } + }); + } else if (pref instanceof CustomDialogListPreference listPref) { + listPref.setOnPreferenceChangeListener((preference, newValue) -> { + BaseSearchResultItem.PreferenceSearchItem searchItem = + (BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(preference.getKey()); + if (searchItem == null) return true; + + int index = listPref.findIndexOfValue(newValue.toString()); + if (index >= 0) { + // Check if a static summary is set. + boolean isStaticSummary = listPref.getStaticSummary() != null; + if (!isStaticSummary) { + // Only update summary if it is not static. + CharSequence newSummary = listPref.getEntries()[index]; + listPref.setSummary(newSummary); + } + } + + listPref.clearHighlightedEntriesForDialog(); + searchItem.refreshHighlighting(); + refreshSearchResults(); + return true; + }); + } + + // Let subclasses handle special preferences. + setupSpecialPreferenceListeners(item); + } + } + + /** + * Collects searchable preferences from a preference group. + */ + protected void collectSearchablePreferences(PreferenceGroup group) { + collectSearchablePreferencesWithKeys(group, "", new ArrayList<>(), 1, 0); + } + + /** + * Collects searchable preferences with their navigation paths and keys. + * + * @param group The preference group to collect from. + * @param parentPath The navigation path of the parent group. + * @param parentKeys The keys of parent preferences. + * @param includeDepth The maximum depth to include in the search index. + * @param currentDepth The current depth in the preference hierarchy. + */ + protected void collectSearchablePreferencesWithKeys(PreferenceGroup group, String parentPath, + List parentKeys, int includeDepth, int currentDepth) { + if (group == null) return; + + for (int i = 0, count = group.getPreferenceCount(); i < count; i++) { + Preference preference = group.getPreference(i); + + // Add to search results only if it is not a category, special group, or PreferenceScreen. + if (shouldIncludePreference(preference, currentDepth, includeDepth)) { + allSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem( + preference, parentPath, parentKeys)); + } + + // If the preference is a group, recurse into it. + if (preference instanceof PreferenceGroup subGroup) { + String newPath = parentPath; + List newKeys = new ArrayList<>(parentKeys); + + // Append the group title to the path and save key for navigation. + if (!isSpecialPreferenceGroup(preference) + && !(preference instanceof NoTitlePreferenceCategory)) { + CharSequence title = preference.getTitle(); + if (!TextUtils.isEmpty(title)) { + newPath = TextUtils.isEmpty(parentPath) + ? title.toString() + : parentPath + " > " + title; + } + + // Add key for navigation if this is a PreferenceScreen or group with navigation capability. + String key = preference.getKey(); + if (!TextUtils.isEmpty(key) && (preference instanceof PreferenceScreen + || searchResultsAdapter.hasNavigationCapability(preference))) { + newKeys.add(key); + } + } + + collectSearchablePreferencesWithKeys(subGroup, newPath, newKeys, includeDepth, currentDepth + 1); + } + } + } + + /** + * Filters all search items based on the provided query and displays results in the overlay. + * Applies highlighting to matching text and shows a "no results" message if nothing matches. + */ + protected void filterAndShowResults(String query) { + hideSearchHistory(); + // Keep track of the previously displayed items to clear their highlights. + List previouslyDisplayedItems = new ArrayList<>(filteredSearchItems); + + filteredSearchItems.clear(); + + String queryLower = Utils.normalizeTextToLowercase(query); + Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE); + + // Clear highlighting only for items that were previously visible. + // This avoids iterating through all items on every keystroke during filtering. + for (BaseSearchResultItem item : previouslyDisplayedItems) { + item.clearHighlighting(); + } + + // Collect matched items first. + List matched = new ArrayList<>(); + int matchCount = 0; + for (BaseSearchResultItem item : allSearchItems) { + if (matchCount >= MAX_SEARCH_RESULTS) break; // Stop after collecting max results. + if (item.matchesQuery(queryLower)) { + item.applyHighlighting(queryPattern); + matched.add(item); + matchCount++; + } + } + + // Build filteredSearchItems, inserting parent enablers for disabled dependents. + Set addedParentKeys = new HashSet<>(2 * matched.size()); + for (BaseSearchResultItem item : matched) { + if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) { + String key = prefItem.preference.getKey(); + Setting setting = (key != null) ? Setting.getSettingFromPath(key) : null; + if (setting != null && !setting.isAvailable()) { + List> parentSettings = setting.getParentSettings(); + for (Setting parentSetting : parentSettings) { + BaseSearchResultItem parentItem = keyToSearchItem.get(parentSetting.key); + if (parentItem != null && !addedParentKeys.contains(parentSetting.key)) { + if (!parentItem.matchesQuery(queryLower)) { + // Apply highlighting to parent items even if they don't match the query. + // This ensures they get their current effective summary calculated. + parentItem.applyHighlighting(queryPattern); + filteredSearchItems.add(parentItem); + } + addedParentKeys.add(parentSetting.key); + } + } + } + filteredSearchItems.add(item); + if (key != null) { + addedParentKeys.add(key); + } + } + } + + if (!filteredSearchItems.isEmpty()) { + //noinspection ComparatorCombinators + Collections.sort(filteredSearchItems, (o1, o2) -> + o1.navigationPath.compareTo(o2.navigationPath) + ); + List displayItems = new ArrayList<>(); + String currentPath = null; + for (BaseSearchResultItem item : filteredSearchItems) { + if (!item.navigationPath.equals(currentPath)) { + BaseSearchResultItem header = new BaseSearchResultItem.GroupHeaderItem(item.navigationPath, item.navigationKeys); + displayItems.add(header); + currentPath = item.navigationPath; + } + displayItems.add(item); + } + filteredSearchItems.clear(); + filteredSearchItems.addAll(displayItems); + } + // Show "No results found" if search results are empty. + if (filteredSearchItems.isEmpty()) { + Preference noResultsPreference = new Preference(activity); + noResultsPreference.setKey("no_results_placeholder"); + 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()); + filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList())); + } + + searchResultsAdapter.notifyDataSetChanged(); + overlayContainer.setVisibility(View.VISIBLE); + } + + /** + * Opens the search interface by showing the search view and hiding the menu item. + * Configures the UI for search mode, shows the keyboard, and displays search suggestions. + */ + protected void openSearch() { + isSearchActive = true; + toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(false); + toolbar.setTitle(""); + searchContainer.setVisibility(View.VISIBLE); + searchView.requestFocus(); + // Configure soft input mode to adjust layout and show keyboard. + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN + | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + inputMethodManager.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT); + // Always show search history when opening search. + showSearchHistory(); + } + + /** + * Closes the search interface and restores the normal UI state. + * Hides the overlay, clears search results, dismisses the keyboard, and removes highlighting. + */ + public void closeSearch() { + isSearchActive = false; + isShowingSearchHistory = false; + + searchHistoryManager.hideSearchHistoryContainer(); + overlayContainer.setVisibility(View.GONE); + + filteredSearchItems.clear(); + + searchContainer.setVisibility(View.GONE); + toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(true); + toolbar.setTitle(originalTitle); + searchView.setQuery("", false); + // Hide keyboard and reset soft input mode. + inputMethodManager.hideSoftInputFromWindow(searchView.getWindowToken(), 0); + activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN); + // Clear highlighting for all search items. + for (BaseSearchResultItem item : allSearchItems) { + item.clearHighlighting(); + } + + searchResultsAdapter.notifyDataSetChanged(); + } + + /** + * Shows the search history if enabled. + */ + protected void showSearchHistory() { + if (searchHistoryManager.isSearchHistoryEnabled()) { + overlayContainer.setVisibility(View.VISIBLE); + searchHistoryManager.showSearchHistory(); + isShowingSearchHistory = true; + } else { + hideAllOverlays(); + } + } + + /** + * Hides the search history container. + */ + protected void hideSearchHistory() { + searchHistoryManager.hideSearchHistoryContainer(); + isShowingSearchHistory = false; + } + + /** + * Hides all overlay containers, including search results and history. + */ + protected void hideAllOverlays() { + hideSearchHistory(); + hideSearchResults(); + } + + /** + * Hides the search results overlay and clears the filtered results. + */ + protected void hideSearchResults() { + overlayContainer.setVisibility(View.GONE); + filteredSearchItems.clear(); + searchResultsAdapter.notifyDataSetChanged(); + for (BaseSearchResultItem item : allSearchItems) { + item.clearHighlighting(); + } + } + + /** + * Refreshes the search results display if the search is active and history is not shown. + */ + protected void refreshSearchResults() { + if (isSearchActive && !isShowingSearchHistory) { + searchResultsAdapter.notifyDataSetChanged(); + } + } + + /** + * Finds a search item corresponding to the given preference. + * + * @param preference The preference to find a search item for. + * @return The corresponding PreferenceSearchItem, or null if not found. + */ + public BaseSearchResultItem.PreferenceSearchItem findSearchItemByPreference(Preference preference) { + // First, search in filtered results. + for (BaseSearchResultItem item : filteredSearchItems) { + if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) { + if (prefItem.preference == preference) { + return prefItem; + } + } + } + // If not found, search in all items. + for (BaseSearchResultItem item : allSearchItems) { + if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) { + if (prefItem.preference == preference) { + return prefItem; + } + } + } + + return null; + } + + /** + * Gets the background color for search view components based on current theme. + */ + @ColorInt + public static int getSearchViewBackground() { + return Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), Utils.isDarkModeEnabled() ? 1.11f : 0.95f); + } + + /** + * Creates a rounded background drawable for the main search view. + */ + protected static GradientDrawable createBackgroundDrawable() { + GradientDrawable background = new GradientDrawable(); + background.setShape(GradientDrawable.RECTANGLE); + background.setCornerRadius(Dim.dp28); + background.setColor(getSearchViewBackground()); + return background; + } + + /** + * Return if a search is currently active. + */ + public boolean isSearchActive() { + return isSearchActive; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java new file mode 100644 index 0000000000..773c8a9241 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/search/SearchHistoryManager.java @@ -0,0 +1,402 @@ +package app.revanced.extension.shared.settings.search; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow; +import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_ENTRIES; +import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_HISTORY; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.util.Pair; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +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; + +/** + * Manager for search history functionality. + */ +public class SearchHistoryManager { + /** + * Interface for handling history item selection. + */ + 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"); + private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow( + ResourceType.ID, "history_text"); + private static final int ID_HISTORY_ICON = getResourceIdentifierOrThrow( + ResourceType.ID, "history_icon"); + private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow( + ResourceType.ID, "delete_icon"); + private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow( + ResourceType.ID, "empty_history_title"); + private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow( + ResourceType.ID, "empty_history_summary"); + private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow( + ResourceType.ID, "search_history_header"); + private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow( + ResourceType.ID, "revanced_settings_search_tips_summary"); + private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow( + ResourceType.LAYOUT, "revanced_preference_search_history_screen"); + private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow( + ResourceType.LAYOUT, "revanced_preference_search_history_item"); + 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"); + + private final Deque searchHistory; + private final Activity activity; + private final SearchHistoryAdapter searchHistoryAdapter; + private final boolean showSettingsSearchHistory; + private final FrameLayout searchHistoryContainer; + + public interface OnSelectHistoryItemListener { + void onSelectHistoryItem(String query); + } + + /** + * Constructor for SearchHistoryManager. + * + * @param activity The parent activity. + * @param overlayContainer The overlay container to hold the search history container. + * @param onSelectHistoryItemAction Callback for when a history item is selected. + */ + SearchHistoryManager(Activity activity, FrameLayout overlayContainer, + OnSelectHistoryItemListener onSelectHistoryItemAction) { + this.activity = activity; + this.showSettingsSearchHistory = SETTINGS_SEARCH_HISTORY.get(); + this.searchHistory = new LinkedList<>(); + + // Initialize search history from settings. + if (showSettingsSearchHistory) { + String entries = SETTINGS_SEARCH_ENTRIES.get(); + if (!entries.isBlank()) { + searchHistory.addAll(Arrays.asList(entries.split("\n"))); + } + } else { + // Clear old saved history if the feature is disabled. + SETTINGS_SEARCH_ENTRIES.resetToDefault(); + } + + // Create search history container. + this.searchHistoryContainer = new FrameLayout(activity); + searchHistoryContainer.setVisibility(View.GONE); + + // Inflate search history layout. + LayoutInflater inflater = LayoutInflater.from(activity); + 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)); + + // Add history container to overlay. + FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT); + overlayParams.gravity = Gravity.TOP; + overlayContainer.addView(searchHistoryContainer, overlayParams); + + // Find the LinearLayout for the history list within the container. + LinearLayout searchHistoryListView = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST); + if (searchHistoryListView == null) { + throw new IllegalStateException("Search history list view not found in container"); + } + + // Set up history adapter. Use a copy of the search history. + this.searchHistoryAdapter = new SearchHistoryAdapter(activity, searchHistoryListView, + new ArrayList<>(searchHistory), onSelectHistoryItemAction); + + // Set up clear history button. + TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON); + clearHistoryButton.setOnClickListener(v -> createAndShowDialog( + str("revanced_settings_search_clear_history"), + str("revanced_settings_search_clear_history_message"), + this::clearAllSearchHistory + )); + + // Set up search tips summary. + CharSequence text = BulletPointPreference.formatIntoBulletPoints( + str("revanced_settings_search_tips_summary")); + TextView tipsSummary = historyView.findViewById(ID_SEARCH_TIPS_SUMMARY); + tipsSummary.setText(text); + } + + /** + * Shows search history screen - either with history items or empty history message. + */ + public void showSearchHistory() { + if (!showSettingsSearchHistory) { + return; + } + + // Find all view elements. + TextView emptyHistoryTitle = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_TITLE); + TextView emptyHistorySummary = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_SUMMARY); + TextView historyHeader = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_HEADER); + LinearLayout historyList = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST); + TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON); + + if (searchHistory.isEmpty()) { + // Show empty history state. + showEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary); + hideHistoryViews(historyHeader, historyList, clearHistoryButton); + } else { + // Show history list state. + hideEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary); + showHistoryViews(historyHeader, historyList, clearHistoryButton); + + // Update adapter with current history. + searchHistoryAdapter.clear(); + searchHistoryAdapter.addAll(searchHistory); + searchHistoryAdapter.notifyDataSetChanged(); + } + + // Show the search history container. + showSearchHistoryContainer(); + } + + /** + * Saves a search query to the history, maintaining the size limit. + */ + public void saveSearchQuery(String query) { + if (!showSettingsSearchHistory) return; + + searchHistory.remove(query); // Remove if already exists to update position. + searchHistory.addFirst(query); // Add to the most recent. + + // Remove extra old entries. + while (searchHistory.size() > MAX_HISTORY_SIZE) { + String last = searchHistory.removeLast(); + Logger.printDebug(() -> "Removing search history query: " + last); + } + + saveSearchHistory(); + } + + /** + * Saves the search history to shared preferences. + */ + protected void saveSearchHistory() { + Logger.printDebug(() -> "Saving search history: " + searchHistory); + SETTINGS_SEARCH_ENTRIES.save(String.join("\n", searchHistory)); + } + + /** + * Removes a search query from the history. + */ + public void removeSearchQuery(String query) { + searchHistory.remove(query); + saveSearchHistory(); + } + + /** + * Clears all search history. + */ + public void clearAllSearchHistory() { + searchHistory.clear(); + saveSearchHistory(); + searchHistoryAdapter.clear(); + searchHistoryAdapter.notifyDataSetChanged(); + showSearchHistory(); + } + + /** + * Checks if search history feature is enabled. + */ + public boolean isSearchHistoryEnabled() { + return showSettingsSearchHistory; + } + + /** + * Shows the search history container and overlay. + */ + public void showSearchHistoryContainer() { + searchHistoryContainer.setVisibility(View.VISIBLE); + } + + /** + * Hides the search history container. + */ + public void hideSearchHistoryContainer() { + searchHistoryContainer.setVisibility(View.GONE); + } + + /** + * Helper method to show empty history views. + */ + protected void showEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) { + emptyTitle.setVisibility(View.VISIBLE); + emptyTitle.setText(str("revanced_settings_search_empty_history_title")); + emptySummary.setVisibility(View.VISIBLE); + emptySummary.setText(str("revanced_settings_search_empty_history_summary")); + } + + /** + * Helper method to hide empty history views. + */ + protected void hideEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) { + emptyTitle.setVisibility(View.GONE); + emptySummary.setVisibility(View.GONE); + } + + /** + * Helper method to show history list views. + */ + protected void showHistoryViews(TextView header, LinearLayout list, TextView clearButton) { + header.setVisibility(View.VISIBLE); + list.setVisibility(View.VISIBLE); + clearButton.setVisibility(View.VISIBLE); + } + + /** + * Helper method to hide history list views. + */ + protected void hideHistoryViews(TextView header, LinearLayout list, TextView clearButton) { + header.setVisibility(View.GONE); + list.setVisibility(View.GONE); + clearButton.setVisibility(View.GONE); + } + + /** + * Creates and shows a dialog with the specified title, message, and confirmation action. + * + * @param title The title of the dialog. + * @param message The message to display in the dialog. + * @param confirmAction The action to perform when the dialog is confirmed. + */ + protected void createAndShowDialog(String title, String message, Runnable confirmAction) { + Pair dialogPair = CustomDialog.create( + activity, + title, + message, + null, + null, + confirmAction, + () -> {}, + null, + null, + false + ); + + Dialog dialog = dialogPair.first; + dialog.setCancelable(true); + dialog.show(); + } + + + /** + * Custom adapter for search history items. + */ + protected class SearchHistoryAdapter { + protected final Collection history; + protected final LayoutInflater inflater; + protected final LinearLayout container; + protected final OnSelectHistoryItemListener onSelectHistoryItemListener; + + public SearchHistoryAdapter(Context context, LinearLayout container, Collection history, + OnSelectHistoryItemListener listener) { + this.history = history; + this.inflater = LayoutInflater.from(context); + this.container = container; + this.onSelectHistoryItemListener = listener; + } + + /** + * Updates the container with current history items. + */ + public void notifyDataSetChanged() { + container.removeAllViews(); + for (String query : history) { + View view = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM, + container, false); + // 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"), + () -> { + removeSearchQuery(query); + remove(query); + notifyDataSetChanged(); + } + )); + + container.addView(view); + } + } + + /** + * Clears all views from the container and history list. + */ + public void clear() { + history.clear(); + container.removeAllViews(); + } + + /** + * Adds all provided history items to the container. + */ + public void addAll(Collection items) { + history.addAll(items); + notifyDataSetChanged(); + } + + /** + * Removes a query from the history and updates the container. + */ + public void remove(String query) { + history.remove(query); + if (history.isEmpty()) { + // If history is now empty, show the empty history state. + showSearchHistory(); + } else { + notifyDataSetChanged(); + } + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java new file mode 100644 index 0000000000..9abd430719 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/ClientType.java @@ -0,0 +1,270 @@ +package app.revanced.extension.shared.spoof; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +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. + */ + // https://dumps.tadiphone.dev/dumps/oculus/eureka + ANDROID_VR_1_61_48( + 28, + "ANDROID_VR", + "com.google.android.apps.youtube.vr.oculus", + "Oculus", + "Quest 3", + "Android", + "12", + // Android 12.1 + "32", + "SQ3A.220605.009.A1", + "1.61.48", + false, + false, + true, + "Android VR 1.61" + ), + /** + * Uses non adaptive bitrate, which fixes audio stuttering with YT Music. + * Does not use AV1. + */ + ANDROID_VR_1_43_32( + ANDROID_VR_1_61_48.id, + ANDROID_VR_1_61_48.clientName, + Objects.requireNonNull(ANDROID_VR_1_61_48.packageName), + ANDROID_VR_1_61_48.deviceMake, + ANDROID_VR_1_61_48.deviceModel, + ANDROID_VR_1_61_48.osName, + ANDROID_VR_1_61_48.osVersion, + Objects.requireNonNull(ANDROID_VR_1_61_48.androidSdkVersion), + Objects.requireNonNull(ANDROID_VR_1_61_48.buildId), + "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" + ), + /** + * Cannot play livestreams and lacks HDR, but can play videos with music and labeled "for children". + * Google Pixel 9 Pro Fold + */ + ANDROID_CREATOR( + 14, + "ANDROID_CREATOR", + "com.google.android.apps.youtube.creator", + "Google", + "Pixel 9 Pro Fold", + "Android", + "15", + "35", + "AP3A.241005.015.A2", + "23.47.101", + true, + false, + true, + "Android Studio" + ), + /** + * Internal YT client for an unreleased YT client. May stop working at any time. + */ + VISIONOS(101, + "VISIONOS", + "Apple", + "RealityDevice14,1", + "visionOS", + "1.3.21O771", + "0.1", + "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" + ); + + /** + * YouTube + * client type + */ + public final int id; + + public final String clientName; + + /** + * App package name. + */ + @Nullable + private final String packageName; + + /** + * Player user-agent. + */ + public final String userAgent; + + /** + * Device model, equivalent to {@link Build#MANUFACTURER} (System property: ro.product.vendor.manufacturer) + */ + public final String deviceMake; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.vendor.model) + */ + public final String deviceModel; + + /** + * Device OS name. + */ + public final String osName; + + /** + * Device OS version. + */ + public final String osVersion; + + /** + * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) + * Field is null if not applicable. + */ + @Nullable + public final String androidSdkVersion; + + /** + * Android build id, equivalent to {@link Build#ID}. + * Field is null if not applicable. + */ + @Nullable + private final String buildId; + + /** + * App version. + */ + public final String clientVersion; + + /** + * If the client should use authentication if available. + */ + public final boolean useAuth; + + /** + * If the client supports multiple audio tracks. + */ + 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. + */ + public final String friendlyName; + + /** + * Android constructor. + */ + ClientType(int id, + String clientName, + @NonNull String packageName, + String deviceMake, + String deviceModel, + String osName, + String osVersion, + @NonNull String androidSdkVersion, + @NonNull String buildId, + String clientVersion, + boolean useAuth, + boolean supportsMultiAudioTracks, + boolean usePlayerEndpoint, + String friendlyName) { + this.id = id; + this.clientName = clientName; + this.packageName = packageName; + this.deviceMake = deviceMake; + this.deviceModel = deviceModel; + this.osName = osName; + this.osVersion = osVersion; + this.androidSdkVersion = androidSdkVersion; + this.buildId = buildId; + 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)", + packageName, + clientVersion, + osVersion, + defaultLocale, + deviceModel, + buildId + ); + Logger.printDebug(() -> "userAgent: " + this.userAgent); + } + + @SuppressWarnings("ConstantLocale") + ClientType(int id, + String clientName, + String deviceMake, + String deviceModel, + String osName, + String osVersion, + String clientVersion, + String userAgent, + boolean useAuth, + boolean supportsMultiAudioTracks, + boolean usePlayerEndpoint, + String friendlyName) { + this.id = id; + this.clientName = clientName; + this.deviceMake = deviceMake; + this.deviceModel = deviceModel; + this.osName = osName; + this.osVersion = osVersion; + this.clientVersion = clientVersion; + this.userAgent = userAgent; + this.useAuth = useAuth; + this.supportsMultiAudioTracks = supportsMultiAudioTracks; + this.usePlayerEndpoint = usePlayerEndpoint; + this.friendlyName = friendlyName; + this.packageName = null; + this.androidSdkVersion = null; + this.buildId = null; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java new file mode 100644 index 0000000000..0c861510fe --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/SpoofVideoStreamsPatch.java @@ -0,0 +1,322 @@ +package app.revanced.extension.shared.spoof; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.AppLanguage; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.spoof.requests.StreamingDataRequest; + +@SuppressWarnings("unused") +public class SpoofVideoStreamsPatch { + + /** + * Domain used for internet connectivity verification. + * It has an empty response body and is only used to check for a 204 response code. + *

+ * If an unreachable IP address (127.0.0.1) is used, no response code is provided. + *

+ * YouTube handles unreachable IP addresses without issue. + * YouTube Music has an issue with waiting for the Cronet connect timeout of 30s on mobile networks. + *

+ * Using a VPN or DNS can temporarily resolve this issue, + * But the ideal workaround is to avoid using an unreachable IP address. + */ + private static final String INTERNET_CONNECTION_CHECK_URI_STRING = "https://www.google.com/gen_204"; + private static final Uri INTERNET_CONNECTION_CHECK_URI = Uri.parse(INTERNET_CONNECTION_CHECK_URI_STRING); + + private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_VIDEO_STREAMS.get(); + + @Nullable + private static volatile AppLanguage languageOverride; + + private static volatile ClientType preferredClient = ClientType.ANDROID_REEL; + + /** + * @return If this patch was included during patching. + */ + public static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + @Nullable + public static AppLanguage getLanguageOverride() { + return languageOverride; + } + + /** + * @param language Language override for non-authenticated requests. + */ + public static void setLanguageOverride(@Nullable AppLanguage language) { + languageOverride = language; + } + + public static void setClientsToUse(List availableClients, ClientType client) { + preferredClient = Objects.requireNonNull(client); + StreamingDataRequest.setClientOrderToUse(availableClients, client); + } + + public static ClientType getPreferredClient() { + return preferredClient; + } + + public static boolean spoofingToClientWithNoMultiAudioStreams() { + return isPatchIncluded() + && SPOOF_STREAMING_DATA + && !preferredClient.supportsMultiAudioTracks; + } + + /** + * Injection point. + * Blocks /get_watch requests by returning an unreachable URI. + * + * @param playerRequestUri The URI of the player request. + * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. + */ + public static Uri blockGetWatchRequest(Uri playerRequestUri) { + if (SPOOF_STREAMING_DATA) { + try { + String path = playerRequestUri.getPath(); + + if (path != null && path.contains("get_watch")) { + Logger.printDebug(() -> "Blocking 'get_watch' by returning internet connection check uri"); + + return INTERNET_CONNECTION_CHECK_URI; + } + } catch (Exception ex) { + Logger.printException(() -> "blockGetWatchRequest failure", ex); + } + } + + return playerRequestUri; + } + + /** + * Injection point. + * + * Blocks /get_watch requests by returning an unreachable URI. + * /att/get requests are used to obtain a PoToken challenge. + * See: botGuardScript.js#L15 + *

+ * Since the Spoof streaming data patch was implemented because a valid PoToken cannot be obtained, + * Blocking /att/get requests are not a problem. + */ + public static String blockGetAttRequest(String originalUrlString) { + if (SPOOF_STREAMING_DATA) { + try { + var originalUri = Uri.parse(originalUrlString); + String path = originalUri.getPath(); + + if (path != null && path.contains("att/get")) { + Logger.printDebug(() -> "Blocking 'att/get' by returning internet connection check uri"); + + return INTERNET_CONNECTION_CHECK_URI_STRING; + } + } catch (Exception ex) { + Logger.printException(() -> "blockGetAttRequest failure", ex); + } + } + + return originalUrlString; + } + + /** + * Injection point. + *

+ * Blocks /initplayback requests. + */ + public static String blockInitPlaybackRequest(String originalUrlString) { + if (SPOOF_STREAMING_DATA) { + try { + var originalUri = Uri.parse(originalUrlString); + String path = originalUri.getPath(); + + if (path != null && path.contains("initplayback")) { + Logger.printDebug(() -> "Blocking 'initplayback' by returning internet connection check uri"); + + return INTERNET_CONNECTION_CHECK_URI_STRING; + } + } catch (Exception ex) { + Logger.printException(() -> "blockInitPlaybackRequest failure", ex); + } + } + + return originalUrlString; + } + + /** + * Injection point. + */ + public static boolean isSpoofingEnabled() { + return SPOOF_STREAMING_DATA; + } + + /** + * Injection point. + * Only invoked when playing a livestream on an Apple client. + */ + public static boolean fixHLSCurrentTime(boolean original) { + if (!SPOOF_STREAMING_DATA) { + return original; + } + return false; + } + + /* + * Injection point. + * Fix audio stuttering in YouTube Music. + */ + public static boolean disableSABR() { + return SPOOF_STREAMING_DATA; + } + + /** + * Injection point. + * Turns off a feature flag that interferes with spoofing. + */ + public static boolean useMediaFetchHotConfigReplacement(boolean original) { + if (original) { + Logger.printDebug(() -> "useMediaFetchHotConfigReplacement is set on"); + } + + if (!SPOOF_STREAMING_DATA) { + return original; + } + return false; + } + + /** + * Injection point. + * Turns off a feature flag that interferes with video playback. + */ + public static boolean usePlaybackStartFeatureFlag(boolean original) { + if (original) { + Logger.printDebug(() -> "usePlaybackStartFeatureFlag is set on"); + } + + if (!SPOOF_STREAMING_DATA) { + return original; + } + return false; + } + + /** + * Injection point. + */ + public static void fetchStreams(String url, Map requestHeaders) { + if (SPOOF_STREAMING_DATA) { + try { + Uri uri = Uri.parse(url); + String path = uri.getPath(); + if (path == null || !path.contains("player")) { + return; + } + + // 'get_drm_license' has no video id and appears to happen when waiting for a paid video to start. + // 'heartbeat' has no video id and appears to be only after playback has started. + // 'refresh' has no video id and appears to happen when waiting for a livestream to start. + // 'ad_break' has no video id. + if (path.contains("get_drm_license") || path.contains("heartbeat") + || path.contains("refresh") || path.contains("ad_break")) { + Logger.printDebug(() -> "Ignoring path: " + path); + return; + } + + String id = uri.getQueryParameter("id"); + if (id == null) { + Logger.printException(() -> "Ignoring request with no id: " + url); + return; + } + + StreamingDataRequest.fetchRequest(id, requestHeaders); + } catch (Exception ex) { + Logger.printException(() -> "buildRequest failure", ex); + } + } + } + + /** + * Injection point. + * Fix playback by replace the streaming data. + * Called after {@link #fetchStreams(String, Map)}. + */ + @Nullable + public static byte[] getStreamingData(String videoId) { + if (SPOOF_STREAMING_DATA) { + try { + StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); + if (request != null) { + // This hook is always called off the main thread, + // but this can later be called for the same video id from the main thread. + // This is not a concern, since the fetch will always be finished + // and never block the main thread. + // But if debugging, then still verify this is the situation. + if (BaseSettings.DEBUG.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) { + Logger.printException(() -> "Error: Blocking main thread"); + } + + var stream = request.getStream(); + if (stream != null) { + Logger.printDebug(() -> "Overriding video stream: " + videoId); + return stream; + } + } + + Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); + } catch (Exception ex) { + Logger.printException(() -> "getStreamingData failure", ex); + } + } + + return null; + } + + /** + * Injection point. + * Called after {@link #getStreamingData(String)}. + */ + @Nullable + public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { + if (SPOOF_STREAMING_DATA) { + try { + final int methodPost = 2; + if (method == methodPost) { + String path = uri.getPath(); + if (path != null && path.contains("videoplayback")) { + return null; + } + } + } catch (Exception ex) { + Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex); + } + } + + return postData; + } + + /** + * Injection point. + */ + public static String appendSpoofedClient(String videoFormat) { + try { + if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get() + && !TextUtils.isEmpty(videoFormat)) { + // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages. + return "\u202D" + videoFormat + "\u2009(" // u202D = left to right override + + StreamingDataRequest.getLastSpoofedClientName() + ")"; + } + } catch (Exception ex) { + Logger.printException(() -> "appendSpoofedClient failure", ex); + } + + return videoFormat; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java new file mode 100644 index 0000000000..959048d1e2 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/PlayerRoutes.java @@ -0,0 +1,109 @@ +package app.revanced.extension.shared.spoof.requests; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Locale; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.settings.AppLanguage; +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( + 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/"; + + /** + * TCP connection and HTTP read timeout + */ + private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. + + private PlayerRoutes() { + } + + static String createInnertubeBody(ClientType clientType, String videoId) { + JSONObject innerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + AppLanguage language = SpoofVideoStreamsPatch.getLanguageOverride(); + if (language == null) { + // Force original audio has not overrode the language. + language = AppLanguage.DEFAULT; + } + //noinspection ExtractMethodRecommender + Locale streamLocale = language.getLocale(); + + JSONObject client = new JSONObject(); + + client.put("deviceMake", clientType.deviceMake); + client.put("deviceModel", clientType.deviceModel); + client.put("clientName", clientType.clientName); + client.put("clientVersion", clientType.clientVersion); + client.put("osName", clientType.osName); + client.put("osVersion", clientType.osVersion); + if (clientType.androidSdkVersion != null) { + client.put("androidSdkVersion", clientType.androidSdkVersion); + } + client.put("hl", streamLocale.getLanguage()); + client.put("gl", streamLocale.getCountry()); + 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); + } + } catch (JSONException e) { + Logger.printException(() -> "Failed to create innerTubeBody", e); + } + + return innerTubeBody.toString(); + } + + @SuppressWarnings("SameParameterValue") + static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { + var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); + + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", clientType.userAgent); + // Not a typo. "Client-Name" uses the client type id. + connection.setRequestProperty("X-YouTube-Client-Name", String.valueOf(clientType.id)); + connection.setRequestProperty("X-YouTube-Client-Version", clientType.clientVersion); + + connection.setUseCaches(false); + connection.setDoOutput(true); + + connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + return connection; + } +} \ No newline at end of file diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java new file mode 100644 index 0000000000..fb8a8e79e8 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/spoof/requests/StreamingDataRequest.java @@ -0,0 +1,318 @@ +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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +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, + * where this class fetches the streams only when YT fetches. + *

+ * Effectively the cache expiration of these fetches is the same as the stock app, + * since the stock app would not use expired streams and therefor + * the extension replace stream hook is called only if YT + * did use its own client streams. + */ +public class StreamingDataRequest { + + private static volatile ClientType[] clientOrderToUse = ClientType.values(); + + public static void setClientOrderToUse(List availableClients, ClientType preferredClient) { + Objects.requireNonNull(preferredClient); + + int availableClientSize = availableClients.size(); + if (!availableClients.contains(preferredClient)) { + availableClientSize++; + } + + clientOrderToUse = new ClientType[availableClientSize]; + clientOrderToUse[0] = preferredClient; + + int i = 1; + for (ClientType c : availableClients) { + if (c != preferredClient) { + clientOrderToUse[i++] = c; + } + } + + Logger.printDebug(() -> "Available spoof clients: " + Arrays.toString(clientOrderToUse)); + } + + private static final String AUTHORIZATION_HEADER = "Authorization"; + + private static final String[] REQUEST_HEADER_KEYS = { + AUTHORIZATION_HEADER, // Available only to logged-in users. + "X-GOOG-API-FORMAT-VERSION", + "X-Goog-Visitor-Id" + }; + + /** + * TCP connection and HTTP read timeout. + */ + private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000; + + /** + * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS} + */ + 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 cache = Collections.synchronizedMap( + Utils.createSizeRestrictedMap(50)); + + /** + * Strings found in the response if the video is a livestream. + */ + private static final ByteTrieSearch liveStreamBufferSearch = new ByteTrieSearch( + convertStringsToBytes( + "yt_live_broadcast", + "yt_premiere_broadcast" + ) + ); + + private static volatile ClientType lastSpoofedClientType; + + public static String getLastSpoofedClientName() { + ClientType client = lastSpoofedClientType; + return client == null ? "Unknown" : client.friendlyName; + } + + private final String videoId; + + private final Future future; + + private StreamingDataRequest(String videoId, Map playerHeaders) { + Objects.requireNonNull(playerHeaders); + this.videoId = videoId; + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); + } + + public static void fetchRequest(String videoId, Map fetchHeaders) { + // Always fetch, even if there is an existing request for the same video. + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + } + + @Nullable + public static StreamingDataRequest getRequestForVideoId(String videoId) { + return cache.get(videoId); + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { + if (showToast) Utils.showToastShort(toastMessage); + 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, + Map playerHeaders, + boolean showErrorToasts) { + Objects.requireNonNull(clientType); + Objects.requireNonNull(videoId); + Objects.requireNonNull(playerHeaders); + + 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); + connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); + + boolean authHeadersIncludes = false; + + for (String key : REQUEST_HEADER_KEYS) { + String value = playerHeaders.get(key); + + if (value != null) { + if (key.equals(AUTHORIZATION_HEADER)) { + if (!clientType.useAuth) { + Logger.printDebug(() -> "Not including request header: " + key); + continue; + } + authHeadersIncludes = true; + } + + Logger.printDebug(() -> "Including request header: " + key); + connection.setRequestProperty(key, value); + } + } + + if (!authHeadersIncludes && clientType.useAuth) { + Logger.printDebug(() -> "Skipping client since user is not logged in: " + clientType + + " videoId: " + videoId); + return null; + } + + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType); + + String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + // This situation likely means the patches are outdated. + // Use a toast message that suggests updating. + handleConnectionError("Playback error (App is outdated?) " + clientType + ": " + + responseCode + " response: " + connection.getResponseMessage(), + null, showErrorToasts); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex, showErrorToasts); + } catch (IOException ex) { + handleConnectionError("Network error", ex, showErrorToasts); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static byte[] fetch(String videoId, Map playerHeaders) { + final boolean debugEnabled = BaseSettings.DEBUG.get(); + + // Retry with different client if empty response body is received. + int i = 0; + for (ClientType clientType : clientOrderToUse) { + // Show an error if the last client type fails, or if debug is enabled then show for all attempts. + final boolean showErrorToast = (++i == clientOrderToUse.length) || debugEnabled; + + HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast); + if (connection != null) { + byte[] playerResponseBuffer = buildPlayerResponseBuffer(clientType, connection); + if (playerResponseBuffer != null) { + lastSpoofedClientType = clientType; + + return playerResponseBuffer; + } + } + } + + lastSpoofedClientType = null; + handleConnectionError("Could not fetch any client streams", null, true); + 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() { + try { + return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStream timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStream interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { + Logger.printException(() -> "getStream failure", ex); + } + + return null; + } + + @NonNull + @Override + public String toString() { + return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}'; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/theme/BaseThemePatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/theme/BaseThemePatch.java new file mode 100644 index 0000000000..2d12b0c1f3 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/theme/BaseThemePatch.java @@ -0,0 +1,48 @@ +package app.revanced.extension.shared.theme; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Utils; + +@SuppressWarnings("unused") +public abstract class BaseThemePatch { + // Background colors. + protected static final int BLACK_COLOR = Utils.getResourceColor("yt_black1"); + protected static final int WHITE_COLOR = Utils.getResourceColor("yt_white1"); + + /** + * Check if a value matches any of the provided values. + * + * @param value The value to check. + * @param of The array of values to compare against. + * @return True if the value matches any of the provided values. + */ + protected static boolean anyEquals(int value, int... of) { + for (int v : of) { + if (value == v) { + return true; + } + } + return false; + } + + /** + * Helper method to process color values for Litho components. + * + * @param originalValue The original color value. + * @param darkValues Array of dark mode color values to match. + * @param lightValues Array of light mode color values to match. + * @return The new or original color value. + */ + protected static int processColorValue(int originalValue, int[] darkValues, @Nullable int[] lightValues) { + if (Utils.isDarkModeEnabled()) { + if (anyEquals(originalValue, darkValues)) { + return BLACK_COLOR; + } + } else if (lightValues != null && anyEquals(originalValue, lightValues)) { + return WHITE_COLOR; + } + + return originalValue; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java new file mode 100644 index 0000000000..07e9b41135 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/ColorDot.java @@ -0,0 +1,60 @@ +package app.revanced.extension.shared.ui; + +import static app.revanced.extension.shared.Utils.adjustColorBrightness; +import static app.revanced.extension.shared.Utils.getAppBackgroundColor; +import static app.revanced.extension.shared.Utils.isDarkModeEnabled; +import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA; + +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.view.View; + +import androidx.annotation.ColorInt; + +public class ColorDot { + private static final int STROKE_WIDTH = Dim.dp(1.5f); + + /** + * Creates a circular drawable with a main fill and a stroke. + * Stroke adapts to dark/light theme and transparency, applied only when color is transparent or matches app background. + */ + public static GradientDrawable createColorDotDrawable(@ColorInt int color) { + final boolean isDarkTheme = isDarkModeEnabled(); + final boolean isTransparent = Color.alpha(color) == 0; + final int opaqueColor = color | 0xFF000000; + final int appBackground = getAppBackgroundColor(); + final int strokeColor; + final int strokeWidth; + + // Determine stroke color. + if (isTransparent || (opaqueColor == appBackground)) { + final int baseColor = isTransparent ? appBackground : opaqueColor; + strokeColor = adjustColorBrightness(baseColor, isDarkTheme ? 1.2f : 0.8f); + strokeWidth = STROKE_WIDTH; + } else { + strokeColor = 0; + strokeWidth = 0; + } + + // Create circular drawable with conditional stroke. + GradientDrawable circle = new GradientDrawable(); + circle.setShape(GradientDrawable.OVAL); + circle.setColor(color); + circle.setStroke(strokeWidth, strokeColor); + + return circle; + } + + /** + * Applies the color dot drawable to the target view. + */ + public static void applyColorDot(View targetView, @ColorInt int color, boolean enabled) { + if (targetView == null) return; + targetView.setBackground(createColorDotDrawable(color)); + targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA); + if (!isDarkModeEnabled()) { + targetView.setClipToOutline(true); + targetView.setElevation(Dim.dp2); + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java new file mode 100644 index 0000000000..15d80c916e --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ui/CustomDialog.java @@ -0,0 +1,461 @@ +package app.revanced.extension.shared.ui; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.util.Pair; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import androidx.annotation.Nullable; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * A utility class for creating a customizable dialog with a title, message or EditText, and up to three buttons (OK, Cancel, Neutral). + * The dialog supports themed colors, rounded corners, and dynamic button layout based on screen width. It is dismissible by default. + */ +public class CustomDialog { + private final Context context; + private final Dialog dialog; + private final LinearLayout mainLayout; + + /** + * Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText. + * The dialog's appearance adapts to the app's dark mode setting, with rounded corners and customizable button actions. + * Buttons adjust dynamically to their text content and are arranged in a single row if they fit within 80% of the + * screen width, with the Neutral button aligned to the left and OK/Cancel buttons centered on the right. + * If buttons do not fit, each is placed on a separate row, all aligned to the right. + * + * @param context Context used to create the dialog. + * @param title Title text of the dialog. + * @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText. + * @param editText EditText to include in the dialog, or null if no EditText is needed. + * @param okButtonText OK button text, or null to use the default "OK" string. + * @param onOkClick Action to perform when the OK button is clicked. + * @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed. + * @param neutralButtonText Neutral button text, or null if no Neutral button is needed. + * @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed. + * @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked. + * @return The Dialog and its main LinearLayout container. + */ + public static Pair create(Context context, CharSequence title, CharSequence message, + @Nullable EditText editText, CharSequence okButtonText, + Runnable onOkClick, Runnable onCancelClick, + @Nullable CharSequence neutralButtonText, + @Nullable Runnable onNeutralClick, + boolean dismissDialogOnNeutralClick) { + Logger.printDebug(() -> "Creating custom dialog with title: " + title); + CustomDialog customDialog = new CustomDialog(context, title, message, editText, + okButtonText, onOkClick, onCancelClick, + neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick); + return new Pair<>(customDialog.dialog, customDialog.mainLayout); + } + + /** + * Initializes a custom dialog with the specified parameters. + * + * @param context Context used to create the dialog. + * @param title Title text of the dialog. + * @param message Message text of the dialog, or null if replaced by EditText. + * @param editText EditText to include in the dialog, or null if no EditText is needed. + * @param okButtonText OK button text, or null to use the default "OK" string. + * @param onOkClick Action to perform when the OK button is clicked. + * @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed. + * @param neutralButtonText Neutral button text, or null if no Neutral button is needed. + * @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed. + * @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked. + */ + private CustomDialog(Context context, CharSequence title, CharSequence message, @Nullable EditText editText, + CharSequence okButtonText, Runnable onOkClick, Runnable onCancelClick, + @Nullable CharSequence neutralButtonText, @Nullable Runnable onNeutralClick, + boolean dismissDialogOnNeutralClick) { + this.context = context; + this.dialog = new Dialog(context); + this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar. + + // Create main layout. + mainLayout = createMainLayout(); + addTitle(title); + addContent(message, editText); + addButtons(okButtonText, onOkClick, onCancelClick, neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick); + + // Set dialog content and window attributes. + dialog.setContentView(mainLayout); + Window window = dialog.getWindow(); + if (window != null) { + Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false); + } + } + + /** + * Creates the main layout for the dialog with vertical orientation and rounded corners. + * + * @return The configured LinearLayout for the dialog. + */ + private LinearLayout createMainLayout() { + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(Dim.dp24, Dim.dp16, Dim.dp24, Dim.dp24); + + // Set rounded rectangle background. + ShapeDrawable background = new ShapeDrawable(new RoundRectShape( + Dim.roundedCorners(28), null, null)); + // Dialog background. + background.getPaint().setColor(Utils.getDialogBackgroundColor()); + layout.setBackground(background); + + return layout; + } + + /** + * Adds a title to the dialog if provided. + * + * @param title The title text to display. + */ + private void addTitle(CharSequence title) { + if (TextUtils.isEmpty(title)) return; + + TextView titleView = new TextView(context); + titleView.setText(title); + titleView.setTypeface(Typeface.DEFAULT_BOLD); + titleView.setTextSize(18); + titleView.setTextColor(Utils.getAppForegroundColor()); + titleView.setGravity(Gravity.CENTER); + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, 0, 0, Dim.dp16); + titleView.setLayoutParams(params); + + mainLayout.addView(titleView); + } + + /** + * Adds a message or EditText to the dialog within a ScrollView. + * + * @param message The message text to display (supports Spanned for HTML), or null if replaced by EditText. + * @param editText The EditText to include, or null if no EditText is needed. + */ + private void addContent(CharSequence message, @Nullable EditText editText) { + // Create content container (message/EditText) inside a ScrollView only if message or editText is provided. + if (message == null && editText == null) return; + + ScrollView scrollView = new ScrollView(context); + // Disable the vertical scrollbar. + scrollView.setVerticalScrollBarEnabled(false); + scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); + + LinearLayout contentContainer = new LinearLayout(context); + contentContainer.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(contentContainer); + + // EditText (if provided). + if (editText != null) { + ShapeDrawable background = new ShapeDrawable(new RoundRectShape( + Dim.roundedCorners(10), null, null)); + background.getPaint().setColor(Utils.getEditTextBackground()); + scrollView.setPadding(Dim.dp8, Dim.dp8, Dim.dp8, Dim.dp8); + scrollView.setBackground(background); + scrollView.setClipToOutline(true); + + // Remove EditText from its current parent, if any. + ViewGroup parent = (ViewGroup) editText.getParent(); + if (parent != null) parent.removeView(editText); + // Style the EditText to match the dialog theme. + editText.setTextColor(Utils.getAppForegroundColor()); + editText.setBackgroundColor(Color.TRANSPARENT); + editText.setPadding(0, 0, 0, 0); + contentContainer.addView(editText, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + // Message (if not replaced by EditText). + } else { + TextView messageView = new TextView(context); + // Supports Spanned (HTML). + messageView.setText(message); + messageView.setTextSize(16); + messageView.setTextColor(Utils.getAppForegroundColor()); + // Enable HTML link clicking if the message contains links. + if (message instanceof Spanned) { + messageView.setMovementMethod(LinkMovementMethod.getInstance()); + } + contentContainer.addView(messageView, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + } + + // Weight to take available space. + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 0, + 1.0f); + scrollView.setLayoutParams(params); + // Add ScrollView to main layout only if content exist. + mainLayout.addView(scrollView); + } + + /** + * Adds buttons to the dialog, arranging them dynamically based on their widths. + * + * @param okButtonText OK button text, or null to use the default "OK" string. + * @param onOkClick Action for the OK button click. + * @param onCancelClick Action for the Cancel button click, or null if no Cancel button. + * @param neutralButtonText Neutral button text, or null if no Neutral button. + * @param onNeutralClick Action for the Neutral button click, or null if no Neutral button. + * @param dismissDialogOnNeutralClick If the dialog should dismiss on Neutral button click. + */ + private void addButtons(CharSequence okButtonText, Runnable onOkClick, Runnable onCancelClick, + @Nullable CharSequence neutralButtonText, @Nullable Runnable onNeutralClick, + boolean dismissDialogOnNeutralClick) { + // Button container. + LinearLayout buttonContainer = new LinearLayout(context); + buttonContainer.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + buttonContainerParams.setMargins(0, Dim.dp16, 0, 0); + buttonContainer.setLayoutParams(buttonContainerParams); + + List