diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index b0d7bd3f97..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,14 +0,0 @@ -[*] -max_line_length = off - -[*.{kt,kts}] -# ktlint entries are for ktlint gradle task and ktlint Intellij / Android Studio plugin. -# Ignore all 'property not supported' warnings, these are correct. -ktlint_code_style = android_studio -ktlint_standard_discouraged-comment-location = disabled -ktlint_standard_no-wildcard-imports = disabled -ij_kotlin_imports_layout=* -ij_kotlin_name_count_to_use_star_import = 5 -ij_kotlin_name_count_to_use_star_import_for_members = 5 -ij_kotlin_allow_trailing_comma_on_call_site = false -ij_kotlin_allow_trailing_comma = false diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d4cbc26736..98b14a097d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -70,8 +70,9 @@ body: Before creating a new bug report, please keep the following in mind: - - **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-patches/labels/Bug%20report). + - **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: @@ -102,7 +103,7 @@ body: label: Acknowledgements description: Your bug report will be closed if you don't follow the checklist below. options: - - label: This issue is not a duplicate of an existing bug report. + - label: I have checked all open and closed bug reports and this is not a duplicate. required: true - label: I have chosen an appropriate title. required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 42a6d93be7..13d436ba29 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -70,8 +70,9 @@ body: Before creating a new feature request, please keep the following in mind: - - **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-patches/labels/Feature%20request). + - **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: @@ -98,7 +99,7 @@ body: label: Acknowledgements description: Your feature request will be closed if you don't follow the checklist below. options: - - label: This issue is not a duplicate of an existing feature request. + - label: I have checked all open and closed feature requests and this is not a duplicate required: true - label: I have chosen an appropriate title. required: true diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 250871bcc2..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,16 +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@v5 + with: + 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 dabbeb6b40..c1402f5490 100644 --- a/.github/workflows/open_pull_request.yml +++ b/.github/workflows/open_pull_request.yml @@ -15,17 +15,17 @@ 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 with: - destination_branch: 'main' + destination_branch: main pr_title: 'chore: ${{ env.MESSAGE }}' pr_body: | This pull request will ${{ env.MESSAGE }}. - - ## Dependencies before merge - - [ ] https://github.com/revanced/revanced-integrations + ## Before merging this PR + + - [ ] Pull translations from Crowdin pr_draft: true diff --git a/.github/workflows/pull_strings.yml b/.github/workflows/pull_strings.yml new file mode 100644 index 0000000000..f2da51ec74 --- /dev/null +++ b/.github/workflows/pull_strings.yml @@ -0,0 +1,54 @@ +name: Pull strings + +on: + workflow_dispatch: + +jobs: + pull: + name: Pull strings + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: dev + persist-credentials: true + + - name: Pull strings + uses: crowdin/github-action@v2 + with: + config: crowdin.yml + upload_sources: false + download_translations: true + 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 new file mode 100644 index 0000000000..d13f88573d --- /dev/null +++ b/.github/workflows/push_strings.yml @@ -0,0 +1,32 @@ +name: Push strings + +on: + workflow_dispatch: + push: + branches: + - dev + paths: + - patches/src/main/resources/addresources/values/strings.xml + +jobs: + push: + name: Push strings + runs-on: ubuntu-latest + steps: + - name: Checkout + 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 + with: + config: crowdin.yml + upload_sources: true + env: + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84ac544215..27a19a17c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,28 +10,36 @@ on: jobs: release: name: Release + permissions: + contents: write + packages: write + id-token: write + attestations: write + artifact-metadata: write runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + - name: Setup Java + uses: actions/setup-java@v5 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 + 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 generateMeta 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 @@ -42,9 +50,19 @@ jobs: with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - fingerprint: ${{ env.GPG_FINGERPRINT }} + fingerprint: ${{ vars.GPG_FINGERPRINT }} - name: Release + uses: cycjimmy/semantic-release-action@v5 + id: release env: - GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} - run: npm exec semantic-release + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }} + ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }} + + - name: Attest + if: steps.release.outputs.new_release_published == 'true' + uses: actions/attest@v4 + with: + subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}' + subject-path: patches/build/libs/patches-*.rvp diff --git a/.github/workflows/sync_crowdin.yml b/.github/workflows/sync_crowdin.yml deleted file mode 100644 index e0d0912b72..0000000000 --- a/.github/workflows/sync_crowdin.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Sync Crowdin - -on: - workflow_dispatch: - schedule: - - cron: 0 * 1 * * - push: - paths: - - /src/main/resources/addresources/values/strings.xml - -jobs: - sync: - name: Sync translations - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Sync translations - uses: crowdin/github-action@v1 - with: - config: crowdin.yml - upload_sources: true - upload_translations: 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 - env: - GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} \ No newline at end of file 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/.gitignore b/.gitignore index 5015417667..62f6eb424f 100644 --- a/.gitignore +++ b/.gitignore @@ -122,5 +122,8 @@ gradle-app.setting # Dependency directories node_modules/ -# gradle properties, due to Github token +# Gradle properties, due to Github token ./gradle.properties + +# One package is called the same as the Gradle build folder +!**/src/**/build/ \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 7a0ae16800..0000000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123c2b..0000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.releaserc b/.releaserc index 6193511b8e..ee495bf966 100644 --- a/.releaserc +++ b/.releaserc @@ -21,11 +21,10 @@ "@semantic-release/git", { "assets": [ - "README.md", "CHANGELOG.md", - "gradle.properties", - "patches.json" - ] + "gradle.properties" + ], + "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ], [ @@ -33,20 +32,17 @@ { "assets": [ { - "path": "build/libs/revanced-patches*" - }, - { - "path": "patches.json" + "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 8a0c9e0f40..bfebce7e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7077 @@ +# [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) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Use system language as default iOS audio stream ([#4042](https://github.com/ReVanced/revanced-patches/issues/4042)) ([4017185](https://github.com/ReVanced/revanced-patches/commit/4017185e760c0569e6644b94bbe66a84fa245b4b)) + +## [5.2.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.0...v5.2.1) (2024-12-04) + + +### Bug Fixes + +* **Twitch:** Resolve setting menu crashes ([#4025](https://github.com/ReVanced/revanced-patches/issues/4025)) ([62df596](https://github.com/ReVanced/revanced-patches/commit/62df5965d7331e47b3143425d169a79a19eac447)) +* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4014](https://github.com/ReVanced/revanced-patches/issues/4014)) ([c8eced5](https://github.com/ReVanced/revanced-patches/commit/c8eced54704017df4e91e536dbef1e9514306f67)) +* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4019](https://github.com/ReVanced/revanced-patches/issues/4019)) ([d89ad65](https://github.com/ReVanced/revanced-patches/commit/d89ad6501a7cdb3c074c6204dac7960ca3e252f1)) +* **YouTube Music - Hide category bar:** Add support for latest release ([#3968](https://github.com/ReVanced/revanced-patches/issues/3968)) ([b63fdeb](https://github.com/ReVanced/revanced-patches/commit/b63fdeb10b504468307a77bd5de69407906848bf)) + + +### Performance Improvements + +* Move variables to local scope ([43c0421](https://github.com/ReVanced/revanced-patches/commit/43c04216c6e647eaf6ad7e813eb5f0df0c108b77)) + +## [5.2.1-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.4...v5.2.1-dev.5) (2024-12-01) + + +### Performance Improvements + +* Move variables to local scope ([43c0421](https://github.com/ReVanced/revanced-patches/commit/43c04216c6e647eaf6ad7e813eb5f0df0c108b77)) + +## [5.2.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.3...v5.2.1-dev.4) (2024-11-30) + + +### Bug Fixes + +* **Twitch:** Resolve setting menu crashes ([#4025](https://github.com/ReVanced/revanced-patches/issues/4025)) ([62df596](https://github.com/ReVanced/revanced-patches/commit/62df5965d7331e47b3143425d169a79a19eac447)) + +## [5.2.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.2...v5.2.1-dev.3) (2024-11-29) + + +### Bug Fixes + +* **YouTube Music - Hide category bar:** Add support for latest release ([#3968](https://github.com/ReVanced/revanced-patches/issues/3968)) ([b63fdeb](https://github.com/ReVanced/revanced-patches/commit/b63fdeb10b504468307a77bd5de69407906848bf)) + +## [5.2.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.1-dev.1...v5.2.1-dev.2) (2024-11-28) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4019](https://github.com/ReVanced/revanced-patches/issues/4019)) ([d89ad65](https://github.com/ReVanced/revanced-patches/commit/d89ad6501a7cdb3c074c6204dac7960ca3e252f1)) + +## [5.2.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.2.0...v5.2.1-dev.1) (2024-11-28) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Update spoof target to resolve library tab crashes ([#4014](https://github.com/ReVanced/revanced-patches/issues/4014)) ([c8eced5](https://github.com/ReVanced/revanced-patches/commit/c8eced54704017df4e91e536dbef1e9514306f67)) + +# [5.2.0](https://github.com/ReVanced/revanced-patches/compare/v5.1.0...v5.2.0) (2024-11-27) + + +### Bug Fixes + +* **My Expenses - Unlock pro:** Constrain compatible version to working version ([#3974](https://github.com/ReVanced/revanced-patches/issues/3974)) ([ba3bf69](https://github.com/ReVanced/revanced-patches/commit/ba3bf69df07ec8dab46868c3940ebd56db0cd137)) +* **YouTube - Hide Shorts components:** Add missing options to patch ([65f62fc](https://github.com/ReVanced/revanced-patches/commit/65f62fcd5ac340616a96542c64faf2af2a60df28)) +* **YouTube - Playback speed:** Allow long press 2x speed when using custom playback speeds ([#3990](https://github.com/ReVanced/revanced-patches/issues/3990)) ([79a543a](https://github.com/ReVanced/revanced-patches/commit/79a543a57470638f983862c61270e046f3ac5cb7)) +* **YouTube - Settings:** Do not clip settings menus when using an Android 15 device ([#3999](https://github.com/ReVanced/revanced-patches/issues/3999)) ([7382a02](https://github.com/ReVanced/revanced-patches/commit/7382a020b8322a7abc016a4569bc15f9caf05546)) +* **YouTube - Settings:** Show navigation back button in setting sub menus ([#3991](https://github.com/ReVanced/revanced-patches/issues/3991)) ([e61686c](https://github.com/ReVanced/revanced-patches/commit/e61686c1039ae29e443273e4da4ec63956216841)) +* **YouTube - Spoof video streams:** Log out the iOS client to restore kids videos playback ([#4000](https://github.com/ReVanced/revanced-patches/issues/4000)) ([cc2ac4e](https://github.com/ReVanced/revanced-patches/commit/cc2ac4e4cd15ca2a23d60abd160d915bc98f99b4)) + + +### Features + +* **TikTok:** Add ReVanced settings about screen ([#4009](https://github.com/ReVanced/revanced-patches/issues/4009)) ([12ea26b](https://github.com/ReVanced/revanced-patches/commit/12ea26b10ddea5ad39da1d35e2b8fd0b48c15d88)) +* **VSCO:** Remove non functional `Unlock pro` patch ([4fddb19](https://github.com/ReVanced/revanced-patches/commit/4fddb1930bc7adeee3b60ae9cd346b143e88bd42)) +* **YouTube - Theme:** Apply custom seekbar color to splash screen animation ([#3978](https://github.com/ReVanced/revanced-patches/issues/3978)) ([98d57e2](https://github.com/ReVanced/revanced-patches/commit/98d57e28af7206099867474b7aa3760cd4fe333f)) +* **YouTube:** Support version `19.46.42` ([#4010](https://github.com/ReVanced/revanced-patches/issues/4010)) ([122aac6](https://github.com/ReVanced/revanced-patches/commit/122aac6aee8ef0737f18564f11bbc2a6addf4a6b)) + +# [5.2.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.6...v5.2.0-dev.7) (2024-11-27) + + +### Bug Fixes + +* **YouTube - Settings:** Do not clip settings menus when using an Android 15 device ([#3999](https://github.com/ReVanced/revanced-patches/issues/3999)) ([7382a02](https://github.com/ReVanced/revanced-patches/commit/7382a020b8322a7abc016a4569bc15f9caf05546)) + +# [5.2.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.5...v5.2.0-dev.6) (2024-11-27) + + +### Features + +* **YouTube:** Support version `19.46.42` ([#4010](https://github.com/ReVanced/revanced-patches/issues/4010)) ([122aac6](https://github.com/ReVanced/revanced-patches/commit/122aac6aee8ef0737f18564f11bbc2a6addf4a6b)) + +# [5.2.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.4...v5.2.0-dev.5) (2024-11-27) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Log out the iOS client to restore kids videos playback ([#4000](https://github.com/ReVanced/revanced-patches/issues/4000)) ([cc2ac4e](https://github.com/ReVanced/revanced-patches/commit/cc2ac4e4cd15ca2a23d60abd160d915bc98f99b4)) + + +### Features + +* **TikTok:** Add ReVanced settings about screen ([#4009](https://github.com/ReVanced/revanced-patches/issues/4009)) ([12ea26b](https://github.com/ReVanced/revanced-patches/commit/12ea26b10ddea5ad39da1d35e2b8fd0b48c15d88)) + +# [5.2.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.3...v5.2.0-dev.4) (2024-11-26) + + +### Bug Fixes + +* **YouTube - Playback speed:** Allow long press 2x speed when using custom playback speeds ([#3990](https://github.com/ReVanced/revanced-patches/issues/3990)) ([79a543a](https://github.com/ReVanced/revanced-patches/commit/79a543a57470638f983862c61270e046f3ac5cb7)) + +# [5.2.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.2...v5.2.0-dev.3) (2024-11-26) + + +### Features + +* **VSCO:** Remove non functional `Unlock pro` patch ([4fddb19](https://github.com/ReVanced/revanced-patches/commit/4fddb1930bc7adeee3b60ae9cd346b143e88bd42)) + +# [5.2.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.2.0-dev.1...v5.2.0-dev.2) (2024-11-26) + + +### Bug Fixes + +* **YouTube - Settings:** Show navigation back button in setting sub menus ([#3991](https://github.com/ReVanced/revanced-patches/issues/3991)) ([e61686c](https://github.com/ReVanced/revanced-patches/commit/e61686c1039ae29e443273e4da4ec63956216841)) + +# [5.2.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.1.1-dev.2...v5.2.0-dev.1) (2024-11-25) + + +### Features + +* **YouTube - Theme:** Apply custom seekbar color to splash screen animation ([#3978](https://github.com/ReVanced/revanced-patches/issues/3978)) ([98d57e2](https://github.com/ReVanced/revanced-patches/commit/98d57e28af7206099867474b7aa3760cd4fe333f)) + +## [5.1.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.1.1-dev.1...v5.1.1-dev.2) (2024-11-25) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Add missing options to patch ([65f62fc](https://github.com/ReVanced/revanced-patches/commit/65f62fcd5ac340616a96542c64faf2af2a60df28)) + +## [5.1.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.1.0...v5.1.1-dev.1) (2024-11-24) + + +### Bug Fixes + +* **My Expenses - Unlock pro:** Constrain compatible version to working version ([#3974](https://github.com/ReVanced/revanced-patches/issues/3974)) ([ba3bf69](https://github.com/ReVanced/revanced-patches/commit/ba3bf69df07ec8dab46868c3940ebd56db0cd137)) + +# [5.1.0](https://github.com/ReVanced/revanced-patches/compare/v5.0.2...v5.1.0) (2024-11-24) + + +### Bug Fixes + +* **YouTube - Change header:** Apply header changes to A/B layout ([#3907](https://github.com/ReVanced/revanced-patches/issues/3907)) ([6ccf114](https://github.com/ReVanced/revanced-patches/commit/6ccf11426ec9e9cd9c8e89a2443f0d0645cc78b1)) +* **YouTube - Hide Shorts components:** Do not hide Shorts action buttons on app first launch ([#3933](https://github.com/ReVanced/revanced-patches/issues/3933)) ([0d78815](https://github.com/ReVanced/revanced-patches/commit/0d78815e33bf2ae216e519f067fb773df0f2084e)) +* **YouTube - Playback speed:** Add 'Auto' speed. Always override speed if default is set to 1.0x ([#3914](https://github.com/ReVanced/revanced-patches/issues/3914)) ([497739e](https://github.com/ReVanced/revanced-patches/commit/497739e8ce6933c1f1ea46edffc102e56b985623)) +* **YouTube - SponsorBlock:** Fix create new segment crash on tablet custom roms ([#3946](https://github.com/ReVanced/revanced-patches/issues/3946)) ([a0da377](https://github.com/ReVanced/revanced-patches/commit/a0da377ba8f90ba39e905ed9730b3e819633bd50)) +* **YouTube - Spoof app version:** Adjust legacy spoof targets ([#3934](https://github.com/ReVanced/revanced-patches/issues/3934)) ([f5794c1](https://github.com/ReVanced/revanced-patches/commit/f5794c1f896c331d76fdfc299e31a2773f2209ca)) +* **YouTube - Spoof app version:** Remove broken spoof targets when patching 19.25+ ([#3915](https://github.com/ReVanced/revanced-patches/issues/3915)) ([9e18eca](https://github.com/ReVanced/revanced-patches/commit/9e18ecab1877dd33a3ad0fe216e6b91a8daaf1f8)) + + +### Features + +* **YouTube - Miniplayer:** Add option to disable miniplayer ([#3961](https://github.com/ReVanced/revanced-patches/issues/3961)) ([e565cdb](https://github.com/ReVanced/revanced-patches/commit/e565cdb583aacfc0052d12c430f56fd9abd5bf00)) +* **YouTube:** Support version `19.45.38` ([#3938](https://github.com/ReVanced/revanced-patches/issues/3938)) ([7c4e3fe](https://github.com/ReVanced/revanced-patches/commit/7c4e3fe97e8cbbb8cf16a2fb95f64223ca2bd7ef)) + +# [5.1.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.1.0-dev.2...v5.1.0-dev.3) (2024-11-22) + + +### Features + +* **YouTube - Miniplayer:** Add option to disable miniplayer ([#3961](https://github.com/ReVanced/revanced-patches/issues/3961)) ([e565cdb](https://github.com/ReVanced/revanced-patches/commit/e565cdb583aacfc0052d12c430f56fd9abd5bf00)) + +# [5.1.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.1.0-dev.1...v5.1.0-dev.2) (2024-11-21) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Fix create new segment crash on tablet custom roms ([#3946](https://github.com/ReVanced/revanced-patches/issues/3946)) ([a0da377](https://github.com/ReVanced/revanced-patches/commit/a0da377ba8f90ba39e905ed9730b3e819633bd50)) + +# [5.1.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.5...v5.1.0-dev.1) (2024-11-20) + + +### Features + +* **YouTube:** Support version `19.45.38` ([#3938](https://github.com/ReVanced/revanced-patches/issues/3938)) ([7c4e3fe](https://github.com/ReVanced/revanced-patches/commit/7c4e3fe97e8cbbb8cf16a2fb95f64223ca2bd7ef)) + +## [5.0.3-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.4...v5.0.3-dev.5) (2024-11-18) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Do not hide Shorts action buttons on app first launch ([#3933](https://github.com/ReVanced/revanced-patches/issues/3933)) ([0d78815](https://github.com/ReVanced/revanced-patches/commit/0d78815e33bf2ae216e519f067fb773df0f2084e)) + +## [5.0.3-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.3...v5.0.3-dev.4) (2024-11-18) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Adjust legacy spoof targets ([#3934](https://github.com/ReVanced/revanced-patches/issues/3934)) ([f5794c1](https://github.com/ReVanced/revanced-patches/commit/f5794c1f896c331d76fdfc299e31a2773f2209ca)) + +## [5.0.3-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.2...v5.0.3-dev.3) (2024-11-15) + + +### Bug Fixes + +* **YouTube - Playback speed:** Add 'Auto' speed. Always override speed if default is set to 1.0x ([#3914](https://github.com/ReVanced/revanced-patches/issues/3914)) ([497739e](https://github.com/ReVanced/revanced-patches/commit/497739e8ce6933c1f1ea46edffc102e56b985623)) + +## [5.0.3-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.3-dev.1...v5.0.3-dev.2) (2024-11-15) + + +### Bug Fixes + +* **YouTube - Spoof app version:** Remove broken spoof targets when patching 19.25+ ([#3915](https://github.com/ReVanced/revanced-patches/issues/3915)) ([9e18eca](https://github.com/ReVanced/revanced-patches/commit/9e18ecab1877dd33a3ad0fe216e6b91a8daaf1f8)) + +## [5.0.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.2...v5.0.3-dev.1) (2024-11-13) + + +### Bug Fixes + +* **YouTube - Change header:** Apply header changes to A/B layout ([#3907](https://github.com/ReVanced/revanced-patches/issues/3907)) ([6ccf114](https://github.com/ReVanced/revanced-patches/commit/6ccf11426ec9e9cd9c8e89a2443f0d0645cc78b1)) + +## [5.0.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.1...v5.0.2) (2024-11-12) + + +### Bug Fixes + +* **Sync for Reddit - Fix /s/ links:** Fix patch by using correct fingerprints ([a0ad07e](https://github.com/ReVanced/revanced-patches/commit/a0ad07ef3170dbe1d91ebd40f11d97b63d1c63d0)) +* **Sync for Reddit - Spoof client:** Fix patch by using correct fingerprints ([5776de3](https://github.com/ReVanced/revanced-patches/commit/5776de3cfbfa62360267eb6026525d2da8c45654)) +* **YouTube - Player controls:** Show player control buttons with A/B layout ([#3901](https://github.com/ReVanced/revanced-patches/issues/3901)) ([bb526bc](https://github.com/ReVanced/revanced-patches/commit/bb526bc00a384eb808f46267e5802c8e5beaa7d5)) + +## [5.0.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.2-dev.1...v5.0.2-dev.2) (2024-11-12) + + +### Bug Fixes + +* **YouTube - Player controls:** Show player control buttons with A/B layout ([#3901](https://github.com/ReVanced/revanced-patches/issues/3901)) ([bb526bc](https://github.com/ReVanced/revanced-patches/commit/bb526bc00a384eb808f46267e5802c8e5beaa7d5)) + +## [5.0.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.1...v5.0.2-dev.1) (2024-11-11) + + +### Bug Fixes + +* **Sync for Reddit - Fix /s/ links:** Fix patch by using correct fingerprints ([a0ad07e](https://github.com/ReVanced/revanced-patches/commit/a0ad07ef3170dbe1d91ebd40f11d97b63d1c63d0)) +* **Sync for Reddit - Spoof client:** Fix patch by using correct fingerprints ([5776de3](https://github.com/ReVanced/revanced-patches/commit/5776de3cfbfa62360267eb6026525d2da8c45654)) + +## [5.0.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.0...v5.0.1) (2024-11-11) + + +### Bug Fixes + +* **Sync:** Fix patches by not throwing unnecessarily ([2ee1316](https://github.com/ReVanced/revanced-patches/commit/2ee13160d51dba3c5806594b2387f806e5946b9a)) +* **Tiktok - Settings:** Fix the patch by depending on the correct settings patch ([0c75929](https://github.com/ReVanced/revanced-patches/commit/0c75929a83729841197b482d28f7f7f5f9cec332)) +* **Twitter:** Fix patches by depending on patch that merges required extension ([c330e9d](https://github.com/ReVanced/revanced-patches/commit/c330e9d67d3e8c8c3535fa43e52c9f06e33ff0bf)) +* **Twitter:** Fix patches by matching fingerprint using correct class ([6ae0d12](https://github.com/ReVanced/revanced-patches/commit/6ae0d124e1f27faecd20e4008951b08353572d98)) +* **YouTube - Playback speed:** Remember playback speed when using non 1.0x default speed ([05b9f87](https://github.com/ReVanced/revanced-patches/commit/05b9f8709895dae67e8cc12e8b7bdb87ff401997)), closes [#3810](https://github.com/ReVanced/revanced-patches/issues/3810) + + +### Performance Improvements + +* Check for extension without a class proxy ([a6a74e2](https://github.com/ReVanced/revanced-patches/commit/a6a74e289db1fe04db230d1e864cb9e752f9a01d)) + +## [5.0.1-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.3...v5.0.1-dev.4) (2024-11-11) + + +### Bug Fixes + +* **Twitter:** Fix patches by depending on patch that merges required extension ([c330e9d](https://github.com/ReVanced/revanced-patches/commit/c330e9d67d3e8c8c3535fa43e52c9f06e33ff0bf)) + +## [5.0.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.2...v5.0.1-dev.3) (2024-11-11) + + +### Bug Fixes + +* **YouTube - Playback speed:** Remember playback speed when using non 1.0x default speed ([05b9f87](https://github.com/ReVanced/revanced-patches/commit/05b9f8709895dae67e8cc12e8b7bdb87ff401997)), closes [#3810](https://github.com/ReVanced/revanced-patches/issues/3810) + +## [5.0.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.1-dev.1...v5.0.1-dev.2) (2024-11-11) + + +### Bug Fixes + +* **Twitter:** Fix patches by matching fingerprint using correct class ([6ae0d12](https://github.com/ReVanced/revanced-patches/commit/6ae0d124e1f27faecd20e4008951b08353572d98)) + +## [5.0.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.0.0...v5.0.1-dev.1) (2024-11-11) + + +### Bug Fixes + +* **Sync:** Fix patches by not throwing unnecessarily ([2ee1316](https://github.com/ReVanced/revanced-patches/commit/2ee13160d51dba3c5806594b2387f806e5946b9a)) +* **Tiktok - Settings:** Fix the patch by depending on the correct settings patch ([0c75929](https://github.com/ReVanced/revanced-patches/commit/0c75929a83729841197b482d28f7f7f5f9cec332)) + + +### Performance Improvements + +* Check for extension without a class proxy ([a6a74e2](https://github.com/ReVanced/revanced-patches/commit/a6a74e289db1fe04db230d1e864cb9e752f9a01d)) + +# [5.0.0](https://github.com/ReVanced/revanced-patches/compare/v4.17.0...v5.0.0) (2024-11-10) + + +### Bug Fixes + +* Add missing dependency to patch ([97f5240](https://github.com/ReVanced/revanced-patches/commit/97f5240d53b9978fb3745170fe03619c7c90274a)) +* **MyFitnessPal - Hide ads:** Constrain patch to last working version ([#3847](https://github.com/ReVanced/revanced-patches/issues/3847)) ([f9fa526](https://github.com/ReVanced/revanced-patches/commit/f9fa526b04c2848175c389d6bb911aa5a245b60f)) +* **Twitter - Change link sharing domain:** Support latest app version ([#3786](https://github.com/ReVanced/revanced-patches/issues/3786)) ([b54592c](https://github.com/ReVanced/revanced-patches/commit/b54592cf9c5d859e1af2f02e8e6aaad7d47ab760)) +* **YouTube - Copy video URL:** Support A/B player layout ([0f42574](https://github.com/ReVanced/revanced-patches/commit/0f42574b7f4b1c9a48df8550c7d710093f76ce8c)) +* **YouTube - Custom branding:** Change icon correctly on 19.34+ ([#3866](https://github.com/ReVanced/revanced-patches/issues/3866)) ([2e47903](https://github.com/ReVanced/revanced-patches/commit/2e4790382546256e106a5842cd8c530f41b161e5)) +* **YouTube - Hide ads:** Hide new types of ads ([454281a](https://github.com/ReVanced/revanced-patches/commit/454281ac2108648832b7f0203f5fb7e814887835)) +* **YouTube - Hide layout components:** Move hide chips settings to Feed menu ([1ed677f](https://github.com/ReVanced/revanced-patches/commit/1ed677f7b8ba561b2bb173dcaf5d6123c22179c4)) +* **YouTube - Hide layout components:** Remove obsolete 'Hide gray separator' ([a697701](https://github.com/ReVanced/revanced-patches/commit/a697701c5f1f9510b51e310b1ff212b609f38519)) +* **YouTube - Playback speed:** Remember playback speed with new speed menu ([#3810](https://github.com/ReVanced/revanced-patches/issues/3810)) ([c3a5e14](https://github.com/ReVanced/revanced-patches/commit/c3a5e14a0a24973a0f9956845c9e0f99c1301d42)) +* **YouTube - Playback speed:** Restore old playback speed menu ([#3817](https://github.com/ReVanced/revanced-patches/issues/3817)) ([806b210](https://github.com/ReVanced/revanced-patches/commit/806b21093e3251697f03cd8804e5d5cd26070716)) +* **YouTube - Remember video quality:** Correctly set default quality when changing from a low quality video ([#3879](https://github.com/ReVanced/revanced-patches/issues/3879)) ([ddb73e8](https://github.com/ReVanced/revanced-patches/commit/ddb73e857d7c26fd27ea995a27f53f5660d3f71c)) +* **YouTube - Remove background playback restrictions:** Enable for Shorts as well ([#3671](https://github.com/ReVanced/revanced-patches/issues/3671)) ([7db1a77](https://github.com/ReVanced/revanced-patches/commit/7db1a7751dc47c4e36096fbdc2b3761b0ae11ccb)) +* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([084e0a5](https://github.com/ReVanced/revanced-patches/commit/084e0a527b1c75d1ef15dc706c429aa48d0ffe6b)) +* **YouTube - Return YouTube Dislike:** Use latest separator height ([ae160a3](https://github.com/ReVanced/revanced-patches/commit/ae160a37985cc96c6de7e1a2fe5a1c83bc523046)) +* **YouTube - Seekbar:** Use latest shade of YouTube red ([4b77648](https://github.com/ReVanced/revanced-patches/commit/4b77648607a84eb29f4cae9ddb42b87084be7cd0)) +* **YouTube - Settings:** Use multiline preference title for localized languages ([#3821](https://github.com/ReVanced/revanced-patches/issues/3821)) ([ff85d49](https://github.com/ReVanced/revanced-patches/commit/ff85d490887de64eb6c6fd42e385a3e75969ff10)) +* **YouTube - SponsorBlock:** Show correct segment behavior in settings UI after importing ([e3f25a0](https://github.com/ReVanced/revanced-patches/commit/e3f25a03cd314eeae786e7660a6beacb275a6a76)) +* **YouTube - Spoof app version:** Remove obsolete 17.33.42 spoof target ([#3825](https://github.com/ReVanced/revanced-patches/issues/3825)) ([33aeba2](https://github.com/ReVanced/revanced-patches/commit/33aeba2a0895e9ecaba27ba4a3b22b86c9f1a51c)) +* **YouTube:** Merge `Restore old seekbar thumbnails` into `Seekbar thumbnails` ([#3860](https://github.com/ReVanced/revanced-patches/issues/3860)) ([e377b1e](https://github.com/ReVanced/revanced-patches/commit/e377b1e6ad93dea8e5f3829cd3894f71851887a3)) + + +### Build System + +* Bump ReVanced Patcher ([eee1692](https://github.com/ReVanced/revanced-patches/commit/eee16922779f994f5752190a20a9016ea98ec4cb)) + + +### Features + +* **YouTube - Hide layout components:** Hide player shopping shelf ([#3804](https://github.com/ReVanced/revanced-patches/issues/3804)) ([1952f3b](https://github.com/ReVanced/revanced-patches/commit/1952f3b3c4bca08ed0f6e5b1117e0a6c51f00ed2)) +* **YouTube - Hide player flyout menu items:** Hide stable volume ([#3827](https://github.com/ReVanced/revanced-patches/issues/3827)) ([b91e932](https://github.com/ReVanced/revanced-patches/commit/b91e932e65c04b1c1aee9a2f3dc3a73772d9c225)) +* **YouTube - Miniplayer:** Add horizontal drag gesture ([#3859](https://github.com/ReVanced/revanced-patches/issues/3859)) ([e32b19e](https://github.com/ReVanced/revanced-patches/commit/e32b19e170a5571b23547c3211b497089d0cd441)) +* **YouTube - Player flyout menu:** Hide sleep timer ([#3637](https://github.com/ReVanced/revanced-patches/issues/3637)) ([7e1bdab](https://github.com/ReVanced/revanced-patches/commit/7e1bdab520dba65682f018f819c0b7d9783f94ca)) +* **YouTube:** Add `Seekbar thumbnails` patch ([#3813](https://github.com/ReVanced/revanced-patches/issues/3813)) ([5988b75](https://github.com/ReVanced/revanced-patches/commit/5988b759752b944b6999b401faa394e2089e4003)) +* **YouTube:** Add `Shorts autoplay` patch ([#3794](https://github.com/ReVanced/revanced-patches/issues/3794)) ([96b5aed](https://github.com/ReVanced/revanced-patches/commit/96b5aede482f7a69d6df17864a2e17568b0da880)) +* **YouTube:** Hide player shopping shelf in playlists ([#3806](https://github.com/ReVanced/revanced-patches/issues/3806)) ([a553a13](https://github.com/ReVanced/revanced-patches/commit/a553a13c0326ef2fff7f785fed592d553a7963ce)) +* **YouTube:** Merge multiple layout patches into `Hide Layout Components` ([#3799](https://github.com/ReVanced/revanced-patches/issues/3799)) ([bbcb57a](https://github.com/ReVanced/revanced-patches/commit/bbcb57a32dfc8f031886f98b1b9701285105c579)) +* **YouTube:** Merge multiple player overlay patches into `Hide player overlay buttons` ([#3800](https://github.com/ReVanced/revanced-patches/issues/3800)) ([4ba0300](https://github.com/ReVanced/revanced-patches/commit/4ba0300590dd988bdcaa0761c4e606c1d7f86ce5)) +* **YouTube:** Support version `19.43.41` ([#3854](https://github.com/ReVanced/revanced-patches/issues/3854)) ([85de5c7](https://github.com/ReVanced/revanced-patches/commit/85de5c7d96ce2d67f6386d1438e43620d31cc645)) + + +### BREAKING CHANGES + +* Various APIs have been changed or removed. + +# [5.0.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.3...v5.0.0-dev.4) (2024-11-09) + + +### Bug Fixes + +* **YouTube - Remember video quality:** Correctly set default quality when changing from a low quality video ([#3879](https://github.com/ReVanced/revanced-patches/issues/3879)) ([ddb73e8](https://github.com/ReVanced/revanced-patches/commit/ddb73e857d7c26fd27ea995a27f53f5660d3f71c)) + +# [5.0.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.2...v5.0.0-dev.3) (2024-11-09) + + +### Bug Fixes + +* Add missing dependency to patch ([97f5240](https://github.com/ReVanced/revanced-patches/commit/97f5240d53b9978fb3745170fe03619c7c90274a)) + +# [5.0.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.1...v5.0.0-dev.2) (2024-11-09) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([084e0a5](https://github.com/ReVanced/revanced-patches/commit/084e0a527b1c75d1ef15dc706c429aa48d0ffe6b)) + +# [5.0.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.6...v5.0.0-dev.1) (2024-11-06) + + +### Bug Fixes + +* **MyFitnessPal - Hide ads:** Constrain patch to last working version ([#3847](https://github.com/ReVanced/revanced-patches/issues/3847)) ([f9fa526](https://github.com/ReVanced/revanced-patches/commit/f9fa526b04c2848175c389d6bb911aa5a245b60f)) +* **YouTube - Copy video URL:** Support A/B player layout ([0f42574](https://github.com/ReVanced/revanced-patches/commit/0f42574b7f4b1c9a48df8550c7d710093f76ce8c)) +* **YouTube - Custom branding:** Change icon correctly on 19.34+ ([#3866](https://github.com/ReVanced/revanced-patches/issues/3866)) ([2e47903](https://github.com/ReVanced/revanced-patches/commit/2e4790382546256e106a5842cd8c530f41b161e5)) +* **YouTube - Hide ads:** Hide new types of ads ([454281a](https://github.com/ReVanced/revanced-patches/commit/454281ac2108648832b7f0203f5fb7e814887835)) +* **YouTube - Hide layout components:** Remove obsolete 'Hide gray separator' ([a697701](https://github.com/ReVanced/revanced-patches/commit/a697701c5f1f9510b51e310b1ff212b609f38519)) +* **YouTube - Playback speed:** Restore old playback speed menu ([#3817](https://github.com/ReVanced/revanced-patches/issues/3817)) ([806b210](https://github.com/ReVanced/revanced-patches/commit/806b21093e3251697f03cd8804e5d5cd26070716)) +* **YouTube - Remove background playback restrictions:** Enable for Shorts as well ([#3671](https://github.com/ReVanced/revanced-patches/issues/3671)) ([7db1a77](https://github.com/ReVanced/revanced-patches/commit/7db1a7751dc47c4e36096fbdc2b3761b0ae11ccb)) +* **YouTube - Return YouTube Dislike:** Use latest separator height ([ae160a3](https://github.com/ReVanced/revanced-patches/commit/ae160a37985cc96c6de7e1a2fe5a1c83bc523046)) +* **YouTube - Seekbar:** Use latest shade of YouTube red ([4b77648](https://github.com/ReVanced/revanced-patches/commit/4b77648607a84eb29f4cae9ddb42b87084be7cd0)) +* **YouTube - Settings:** Use multiline preference title for localized languages ([#3821](https://github.com/ReVanced/revanced-patches/issues/3821)) ([ff85d49](https://github.com/ReVanced/revanced-patches/commit/ff85d490887de64eb6c6fd42e385a3e75969ff10)) +* **YouTube - SponsorBlock:** Show correct segment behavior in settings UI after importing ([e3f25a0](https://github.com/ReVanced/revanced-patches/commit/e3f25a03cd314eeae786e7660a6beacb275a6a76)) +* **YouTube - Spoof app version:** Remove obsolete 17.33.42 spoof target ([#3825](https://github.com/ReVanced/revanced-patches/issues/3825)) ([33aeba2](https://github.com/ReVanced/revanced-patches/commit/33aeba2a0895e9ecaba27ba4a3b22b86c9f1a51c)) +* **YouTube:** Merge `Restore old seekbar thumbnails` into `Seekbar thumbnails` ([#3860](https://github.com/ReVanced/revanced-patches/issues/3860)) ([e377b1e](https://github.com/ReVanced/revanced-patches/commit/e377b1e6ad93dea8e5f3829cd3894f71851887a3)) + + +### Build System + +* Bump ReVanced Patcher ([eee1692](https://github.com/ReVanced/revanced-patches/commit/eee16922779f994f5752190a20a9016ea98ec4cb)) + + +### Features + +* **YouTube - Hide player flyout menu items:** Hide stable volume ([#3827](https://github.com/ReVanced/revanced-patches/issues/3827)) ([b91e932](https://github.com/ReVanced/revanced-patches/commit/b91e932e65c04b1c1aee9a2f3dc3a73772d9c225)) +* **YouTube - Miniplayer:** Add horizontal drag gesture ([#3859](https://github.com/ReVanced/revanced-patches/issues/3859)) ([e32b19e](https://github.com/ReVanced/revanced-patches/commit/e32b19e170a5571b23547c3211b497089d0cd441)) +* **YouTube - Player flyout menu:** Hide sleep timer ([#3637](https://github.com/ReVanced/revanced-patches/issues/3637)) ([7e1bdab](https://github.com/ReVanced/revanced-patches/commit/7e1bdab520dba65682f018f819c0b7d9783f94ca)) +* **YouTube:** Add `Seekbar thumbnails` patch ([#3813](https://github.com/ReVanced/revanced-patches/issues/3813)) ([5988b75](https://github.com/ReVanced/revanced-patches/commit/5988b759752b944b6999b401faa394e2089e4003)) +* **YouTube:** Support version `19.43.41` ([#3854](https://github.com/ReVanced/revanced-patches/issues/3854)) ([85de5c7](https://github.com/ReVanced/revanced-patches/commit/85de5c7d96ce2d67f6386d1438e43620d31cc645)) + + +### BREAKING CHANGES + +* Various APIs have been changed or removed. + +# [4.18.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.5...v4.18.0-dev.6) (2024-10-24) + + +### Bug Fixes + +* **YouTube - Playback speed:** Remember playback speed with new speed menu ([#3810](https://github.com/ReVanced/revanced-patches/issues/3810)) ([c3a5e14](https://github.com/ReVanced/revanced-patches/commit/c3a5e14a0a24973a0f9956845c9e0f99c1301d42)) + +# [4.18.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.4...v4.18.0-dev.5) (2024-10-23) + + +### Features + +* **YouTube:** Hide player shopping shelf in playlists ([#3806](https://github.com/ReVanced/revanced-patches/issues/3806)) ([a553a13](https://github.com/ReVanced/revanced-patches/commit/a553a13c0326ef2fff7f785fed592d553a7963ce)) + +# [4.18.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.3...v4.18.0-dev.4) (2024-10-23) + + +### Features + +* **YouTube - Hide layout components:** Hide player shopping shelf ([#3804](https://github.com/ReVanced/revanced-patches/issues/3804)) ([1952f3b](https://github.com/ReVanced/revanced-patches/commit/1952f3b3c4bca08ed0f6e5b1117e0a6c51f00ed2)) + +# [4.18.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.2...v4.18.0-dev.3) (2024-10-22) + + +### Features + +* **YouTube:** Merge multiple layout patches into `Hide Layout Components` ([#3799](https://github.com/ReVanced/revanced-patches/issues/3799)) ([bbcb57a](https://github.com/ReVanced/revanced-patches/commit/bbcb57a32dfc8f031886f98b1b9701285105c579)) +* **YouTube:** Merge multiple player overlay patches into `Hide player overlay buttons` ([#3800](https://github.com/ReVanced/revanced-patches/issues/3800)) ([4ba0300](https://github.com/ReVanced/revanced-patches/commit/4ba0300590dd988bdcaa0761c4e606c1d7f86ce5)) + +# [4.18.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.1...v4.18.0-dev.2) (2024-10-22) + + +### Bug Fixes + +* **Twitter - Change link sharing domain:** Support latest app version ([#3786](https://github.com/ReVanced/revanced-patches/issues/3786)) ([b54592c](https://github.com/ReVanced/revanced-patches/commit/b54592cf9c5d859e1af2f02e8e6aaad7d47ab760)) + +# [4.18.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.17.0...v4.18.0-dev.1) (2024-10-21) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Move hide chips settings to Feed menu ([1ed677f](https://github.com/ReVanced/revanced-patches/commit/1ed677f7b8ba561b2bb173dcaf5d6123c22179c4)) + + +### Features + +* **YouTube:** Add `Shorts autoplay` patch ([#3794](https://github.com/ReVanced/revanced-patches/issues/3794)) ([96b5aed](https://github.com/ReVanced/revanced-patches/commit/96b5aede482f7a69d6df17864a2e17568b0da880)) + +# [4.17.0](https://github.com/ReVanced/revanced-patches/compare/v4.16.0...v4.17.0) (2024-10-20) + + +### Bug Fixes + +* **Twitter - Unlock downloads:** Make it work with latest versions ([#3782](https://github.com/ReVanced/revanced-patches/issues/3782)) ([5189122](https://github.com/ReVanced/revanced-patches/commit/5189122006b0f72d5bfb50422021c3b0f3a9ae4a)) +* **YouTube - GmsCore support:** Add more replacements ([4d39770](https://github.com/ReVanced/revanced-patches/commit/4d39770602b39b6cb399eb0d8c52947b6ebafbb0)) +* **YouTube - GmsCore support:** Remove unclear patch changes ([021d858](https://github.com/ReVanced/revanced-patches/commit/021d8584a7f5a6d1a028c5d18dc91a3b988b2884)) +* **YouTube - Hide layout components:** Adjust settings text ([#3745](https://github.com/ReVanced/revanced-patches/issues/3745)) ([13998bb](https://github.com/ReVanced/revanced-patches/commit/13998bbf95ac3cde8bf24754d60258d0ff9bc4f4)) +* **YouTube - Spoof video streams:** Fix playback for Android VR by removing invalid body as well ([#3769](https://github.com/ReVanced/revanced-patches/issues/3769)) ([5150a15](https://github.com/ReVanced/revanced-patches/commit/5150a15ad4ca73a747f0a89f933db7f2d686ec2d)) + + +### Features + +* **Backdrops - Pro unlock:** Support latest versions by removing version constraint ([a62b506](https://github.com/ReVanced/revanced-patches/commit/a62b50691c49d1ce529a7c9c4e49da0d0dd46df2)) +* **Facebook:** Add `Hide sponsored stories` patch ([#3627](https://github.com/ReVanced/revanced-patches/issues/3627)) ([214c72b](https://github.com/ReVanced/revanced-patches/commit/214c72baeb7f87f21cd2ca34301ab11fa0ff1a4f)) +* **Sync for Reddit:** Add `Fix video downloads` patch ([#3739](https://github.com/ReVanced/revanced-patches/issues/3739)) ([a47ee38](https://github.com/ReVanced/revanced-patches/commit/a47ee38b1cdd974a959008006ecaf58917addc60)) +* **Twitter:** Add `Change link sharing domain` patch ([#3753](https://github.com/ReVanced/revanced-patches/issues/3753)) ([9269a07](https://github.com/ReVanced/revanced-patches/commit/9269a076b674ecdcf478bca842238f6e30869f44)) +* **Willhaben:** Add `Hide ads` patch ([#3740](https://github.com/ReVanced/revanced-patches/issues/3740)) ([1fe3a52](https://github.com/ReVanced/revanced-patches/commit/1fe3a523e99ccfe556d88800686e34ac6ed77b2c)) +* **YouTube - Hide layout components:** Add option to hide Yoodles (YouTube Doodles) ([#3743](https://github.com/ReVanced/revanced-patches/issues/3743)) ([b8c8916](https://github.com/ReVanced/revanced-patches/commit/b8c89164cf3911ac3842df9b0d2ec42b52213505)) +* **YouTube - Hide Shorts components:** Add option to hide `Use template`, `Upcoming`, `Green screen` buttons ([#3752](https://github.com/ReVanced/revanced-patches/issues/3752)) ([f71c406](https://github.com/ReVanced/revanced-patches/commit/f71c4068bc646d02954b59fac4756f1419c55dbe)) +* **YouTube - Hide Shorts components:** Add option to hide like fountain ([#3731](https://github.com/ReVanced/revanced-patches/issues/3731)) ([00a99dd](https://github.com/ReVanced/revanced-patches/commit/00a99dd13be6e5c44fa691d74c92b23ce6ba659d)) +* **YouTube - Hide Shorts components:** Hide `Hashtag` button ([#3787](https://github.com/ReVanced/revanced-patches/issues/3787)) ([828a634](https://github.com/ReVanced/revanced-patches/commit/828a634667c4005a90f3e469ad2c5d69387f0760)) +* **YouTube:** Support versions `19.25` and `19.34` ([#3629](https://github.com/ReVanced/revanced-patches/issues/3629)) ([049e7f0](https://github.com/ReVanced/revanced-patches/commit/049e7f081358d2e1bf87d30e87b01c61b5eeafcc)) + + +### Performance Improvements + +* **YouTube - GmsCore support:** Improve performance by using hashsets ([2c5d390](https://github.com/ReVanced/revanced-patches/commit/2c5d390fb1275dc3da5a3b912e221b7d594a1561)) + +# [4.17.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.12...v4.17.0-dev.13) (2024-10-19) + + +### Bug Fixes + +* **YouTube - GmsCore support:** Add more replacements ([4d39770](https://github.com/ReVanced/revanced-patches/commit/4d39770602b39b6cb399eb0d8c52947b6ebafbb0)) +* **YouTube - GmsCore support:** Remove unclear patch changes ([021d858](https://github.com/ReVanced/revanced-patches/commit/021d8584a7f5a6d1a028c5d18dc91a3b988b2884)) + + +### Performance Improvements + +* **YouTube - GmsCore support:** Improve performance by using hashsets ([2c5d390](https://github.com/ReVanced/revanced-patches/commit/2c5d390fb1275dc3da5a3b912e221b7d594a1561)) + +# [4.17.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.11...v4.17.0-dev.12) (2024-10-19) + + +### Features + +* **YouTube - Hide Shorts components:** Hide `Hashtag` button ([#3787](https://github.com/ReVanced/revanced-patches/issues/3787)) ([828a634](https://github.com/ReVanced/revanced-patches/commit/828a634667c4005a90f3e469ad2c5d69387f0760)) + +# [4.17.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.10...v4.17.0-dev.11) (2024-10-19) + + +### Features + +* **YouTube:** Support versions `19.25` and `19.34` ([#3629](https://github.com/ReVanced/revanced-patches/issues/3629)) ([049e7f0](https://github.com/ReVanced/revanced-patches/commit/049e7f081358d2e1bf87d30e87b01c61b5eeafcc)) + +# [4.17.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.9...v4.17.0-dev.10) (2024-10-17) + + +### Features + +* **Facebook:** Add `Hide sponsored stories` patch ([#3627](https://github.com/ReVanced/revanced-patches/issues/3627)) ([214c72b](https://github.com/ReVanced/revanced-patches/commit/214c72baeb7f87f21cd2ca34301ab11fa0ff1a4f)) + +# [4.17.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.8...v4.17.0-dev.9) (2024-10-17) + + +### Features + +* **Sync for Reddit:** Add `Fix video downloads` patch ([#3739](https://github.com/ReVanced/revanced-patches/issues/3739)) ([a47ee38](https://github.com/ReVanced/revanced-patches/commit/a47ee38b1cdd974a959008006ecaf58917addc60)) + +# [4.17.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.7...v4.17.0-dev.8) (2024-10-17) + + +### Features + +* **Twitter:** Add `Change link sharing domain` patch ([#3753](https://github.com/ReVanced/revanced-patches/issues/3753)) ([9269a07](https://github.com/ReVanced/revanced-patches/commit/9269a076b674ecdcf478bca842238f6e30869f44)) + +# [4.17.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.6...v4.17.0-dev.7) (2024-10-17) + + +### Bug Fixes + +* **Twitter - Unlock downloads:** Make it work with latest versions ([#3782](https://github.com/ReVanced/revanced-patches/issues/3782)) ([5189122](https://github.com/ReVanced/revanced-patches/commit/5189122006b0f72d5bfb50422021c3b0f3a9ae4a)) +* **YouTube - Spoof video streams:** Fix playback for Android VR by removing invalid body as well ([#3769](https://github.com/ReVanced/revanced-patches/issues/3769)) ([5150a15](https://github.com/ReVanced/revanced-patches/commit/5150a15ad4ca73a747f0a89f933db7f2d686ec2d)) + +# [4.17.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.5...v4.17.0-dev.6) (2024-10-14) + + +### Features + +* **YouTube - Hide Shorts components:** Add option to hide `Use template`, `Upcoming`, `Green screen` buttons ([#3752](https://github.com/ReVanced/revanced-patches/issues/3752)) ([f71c406](https://github.com/ReVanced/revanced-patches/commit/f71c4068bc646d02954b59fac4756f1419c55dbe)) + +# [4.17.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.4...v4.17.0-dev.5) (2024-10-07) + + +### Features + +* **Backdrops - Pro unlock:** Support latest versions by removing version constraint ([a62b506](https://github.com/ReVanced/revanced-patches/commit/a62b50691c49d1ce529a7c9c4e49da0d0dd46df2)) + +# [4.17.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.3...v4.17.0-dev.4) (2024-10-06) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Adjust settings text ([#3745](https://github.com/ReVanced/revanced-patches/issues/3745)) ([13998bb](https://github.com/ReVanced/revanced-patches/commit/13998bbf95ac3cde8bf24754d60258d0ff9bc4f4)) + +# [4.17.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.2...v4.17.0-dev.3) (2024-10-06) + + +### Features + +* **YouTube - Hide layout components:** Add option to hide Yoodles (YouTube Doodles) ([#3743](https://github.com/ReVanced/revanced-patches/issues/3743)) ([b8c8916](https://github.com/ReVanced/revanced-patches/commit/b8c89164cf3911ac3842df9b0d2ec42b52213505)) + +# [4.17.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.17.0-dev.1...v4.17.0-dev.2) (2024-10-05) + + +### Features + +* **Willhaben:** Add `Hide ads` patch ([#3740](https://github.com/ReVanced/revanced-patches/issues/3740)) ([1fe3a52](https://github.com/ReVanced/revanced-patches/commit/1fe3a523e99ccfe556d88800686e34ac6ed77b2c)) + +# [4.17.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.16.1-dev.1...v4.17.0-dev.1) (2024-10-02) + + +### Features + +* **YouTube - Hide Shorts components:** Add option to hide like fountain ([#3731](https://github.com/ReVanced/revanced-patches/issues/3731)) ([00a99dd](https://github.com/ReVanced/revanced-patches/commit/00a99dd13be6e5c44fa691d74c92b23ce6ba659d)) + +## [4.16.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.16.0...v4.16.1-dev.1) (2024-10-01) + +# [4.16.0](https://github.com/ReVanced/revanced-patches/compare/v4.15.0...v4.16.0) (2024-09-30) + + +### Bug Fixes + +* **Soundcloud:** Support latest versions ([#3702](https://github.com/ReVanced/revanced-patches/issues/3702)) ([099ac5e](https://github.com/ReVanced/revanced-patches/commit/099ac5ea2cf55633a7c6a7e6f8e963599bcd5784)) +* **Twitter - Open links with app chooser:** Fix incorrect version in compatibility list ([#3683](https://github.com/ReVanced/revanced-patches/issues/3683)) ([adafe85](https://github.com/ReVanced/revanced-patches/commit/adafe85d77f6a0031a5523b9b7da69475959d78d)) +* **YouTube - SponsorBlock:** Fade out SB buttons without overlapping other buttons ([#3719](https://github.com/ReVanced/revanced-patches/issues/3719)) ([bf96108](https://github.com/ReVanced/revanced-patches/commit/bf9610894f0a9f9e751e2eed5b825c5d327a722c)) +* **YouTube:** Show video chapter titles without clipping when overlay buttons are enabled ([#3674](https://github.com/ReVanced/revanced-patches/issues/3674)) ([4b88c31](https://github.com/ReVanced/revanced-patches/commit/4b88c316ed90c56e83e2aee266561833b36fc37d)) + + +### Features + +* **Google Photos:** Restore hidden 'Back up while charging' toggle ([#3678](https://github.com/ReVanced/revanced-patches/issues/3678)) ([f9e19ce](https://github.com/ReVanced/revanced-patches/commit/f9e19ce6e9185fdf31b2b0d5f2934f6e8a544b8e)) +* **YouTube - Disable precise seeking gesture:** Hide "pull up" label that shows up when swiping ([#3668](https://github.com/ReVanced/revanced-patches/issues/3668)) ([3fa8af9](https://github.com/ReVanced/revanced-patches/commit/3fa8af9fe534b59ad093c36f1927f56f549a330d)) +* **YouTube - Hide Shorts components:** Add `Hide save music`, `Hide stickers` ([#3710](https://github.com/ReVanced/revanced-patches/issues/3710)) ([8c99321](https://github.com/ReVanced/revanced-patches/commit/8c99321df4db696156330fc90dd547c1345d880e)) +* **YouTube - Hide Shorts components:** Add patch option to hide Shorts app shortcut (long press app icon) ([#3699](https://github.com/ReVanced/revanced-patches/issues/3699)) ([0d4e1f5](https://github.com/ReVanced/revanced-patches/commit/0d4e1f5d03cf3dcc06fd41165e26a1ce901b976b)) +* **YouTube - Hide Shorts components:** Add patch option to hide Shorts from app launcher widget Beta ([#3707](https://github.com/ReVanced/revanced-patches/issues/3707)) ([838f183](https://github.com/ReVanced/revanced-patches/commit/838f1834a5df547ce2c3217b874c0594b6878a67)) + +# [4.16.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.16.0-dev.6...v4.16.0-dev.7) (2024-09-29) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Fade out SB buttons without overlapping other buttons ([#3719](https://github.com/ReVanced/revanced-patches/issues/3719)) ([bf96108](https://github.com/ReVanced/revanced-patches/commit/bf9610894f0a9f9e751e2eed5b825c5d327a722c)) + +# [4.16.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.16.0-dev.5...v4.16.0-dev.6) (2024-09-29) + + +### Features + +* **YouTube - Hide Shorts components:** Add `Hide save music`, `Hide stickers` ([#3710](https://github.com/ReVanced/revanced-patches/issues/3710)) ([8c99321](https://github.com/ReVanced/revanced-patches/commit/8c99321df4db696156330fc90dd547c1345d880e)) + +# [4.16.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.16.0-dev.4...v4.16.0-dev.5) (2024-09-29) + + +### Features + +* **YouTube - Disable precise seeking gesture:** Hide "pull up" label that shows up when swiping ([#3668](https://github.com/ReVanced/revanced-patches/issues/3668)) ([3fa8af9](https://github.com/ReVanced/revanced-patches/commit/3fa8af9fe534b59ad093c36f1927f56f549a330d)) + +# [4.16.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.16.0-dev.3...v4.16.0-dev.4) (2024-09-29) + + +### Bug Fixes + +* **Soundcloud:** Support latest versions ([#3702](https://github.com/ReVanced/revanced-patches/issues/3702)) ([099ac5e](https://github.com/ReVanced/revanced-patches/commit/099ac5ea2cf55633a7c6a7e6f8e963599bcd5784)) + +# [4.16.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.16.0-dev.2...v4.16.0-dev.3) (2024-09-29) + + +### Features + +* **Google Photos:** Restore hidden 'Back up while charging' toggle ([#3678](https://github.com/ReVanced/revanced-patches/issues/3678)) ([f9e19ce](https://github.com/ReVanced/revanced-patches/commit/f9e19ce6e9185fdf31b2b0d5f2934f6e8a544b8e)) + +# [4.16.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.16.0-dev.1...v4.16.0-dev.2) (2024-09-28) + + +### Features + +* **YouTube - Hide Shorts components:** Add patch option to hide Shorts from app launcher widget Beta ([#3707](https://github.com/ReVanced/revanced-patches/issues/3707)) ([838f183](https://github.com/ReVanced/revanced-patches/commit/838f1834a5df547ce2c3217b874c0594b6878a67)) + +# [4.16.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.15.1-dev.2...v4.16.0-dev.1) (2024-09-27) + + +### Features + +* **YouTube - Hide Shorts components:** Add patch option to hide Shorts app shortcut (long press app icon) ([#3699](https://github.com/ReVanced/revanced-patches/issues/3699)) ([0d4e1f5](https://github.com/ReVanced/revanced-patches/commit/0d4e1f5d03cf3dcc06fd41165e26a1ce901b976b)) + +## [4.15.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.15.1-dev.1...v4.15.1-dev.2) (2024-09-23) + + +### Bug Fixes + +* **YouTube:** Show video chapter titles without clipping when overlay buttons are enabled ([#3674](https://github.com/ReVanced/revanced-patches/issues/3674)) ([4b88c31](https://github.com/ReVanced/revanced-patches/commit/4b88c316ed90c56e83e2aee266561833b36fc37d)) + +## [4.15.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.15.0...v4.15.1-dev.1) (2024-09-23) + + +### Bug Fixes + +* **Twitter - Open links with app chooser:** Fix incorrect version in compatibility list ([#3683](https://github.com/ReVanced/revanced-patches/issues/3683)) ([adafe85](https://github.com/ReVanced/revanced-patches/commit/adafe85d77f6a0031a5523b9b7da69475959d78d)) + +# [4.15.0](https://github.com/ReVanced/revanced-patches/compare/v4.14.1...v4.15.0) (2024-09-23) + + +### Bug Fixes + +* **TikTok - Playback speed:** Prevent crash by fixing invalid patch ([82d53cb](https://github.com/ReVanced/revanced-patches/commit/82d53cbc3bbfa585ba4337fdfaec9f0f19c802e6)) +* **TikTok - Settings:** Prevent crash by fixing invalid patch ([8074032](https://github.com/ReVanced/revanced-patches/commit/8074032fad3eff1c03296a882d2e2820da99b592)) +* **Twitter - Open links with app chooser:** Constrain patch to last working version `10.48.0-release` ([b9955d5](https://github.com/ReVanced/revanced-patches/commit/b9955d5ff6e456593b01f0f25d80ff660d02082a)) +* **YouTube - Spoof video streams:** Change default client type to Android VR ([74c8637](https://github.com/ReVanced/revanced-patches/commit/74c8637943347078955f51325bc6af92a35d4463)) +* **YouTube - Spoof video streams:** Change default client type to Android VR ([#3672](https://github.com/ReVanced/revanced-patches/issues/3672)) ([a3306f6](https://github.com/ReVanced/revanced-patches/commit/a3306f6717a09b734354f00363a96abad0ae14e7)) + + +### Features + +* **TikTok:** Bump patches to support the latest version 36.5.4 ([e5dcb72](https://github.com/ReVanced/revanced-patches/commit/e5dcb72597092fb32003f11fdf6f861ede4e7ff3)) + +# [4.15.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.14.2-dev.2...v4.15.0-dev.1) (2024-09-22) + + +### Features + +* **TikTok:** Bump patches to support the latest version 36.5.4 ([e5dcb72](https://github.com/ReVanced/revanced-patches/commit/e5dcb72597092fb32003f11fdf6f861ede4e7ff3)) + +## [4.14.2-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.14.2-dev.1...v4.14.2-dev.2) (2024-09-21) + + +### Bug Fixes + +* **YouTube - Spoof video streams:** Change default client type to Android VR ([74c8637](https://github.com/ReVanced/revanced-patches/commit/74c8637943347078955f51325bc6af92a35d4463)) +* **YouTube - Spoof video streams:** Change default client type to Android VR ([#3672](https://github.com/ReVanced/revanced-patches/issues/3672)) ([a3306f6](https://github.com/ReVanced/revanced-patches/commit/a3306f6717a09b734354f00363a96abad0ae14e7)) + +## [4.14.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.14.1...v4.14.2-dev.1) (2024-09-21) + + +### Bug Fixes + +* **TikTok - Playback speed:** Prevent crash by fixing invalid patch ([82d53cb](https://github.com/ReVanced/revanced-patches/commit/82d53cbc3bbfa585ba4337fdfaec9f0f19c802e6)) +* **TikTok - Settings:** Prevent crash by fixing invalid patch ([8074032](https://github.com/ReVanced/revanced-patches/commit/8074032fad3eff1c03296a882d2e2820da99b592)) +* **Twitter - Open links with app chooser:** Constrain patch to last working version `10.48.0-release` ([b9955d5](https://github.com/ReVanced/revanced-patches/commit/b9955d5ff6e456593b01f0f25d80ff660d02082a)) + +## [4.14.1](https://github.com/ReVanced/revanced-patches/compare/v4.14.0...v4.14.1) (2024-09-18) + + +### Bug Fixes + +* **YouTube - Check environment:** Only use fields available since Android 8 ([#3655](https://github.com/ReVanced/revanced-patches/issues/3655)) ([4413533](https://github.com/ReVanced/revanced-patches/commit/441353306572340131030e1c4fee1ab6acb63cd9)) + +## [4.14.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.14.0...v4.14.1-dev.1) (2024-09-18) + + +### Bug Fixes + +* **YouTube - Check environment:** Only use fields available since Android 8 ([#3655](https://github.com/ReVanced/revanced-patches/issues/3655)) ([4413533](https://github.com/ReVanced/revanced-patches/commit/441353306572340131030e1c4fee1ab6acb63cd9)) + +# [4.14.0](https://github.com/ReVanced/revanced-patches/compare/v4.13.3...v4.14.0) (2024-09-18) + + +### Bug Fixes + +* **Pixiv - Hide ads:** Fix for latest version ([#3616](https://github.com/ReVanced/revanced-patches/issues/3616)) ([98956e8](https://github.com/ReVanced/revanced-patches/commit/98956e8f1a41347bb435720bbf984969469a7110)) +* **Soundcloud - Hide ads:** Support latest version ([#3628](https://github.com/ReVanced/revanced-patches/issues/3628)) ([66e7e33](https://github.com/ReVanced/revanced-patches/commit/66e7e33efce9b702fdfcc2b9803e9da8491c1f08)) +* **SwissID:** Rename `Remove Google Play Integrity Integrity check` to `Remove Google Play Integrity check` ([#3558](https://github.com/ReVanced/revanced-patches/issues/3558)) ([0f5a771](https://github.com/ReVanced/revanced-patches/commit/0f5a771a5cff5684b4a8fd317f4938fe2cf3cbbe)) +* **YouTube - ReturnYouTubeDislike:** Show estimated like count for videos with hidden likes ([#3601](https://github.com/ReVanced/revanced-patches/issues/3601)) ([005be82](https://github.com/ReVanced/revanced-patches/commit/005be82d71b2a42387b1b57035930b20f4663794)) +* **YouTube - SponsorBlock:** Add summary text to 'view my segments' button ([df80b9f](https://github.com/ReVanced/revanced-patches/commit/df80b9f92f0d981b9a40b7756d74f8ccc3dcb1e9)) +* **YouTube - SponsorBlock:** Handle if the user enters an invalid number into any SB settings ([37b3dd1](https://github.com/ReVanced/revanced-patches/commit/37b3dd1e789f8bb16fa1b9dd582e39c89dbe730c)) +* **YouTube:** Fix issues related to playback by replace streaming data ([#3582](https://github.com/ReVanced/revanced-patches/issues/3582)) ([dfa94d7](https://github.com/ReVanced/revanced-patches/commit/dfa94d70f65150d6ef24ea6378b8e6a317055186)) + + +### Features + +* Add `Change data directory location` patch ([#3602](https://github.com/ReVanced/revanced-patches/issues/3602)) ([5998029](https://github.com/ReVanced/revanced-patches/commit/59980292809cc0626bf49a160eeb05a1523c4eda)) +* Add `Check environment` patch ([#3610](https://github.com/ReVanced/revanced-patches/issues/3610)) ([fbcbdaf](https://github.com/ReVanced/revanced-patches/commit/fbcbdafa4938a35b5fdec46aae7b250a84b9c139)) +* **Duolingo:** Add `Disable ads` and `Enable debug menu` patch ([#3422](https://github.com/ReVanced/revanced-patches/issues/3422)) ([d0a8599](https://github.com/ReVanced/revanced-patches/commit/d0a8599f76ce653e5d7c98069ad3c58b9ab9c5eb)) +* **Sync for Reddit:** Add `Fix /user/ endpoint` patch ([46d11f3](https://github.com/ReVanced/revanced-patches/commit/46d11f3530fcdae9ed08b7e93aac235638a92dff)) +* **Sync for Reddit:** Rename patch to `Use /user/ endpoint` ([98ead49](https://github.com/ReVanced/revanced-patches/commit/98ead493380932cb105530f4ba992673fd364d82)) +* **YouTube - Hide Shorts components:** Hide 'Use this sound' button ([#3647](https://github.com/ReVanced/revanced-patches/issues/3647)) ([33fc090](https://github.com/ReVanced/revanced-patches/commit/33fc09061431d4aa457d743c09a0de31ec566df1)) +* **YouTube - Keyword filter:** Add syntax to match whole keywords and not substrings ([#3592](https://github.com/ReVanced/revanced-patches/issues/3592)) ([f5fb351](https://github.com/ReVanced/revanced-patches/commit/f5fb3512cfafe214ba6a6d25ba0825ae1884a0ff)) +* **YouTube - Spoof client:** Allow forcing AVC codec with iOS ([#3570](https://github.com/ReVanced/revanced-patches/issues/3570)) ([1a49d1f](https://github.com/ReVanced/revanced-patches/commit/1a49d1f3c2a343d05d0abc07c143add486246fd0)) +* **YouTube Music:** Make working patches compatible with latest versions ([#3556](https://github.com/ReVanced/revanced-patches/issues/3556)) ([12f6f19](https://github.com/ReVanced/revanced-patches/commit/12f6f1966ad04631451940f7b64d785c3ef481a0)) +* **YouTube:** Add donation link to settings about screen ([#3626](https://github.com/ReVanced/revanced-patches/issues/3626)) ([0684ab5](https://github.com/ReVanced/revanced-patches/commit/0684ab5f183631de5720352049cfd293daa58eb0)) + +# [4.14.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.14...v4.14.0-dev.15) (2024-09-17) + + +### Bug Fixes + +* **YouTube:** Fix issues related to playback by replace streaming data ([#3582](https://github.com/ReVanced/revanced-patches/issues/3582)) ([dfa94d7](https://github.com/ReVanced/revanced-patches/commit/dfa94d70f65150d6ef24ea6378b8e6a317055186)) + +# [4.14.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.13...v4.14.0-dev.14) (2024-09-17) + + +### Features + +* **YouTube Music:** Make working patches compatible with latest versions ([#3556](https://github.com/ReVanced/revanced-patches/issues/3556)) ([12f6f19](https://github.com/ReVanced/revanced-patches/commit/12f6f1966ad04631451940f7b64d785c3ef481a0)) + +# [4.14.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.12...v4.14.0-dev.13) (2024-09-17) + + +### Features + +* **YouTube - Hide Shorts components:** Hide 'Use this sound' button ([#3647](https://github.com/ReVanced/revanced-patches/issues/3647)) ([33fc090](https://github.com/ReVanced/revanced-patches/commit/33fc09061431d4aa457d743c09a0de31ec566df1)) + +# [4.14.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.11...v4.14.0-dev.12) (2024-09-14) + + +### Bug Fixes + +* **Soundcloud - Hide ads:** Support latest version ([#3628](https://github.com/ReVanced/revanced-patches/issues/3628)) ([66e7e33](https://github.com/ReVanced/revanced-patches/commit/66e7e33efce9b702fdfcc2b9803e9da8491c1f08)) + +# [4.14.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.10...v4.14.0-dev.11) (2024-09-12) + + +### Features + +* **Sync for Reddit:** Rename patch to `Use /user/ endpoint` ([98ead49](https://github.com/ReVanced/revanced-patches/commit/98ead493380932cb105530f4ba992673fd364d82)) + +# [4.14.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.9...v4.14.0-dev.10) (2024-09-12) + + +### Features + +* **Sync for Reddit:** Add `Fix /user/ endpoint` patch ([46d11f3](https://github.com/ReVanced/revanced-patches/commit/46d11f3530fcdae9ed08b7e93aac235638a92dff)) + +# [4.14.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.8...v4.14.0-dev.9) (2024-09-09) + + +### Features + +* **YouTube:** Add donation link to settings about screen ([#3626](https://github.com/ReVanced/revanced-patches/issues/3626)) ([0684ab5](https://github.com/ReVanced/revanced-patches/commit/0684ab5f183631de5720352049cfd293daa58eb0)) + +# [4.14.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.7...v4.14.0-dev.8) (2024-09-09) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Add summary text to 'view my segments' button ([df80b9f](https://github.com/ReVanced/revanced-patches/commit/df80b9f92f0d981b9a40b7756d74f8ccc3dcb1e9)) + +# [4.14.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.6...v4.14.0-dev.7) (2024-09-06) + + +### Features + +* Add `Check environment` patch ([#3610](https://github.com/ReVanced/revanced-patches/issues/3610)) ([fbcbdaf](https://github.com/ReVanced/revanced-patches/commit/fbcbdafa4938a35b5fdec46aae7b250a84b9c139)) + +# [4.14.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.5...v4.14.0-dev.6) (2024-09-06) + + +### Features + +* Add `Change data directory location` patch ([#3602](https://github.com/ReVanced/revanced-patches/issues/3602)) ([5998029](https://github.com/ReVanced/revanced-patches/commit/59980292809cc0626bf49a160eeb05a1523c4eda)) + +# [4.14.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.4...v4.14.0-dev.5) (2024-09-06) + + +### Bug Fixes + +* **Pixiv - Hide ads:** Fix for latest version ([#3616](https://github.com/ReVanced/revanced-patches/issues/3616)) ([98956e8](https://github.com/ReVanced/revanced-patches/commit/98956e8f1a41347bb435720bbf984969469a7110)) + +# [4.14.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.3...v4.14.0-dev.4) (2024-09-01) + + +### Bug Fixes + +* **YouTube - ReturnYouTubeDislike:** Show estimated like count for videos with hidden likes ([#3601](https://github.com/ReVanced/revanced-patches/issues/3601)) ([005be82](https://github.com/ReVanced/revanced-patches/commit/005be82d71b2a42387b1b57035930b20f4663794)) + +# [4.14.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.2...v4.14.0-dev.3) (2024-08-30) + + +### Features + +* **YouTube - Keyword filter:** Add syntax to match whole keywords and not substrings ([#3592](https://github.com/ReVanced/revanced-patches/issues/3592)) ([f5fb351](https://github.com/ReVanced/revanced-patches/commit/f5fb3512cfafe214ba6a6d25ba0825ae1884a0ff)) + +# [4.14.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.14.0-dev.1...v4.14.0-dev.2) (2024-08-24) + + +### Features + +* **Duolingo:** Add `Disable ads` and `Enable debug menu` patch ([#3422](https://github.com/ReVanced/revanced-patches/issues/3422)) ([d0a8599](https://github.com/ReVanced/revanced-patches/commit/d0a8599f76ce653e5d7c98069ad3c58b9ab9c5eb)) + +# [4.14.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.13.4-dev.2...v4.14.0-dev.1) (2024-08-22) + + +### Features + +* **YouTube - Spoof client:** Allow forcing AVC codec with iOS ([#3570](https://github.com/ReVanced/revanced-patches/issues/3570)) ([1a49d1f](https://github.com/ReVanced/revanced-patches/commit/1a49d1f3c2a343d05d0abc07c143add486246fd0)) + +## [4.13.4-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.13.4-dev.1...v4.13.4-dev.2) (2024-08-20) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Handle if the user enters an invalid number into any SB settings ([37b3dd1](https://github.com/ReVanced/revanced-patches/commit/37b3dd1e789f8bb16fa1b9dd582e39c89dbe730c)) + +## [4.13.4-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.13.3...v4.13.4-dev.1) (2024-08-18) + + +### Bug Fixes + +* **SwissID:** Rename `Remove Google Play Integrity Integrity check` to `Remove Google Play Integrity check` ([#3558](https://github.com/ReVanced/revanced-patches/issues/3558)) ([0f5a771](https://github.com/ReVanced/revanced-patches/commit/0f5a771a5cff5684b4a8fd317f4938fe2cf3cbbe)) + +## [4.13.3](https://github.com/ReVanced/revanced-patches/compare/v4.13.2...v4.13.3) (2024-08-15) + + +### Bug Fixes + +* **YouTube:** Remove translated string that breaks patching ([a48c2db](https://github.com/ReVanced/revanced-patches/commit/a48c2db53d84767c8fd5d569f9ce1c46c2bfd9a1)) + +## [4.13.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.13.2...v4.13.3-dev.1) (2024-08-15) + + +### Bug Fixes + +* **YouTube:** Remove translated string that breaks patching ([a48c2db](https://github.com/ReVanced/revanced-patches/commit/a48c2db53d84767c8fd5d569f9ce1c46c2bfd9a1)) + +## [4.13.2](https://github.com/ReVanced/revanced-patches/compare/v4.13.1...v4.13.2) (2024-08-15) + + +### Bug Fixes + +* **YouTube - GmsCore Support:** Fix patch exception by using correct patch offset ([#3543](https://github.com/ReVanced/revanced-patches/issues/3543)) ([b2b8454](https://github.com/ReVanced/revanced-patches/commit/b2b8454aa992bcb217fb03eb4de5532e0a9bd354)) + +## [4.13.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.13.1...v4.13.2-dev.1) (2024-08-15) + + +### Bug Fixes + +* **YouTube - GmsCore Support:** Fix patch exception by using correct patch offset ([#3543](https://github.com/ReVanced/revanced-patches/issues/3543)) ([b2b8454](https://github.com/ReVanced/revanced-patches/commit/b2b8454aa992bcb217fb03eb4de5532e0a9bd354)) + +## [4.13.1](https://github.com/ReVanced/revanced-patches/compare/v4.13.0...v4.13.1) (2024-08-15) + + +### Bug Fixes + +* **YouTube - Check watch history domain name resolution:** Add compatibility field ([6c598f0](https://github.com/ReVanced/revanced-patches/commit/6c598f084ed90ee1318e4c66d8c1751c797b8e3b)) + +## [4.13.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.13.0...v4.13.1-dev.1) (2024-08-15) + + +### Bug Fixes + +* **YouTube - Check watch history domain name resolution:** Add compatibility field ([6c598f0](https://github.com/ReVanced/revanced-patches/commit/6c598f084ed90ee1318e4c66d8c1751c797b8e3b)) + +# [4.13.0](https://github.com/ReVanced/revanced-patches/compare/v4.12.0...v4.13.0) (2024-08-15) + + +### Bug Fixes + +* **YouTube - GmsCore support:** Fix notifications not working by using the correct permissions ([19ddae2](https://github.com/ReVanced/revanced-patches/commit/19ddae2d15e513e18eb1556c468cd94bd197685b)) + + +### Features + +* **Google Photos:** Add `Spoof features` patch ([#3459](https://github.com/ReVanced/revanced-patches/issues/3459)) ([7c218cd](https://github.com/ReVanced/revanced-patches/commit/7c218cd168aa72eb99bcb47d12dfa45616e8ad88)) +* **SCB Easy:** Remove broken `Remove debugging detection` patch ([#3518](https://github.com/ReVanced/revanced-patches/issues/3518)) ([f4e23cb](https://github.com/ReVanced/revanced-patches/commit/f4e23cbb8a24638318d8cee20a1991c51855d9d2)) +* **YouTube:** Add `Check watch history domain name resolution` patch ([#3537](https://github.com/ReVanced/revanced-patches/issues/3537)) ([2af1425](https://github.com/ReVanced/revanced-patches/commit/2af142525cda07a131335faadd4b3889979fd077)) + +# [4.13.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.13.0-dev.1...v4.13.0-dev.2) (2024-08-15) + + +### Features + +* **YouTube:** Add `Check watch history domain name resolution` patch ([#3537](https://github.com/ReVanced/revanced-patches/issues/3537)) ([2af1425](https://github.com/ReVanced/revanced-patches/commit/2af142525cda07a131335faadd4b3889979fd077)) + +# [4.13.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.12.1-dev.1...v4.13.0-dev.1) (2024-08-15) + + +### Features + +* **Google Photos:** Add `Spoof features` patch ([#3459](https://github.com/ReVanced/revanced-patches/issues/3459)) ([7c218cd](https://github.com/ReVanced/revanced-patches/commit/7c218cd168aa72eb99bcb47d12dfa45616e8ad88)) +* **SCB Easy:** Remove broken `Remove debugging detection` patch ([#3518](https://github.com/ReVanced/revanced-patches/issues/3518)) ([f4e23cb](https://github.com/ReVanced/revanced-patches/commit/f4e23cbb8a24638318d8cee20a1991c51855d9d2)) + +## [4.12.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.12.0...v4.12.1-dev.1) (2024-08-15) + + +### Bug Fixes + +* **YouTube - GmsCore support:** Fix notifications not working by using the correct permissions ([19ddae2](https://github.com/ReVanced/revanced-patches/commit/19ddae2d15e513e18eb1556c468cd94bd197685b)) + +# [4.12.0](https://github.com/ReVanced/revanced-patches/compare/v4.11.0...v4.12.0) (2024-08-06) + + +### Bug Fixes + +* **Google Photos - GmsCore support:** Fix by checking first if a method exists before trying to patch it ([acf38ca](https://github.com/ReVanced/revanced-patches/commit/acf38cafae5eb9896b43f3a6cbd808ac273cd081)) +* **Instagram - Hide ads:** Restore compatibility with latest version by fixing fingerprint ([#3455](https://github.com/ReVanced/revanced-patches/issues/3455)) ([4505fa4](https://github.com/ReVanced/revanced-patches/commit/4505fa4138bb55c8957790239c01b8dda63d6cdd)) +* **Messenger - Disable switching emoji to sticker:** Constrain to last working version `439.0.0.29.119` ([6207c31](https://github.com/ReVanced/revanced-patches/commit/6207c314c657a1188d1081b0a196a61e49cad83b)) +* **SoundCloud - Enable offline sync:** Stop crashing by reversing order of patching instructions from last to first to retain indices ([63b6ced](https://github.com/ReVanced/revanced-patches/commit/63b6cede5fa5bcf377ced422da4e861996a41f0d)) +* **YouTube - Bypass image region restrictions:** Move setting to `Misc` menu ([094ae59](https://github.com/ReVanced/revanced-patches/commit/094ae59fc92663fff6c5d6f5cbece41822a326f9)) +* **YouTube - Client Spoof:** Restore missing high qualities by spoofing the iOS client user agent ([#3468](https://github.com/ReVanced/revanced-patches/issues/3468)) ([0e6ae5f](https://github.com/ReVanced/revanced-patches/commit/0e6ae5fee752a76604cf9b95f9a76c0cbe5f7dae)) +* **YouTube - Hide keyword content:** Do not hide flyout menu ([687c9f7](https://github.com/ReVanced/revanced-patches/commit/687c9f7eb03cca5f7b3486f07f2e3453ebc77faf)) +* **YouTube - SponsorBlock:** Correctly show minute timestamp when creating a new segment ([d74c366](https://github.com/ReVanced/revanced-patches/commit/d74c366dbf5f25c20fbfc5a0157c3c15dda82a16)) +* **YouTube - SponsorBlock:** Improve create segment manual seek accuracy ([#3491](https://github.com/ReVanced/revanced-patches/issues/3491)) ([1544981](https://github.com/ReVanced/revanced-patches/commit/15449819ff74b636fb2fa6aacd770142c51d2e5d)) +* **YouTube - Spoof client:** Fix tracking history on brand accounts ([#3480](https://github.com/ReVanced/revanced-patches/issues/3480)) ([69c1f16](https://github.com/ReVanced/revanced-patches/commit/69c1f16f7eb0d5759a44f7f7a09b1757ce8f61dd)) +* **YouTube - Spoof client:** Restore livestream audio only playback with iOS spoofing ([#3504](https://github.com/ReVanced/revanced-patches/issues/3504)) ([90d3288](https://github.com/ReVanced/revanced-patches/commit/90d32880906787d82c4b9a7a1099b46dff3a0870)) + + +### Features + +* Add `Hide mock location` patch ([#3417](https://github.com/ReVanced/revanced-patches/issues/3417)) ([5f81b40](https://github.com/ReVanced/revanced-patches/commit/5f81b40e7d5567fb5689d08ccc9caeaa267c3143)) +* Add `Spoof build info` patch ([e7829b4](https://github.com/ReVanced/revanced-patches/commit/e7829b41e782c9feda23b9d6acf48bae277d24d9)) +* **Boost for Reddit:** Add `Disable ads` patch ([#3474](https://github.com/ReVanced/revanced-patches/issues/3474)) ([b292c20](https://github.com/ReVanced/revanced-patches/commit/b292c200bf4ea5b4f71d96690ac011e7843552f0)) +* **CandyLink:** Remove non-functional `Unlock pro` patch ([7ae9f8f](https://github.com/ReVanced/revanced-patches/commit/7ae9f8fa0a349b91853e9554f18e564ca6ff887c)) +* **Expense Manager:** Remove non-functional `Unlock pro` patch ([ebbcac7](https://github.com/ReVanced/revanced-patches/commit/ebbcac74fd8598daebb4be0bd7c430c41333e2d4)) +* **Google News:** Add `Enable CustomTabs` and `GmsCore support` patch ([#3111](https://github.com/ReVanced/revanced-patches/issues/3111)) ([ad59096](https://github.com/ReVanced/revanced-patches/commit/ad590962275f888b335252ad5bed0f34e959d3c7)) +* **Google Photos:** Add `GmsCore support` patch ([#3414](https://github.com/ReVanced/revanced-patches/issues/3414)) ([24528e0](https://github.com/ReVanced/revanced-patches/commit/24528e0a6eec17ce0a3c52f8862585933615ad28)) +* **Instagram:** Remove unnecessary `Hide timeline ads` patch ([5e1d001](https://github.com/ReVanced/revanced-patches/commit/5e1d001056df68e1e2b39f1365215c91bcc9e46b)) +* **SoundCloud:** Add `Enable offline sync` patch ([#3407](https://github.com/ReVanced/revanced-patches/issues/3407)) ([4de86c6](https://github.com/ReVanced/revanced-patches/commit/4de86c6407376bcd3cc0513a2f0707410b8d7ccd)) +* **SwissID:** Add `Remove Google Play Integrity Integrity check` patch ([#3478](https://github.com/ReVanced/revanced-patches/issues/3478)) ([60492ae](https://github.com/ReVanced/revanced-patches/commit/60492aea7863e07d8bf1af9380ae9295ca161f3c)) +* **YouTube - Description components:** Add `Hide 'Key concepts' section` option ([#3495](https://github.com/ReVanced/revanced-patches/issues/3495)) ([d75b645](https://github.com/ReVanced/revanced-patches/commit/d75b64595a7ac26faca4c0ae21923b22f6783975)) +* **YouTube:** Add `Bypass image region restrictions` patch ([#3442](https://github.com/ReVanced/revanced-patches/issues/3442)) ([765fab2](https://github.com/ReVanced/revanced-patches/commit/765fab2af2769349446cc0f2109343ef3bd8c621)) + +# [4.12.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.16...v4.12.0-dev.17) (2024-08-06) + + +### Bug Fixes + +* **Google Photos - GmsCore support:** Fix by checking first if a method exists before trying to patch it ([acf38ca](https://github.com/ReVanced/revanced-patches/commit/acf38cafae5eb9896b43f3a6cbd808ac273cd081)) +* **Messenger - Disable switching emoji to sticker:** Constrain to last working version `439.0.0.29.119` ([6207c31](https://github.com/ReVanced/revanced-patches/commit/6207c314c657a1188d1081b0a196a61e49cad83b)) + + +### Features + +* **CandyLink:** Remove non-functional `Unlock pro` patch ([7ae9f8f](https://github.com/ReVanced/revanced-patches/commit/7ae9f8fa0a349b91853e9554f18e564ca6ff887c)) +* **Expense Manager:** Remove non-functional `Unlock pro` patch ([ebbcac7](https://github.com/ReVanced/revanced-patches/commit/ebbcac74fd8598daebb4be0bd7c430c41333e2d4)) +* **Instagram:** Remove unnecessary `Hide timeline ads` patch ([5e1d001](https://github.com/ReVanced/revanced-patches/commit/5e1d001056df68e1e2b39f1365215c91bcc9e46b)) + +# [4.12.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.15...v4.12.0-dev.16) (2024-08-04) + + +### Bug Fixes + +* **YouTube - Spoof client:** Restore livestream audio only playback with iOS spoofing ([#3504](https://github.com/ReVanced/revanced-patches/issues/3504)) ([90d3288](https://github.com/ReVanced/revanced-patches/commit/90d32880906787d82c4b9a7a1099b46dff3a0870)) + +# [4.12.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.14...v4.12.0-dev.15) (2024-08-02) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Improve create segment manual seek accuracy ([#3491](https://github.com/ReVanced/revanced-patches/issues/3491)) ([1544981](https://github.com/ReVanced/revanced-patches/commit/15449819ff74b636fb2fa6aacd770142c51d2e5d)) + +# [4.12.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.13...v4.12.0-dev.14) (2024-08-01) + + +### Features + +* **YouTube - Description components:** Add `Hide 'Key concepts' section` option ([#3495](https://github.com/ReVanced/revanced-patches/issues/3495)) ([d75b645](https://github.com/ReVanced/revanced-patches/commit/d75b64595a7ac26faca4c0ae21923b22f6783975)) + +# [4.12.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.12...v4.12.0-dev.13) (2024-07-31) + + +### Features + +* **Boost for Reddit:** Add `Disable ads` patch ([#3474](https://github.com/ReVanced/revanced-patches/issues/3474)) ([b292c20](https://github.com/ReVanced/revanced-patches/commit/b292c200bf4ea5b4f71d96690ac011e7843552f0)) + +# [4.12.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.11...v4.12.0-dev.12) (2024-07-28) + + +### Features + +* **SwissID:** Add `Remove Google Play Integrity Integrity check` patch ([#3478](https://github.com/ReVanced/revanced-patches/issues/3478)) ([60492ae](https://github.com/ReVanced/revanced-patches/commit/60492aea7863e07d8bf1af9380ae9295ca161f3c)) + +# [4.12.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.10...v4.12.0-dev.11) (2024-07-28) + + +### Bug Fixes + +* **YouTube - Bypass image region restrictions:** Move setting to `Misc` menu ([094ae59](https://github.com/ReVanced/revanced-patches/commit/094ae59fc92663fff6c5d6f5cbece41822a326f9)) + +# [4.12.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.9...v4.12.0-dev.10) (2024-07-28) + + +### Bug Fixes + +* **YouTube - Client Spoof:** Restore missing high qualities by spoofing the iOS client user agent ([#3468](https://github.com/ReVanced/revanced-patches/issues/3468)) ([0e6ae5f](https://github.com/ReVanced/revanced-patches/commit/0e6ae5fee752a76604cf9b95f9a76c0cbe5f7dae)) + +# [4.12.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.8...v4.12.0-dev.9) (2024-07-28) + + +### Bug Fixes + +* **YouTube - Spoof client:** Fix tracking history on brand accounts ([#3480](https://github.com/ReVanced/revanced-patches/issues/3480)) ([69c1f16](https://github.com/ReVanced/revanced-patches/commit/69c1f16f7eb0d5759a44f7f7a09b1757ce8f61dd)) + +# [4.12.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.7...v4.12.0-dev.8) (2024-07-26) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Correctly show minute timestamp when creating a new segment ([d74c366](https://github.com/ReVanced/revanced-patches/commit/d74c366dbf5f25c20fbfc5a0157c3c15dda82a16)) + +# [4.12.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.6...v4.12.0-dev.7) (2024-07-24) + + +### Bug Fixes + +* **SoundCloud - Enable offline sync:** Stop crashing by reversing order of patching instructions from last to first to retain indices ([63b6ced](https://github.com/ReVanced/revanced-patches/commit/63b6cede5fa5bcf377ced422da4e861996a41f0d)) + +# [4.12.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.5...v4.12.0-dev.6) (2024-07-20) + + +### Features + +* Add `Spoof build info` patch ([e7829b4](https://github.com/ReVanced/revanced-patches/commit/e7829b41e782c9feda23b9d6acf48bae277d24d9)) + +# [4.12.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.4...v4.12.0-dev.5) (2024-07-20) + + +### Features + +* Add `Hide mock location` patch ([#3417](https://github.com/ReVanced/revanced-patches/issues/3417)) ([5f81b40](https://github.com/ReVanced/revanced-patches/commit/5f81b40e7d5567fb5689d08ccc9caeaa267c3143)) +* **Google Photos:** Add `GmsCore support` patch ([#3414](https://github.com/ReVanced/revanced-patches/issues/3414)) ([24528e0](https://github.com/ReVanced/revanced-patches/commit/24528e0a6eec17ce0a3c52f8862585933615ad28)) + +# [4.12.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.3...v4.12.0-dev.4) (2024-07-20) + + +### Features + +* **Google News:** Add `Enable CustomTabs` and `GmsCore support` patch ([#3111](https://github.com/ReVanced/revanced-patches/issues/3111)) ([ad59096](https://github.com/ReVanced/revanced-patches/commit/ad590962275f888b335252ad5bed0f34e959d3c7)) + +# [4.12.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.2...v4.12.0-dev.3) (2024-07-18) + + +### Bug Fixes + +* **Instagram - Hide ads:** Restore compatibility with latest version by fixing fingerprint ([#3455](https://github.com/ReVanced/revanced-patches/issues/3455)) ([4505fa4](https://github.com/ReVanced/revanced-patches/commit/4505fa4138bb55c8957790239c01b8dda63d6cdd)) + +# [4.12.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.12.0-dev.1...v4.12.0-dev.2) (2024-07-15) + + +### Features + +* **YouTube:** Add `Bypass image region restrictions` patch ([#3442](https://github.com/ReVanced/revanced-patches/issues/3442)) ([765fab2](https://github.com/ReVanced/revanced-patches/commit/765fab2af2769349446cc0f2109343ef3bd8c621)) + +# [4.12.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.11.1-dev.1...v4.12.0-dev.1) (2024-07-13) + + +### Features + +* **SoundCloud:** Add `Enable offline sync` patch ([#3407](https://github.com/ReVanced/revanced-patches/issues/3407)) ([4de86c6](https://github.com/ReVanced/revanced-patches/commit/4de86c6407376bcd3cc0513a2f0707410b8d7ccd)) + +## [4.11.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.11.0...v4.11.1-dev.1) (2024-07-12) + + +### Bug Fixes + +* **YouTube - Hide keyword content:** Do not hide flyout menu ([687c9f7](https://github.com/ReVanced/revanced-patches/commit/687c9f7eb03cca5f7b3486f07f2e3453ebc77faf)) + +## [4.11.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.11.0...v4.11.1-dev.1) (2024-07-12) + + +### Bug Fixes + +* **YouTube - Hide keyword content:** Do not hide flyout menu ([687c9f7](https://github.com/ReVanced/revanced-patches/commit/687c9f7eb03cca5f7b3486f07f2e3453ebc77faf)) + +# [4.11.0](https://github.com/ReVanced/revanced-patches/compare/v4.10.0...v4.11.0) (2024-07-11) + + +### Bug Fixes + +* **Boost for reddit - Fix missing audio in video downloads:** Replace correct strings ([#3379](https://github.com/ReVanced/revanced-patches/issues/3379)) ([b43db98](https://github.com/ReVanced/revanced-patches/commit/b43db98e8483e2939d8fb9cd02443f24aeaae3c3)) +* **Windy - Unlock pro:** Revert changing package name ([#3402](https://github.com/ReVanced/revanced-patches/issues/3402)) ([541f1e7](https://github.com/ReVanced/revanced-patches/commit/541f1e702665630a3737f67a4cc0c4f7b4899f8f)) +* **Windy - Unlock pro:** Use correct package name ([#3397](https://github.com/ReVanced/revanced-patches/issues/3397)) ([1d8459a](https://github.com/ReVanced/revanced-patches/commit/1d8459ac992b12371c41df3c25b6386119770d15)) +* **YouTube - Hide layout components:** Detect if a keyword filter hides all videos ([#3365](https://github.com/ReVanced/revanced-patches/issues/3365)) ([6aa47ec](https://github.com/ReVanced/revanced-patches/commit/6aa47ec1050cf32158ab608441c0649501184971)) +* **YouTube - Settings:** Move some settings to different menus, adjust default setting values ([#3415](https://github.com/ReVanced/revanced-patches/issues/3415)) ([7201ac4](https://github.com/ReVanced/revanced-patches/commit/7201ac45c158682413c8584aac7bb37b770fc924)) +* **YouTube - SponsorBlock:** Skip segments when casting ([#3331](https://github.com/ReVanced/revanced-patches/issues/3331)) ([d9395fd](https://github.com/ReVanced/revanced-patches/commit/d9395fdbca45cf68fbc63469e228eefbd6c2bf2e)) + + +### Features + +* Add `Remove share targets` patch ([#3334](https://github.com/ReVanced/revanced-patches/issues/3334)) ([9414122](https://github.com/ReVanced/revanced-patches/commit/94141228163aee8d051491db51fc1e4c8b86f0e6)) +* Add translations ([#2963](https://github.com/ReVanced/revanced-patches/issues/2963)) ([69ea6f3](https://github.com/ReVanced/revanced-patches/commit/69ea6f3bc2b5f419320f17c150489dcb9eed76ce)) +* **Bandcamp:** Add `Remove play limits` patch ([#3366](https://github.com/ReVanced/revanced-patches/issues/3366)) ([ad8d3bb](https://github.com/ReVanced/revanced-patches/commit/ad8d3bb1c86f1324234e890f1171ec4a18e56dd9)) +* **Instagram:** Add `Hide ads` patch ([#3380](https://github.com/ReVanced/revanced-patches/issues/3380)) ([c6b2f8c](https://github.com/ReVanced/revanced-patches/commit/c6b2f8c0172b4fd142927d9448ed855831c86ae4)) +* **RAR:** Add `Hide purchase reminder` patch ([#3321](https://github.com/ReVanced/revanced-patches/issues/3321)) ([8fbe7e3](https://github.com/ReVanced/revanced-patches/commit/8fbe7e3d38c43adfa0755bcbe87f8c6b7696da3a)) +* **Soundcloud:** Add `Hide ads` and `Disable telemetry` patch ([#3386](https://github.com/ReVanced/revanced-patches/issues/3386)) ([3c79f3d](https://github.com/ReVanced/revanced-patches/commit/3c79f3d34d978aead60de19e64d05fbebc1bc73a)) +* **Stocard:** Add `Hide offers tab` and `Hide story bubbles` patch ([#3359](https://github.com/ReVanced/revanced-patches/issues/3359)) ([fbd0507](https://github.com/ReVanced/revanced-patches/commit/fbd0507ce5cdeb93a1f661aa8097139c61e643a0)) + +# [4.11.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.11.0-dev.6...v4.11.0-dev.7) (2024-07-10) + + +### Features + +* **Bandcamp:** Add `Remove play limits` patch ([#3366](https://github.com/ReVanced/revanced-patches/issues/3366)) ([ad8d3bb](https://github.com/ReVanced/revanced-patches/commit/ad8d3bb1c86f1324234e890f1171ec4a18e56dd9)) + +# [4.11.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.11.0-dev.5...v4.11.0-dev.6) (2024-07-10) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Skip segments when casting ([#3331](https://github.com/ReVanced/revanced-patches/issues/3331)) ([d9395fd](https://github.com/ReVanced/revanced-patches/commit/d9395fdbca45cf68fbc63469e228eefbd6c2bf2e)) + + +### Features + +* **Soundcloud:** Add `Hide ads` and `Disable telemetry` patch ([#3386](https://github.com/ReVanced/revanced-patches/issues/3386)) ([3c79f3d](https://github.com/ReVanced/revanced-patches/commit/3c79f3d34d978aead60de19e64d05fbebc1bc73a)) + +# [4.11.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.11.0-dev.4...v4.11.0-dev.5) (2024-07-05) + + +### Bug Fixes + +* **YouTube - Settings:** Move some settings to different menus, adjust default setting values ([#3415](https://github.com/ReVanced/revanced-patches/issues/3415)) ([7201ac4](https://github.com/ReVanced/revanced-patches/commit/7201ac45c158682413c8584aac7bb37b770fc924)) + +# [4.11.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.11.0-dev.3...v4.11.0-dev.4) (2024-07-01) + + +### Bug Fixes + +* **Windy - Unlock pro:** Revert changing package name ([#3402](https://github.com/ReVanced/revanced-patches/issues/3402)) ([541f1e7](https://github.com/ReVanced/revanced-patches/commit/541f1e702665630a3737f67a4cc0c4f7b4899f8f)) + +# [4.11.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.11.0-dev.2...v4.11.0-dev.3) (2024-06-30) + + +### Bug Fixes + +* **Windy - Unlock pro:** Use correct package name ([#3397](https://github.com/ReVanced/revanced-patches/issues/3397)) ([1d8459a](https://github.com/ReVanced/revanced-patches/commit/1d8459ac992b12371c41df3c25b6386119770d15)) + +# [4.11.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.11.0-dev.1...v4.11.0-dev.2) (2024-06-27) + + +### Features + +* Add translations ([#2963](https://github.com/ReVanced/revanced-patches/issues/2963)) ([69ea6f3](https://github.com/ReVanced/revanced-patches/commit/69ea6f3bc2b5f419320f17c150489dcb9eed76ce)) + +# [4.11.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.10.1-dev.2...v4.11.0-dev.1) (2024-06-27) + + +### Features + +* Add `Remove share targets` patch ([#3334](https://github.com/ReVanced/revanced-patches/issues/3334)) ([9414122](https://github.com/ReVanced/revanced-patches/commit/94141228163aee8d051491db51fc1e4c8b86f0e6)) +* **Instagram:** Add `Hide ads` patch ([#3380](https://github.com/ReVanced/revanced-patches/issues/3380)) ([c6b2f8c](https://github.com/ReVanced/revanced-patches/commit/c6b2f8c0172b4fd142927d9448ed855831c86ae4)) +* **RAR:** Add `Hide purchase reminder` patch ([#3321](https://github.com/ReVanced/revanced-patches/issues/3321)) ([8fbe7e3](https://github.com/ReVanced/revanced-patches/commit/8fbe7e3d38c43adfa0755bcbe87f8c6b7696da3a)) +* **Stocard:** Add `Hide offers tab` and `Hide story bubbles` patch ([#3359](https://github.com/ReVanced/revanced-patches/issues/3359)) ([fbd0507](https://github.com/ReVanced/revanced-patches/commit/fbd0507ce5cdeb93a1f661aa8097139c61e643a0)) + +## [4.10.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.10.1-dev.1...v4.10.1-dev.2) (2024-06-24) + + +### Bug Fixes + +* **Boost for reddit - Fix missing audio in video downloads:** Replace correct strings ([#3379](https://github.com/ReVanced/revanced-patches/issues/3379)) ([b43db98](https://github.com/ReVanced/revanced-patches/commit/b43db98e8483e2939d8fb9cd02443f24aeaae3c3)) + +## [4.10.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.10.0...v4.10.1-dev.1) (2024-06-23) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Detect if a keyword filter hides all videos ([#3365](https://github.com/ReVanced/revanced-patches/issues/3365)) ([6aa47ec](https://github.com/ReVanced/revanced-patches/commit/6aa47ec1050cf32158ab608441c0649501184971)) + +# [4.10.0](https://github.com/ReVanced/revanced-patches/compare/v4.9.0...v4.10.0) (2024-06-23) + + +### Bug Fixes + +* Correct invalid string name ([b84494f](https://github.com/ReVanced/revanced-patches/commit/b84494f4e26e040ada69ed7a516f331f2d47da87)) +* **YouTube - Client spoof:** Correctly play more livestreams using Android VR ([#3316](https://github.com/ReVanced/revanced-patches/issues/3316)) ([c05264a](https://github.com/ReVanced/revanced-patches/commit/c05264af3944cbfe8d9aa34fb0e0fddb05a1d42f)) +* **YouTube - Hide description components:** Replace `Hide game section` and `Hide music section` with `Hide attributes section` ([#3327](https://github.com/ReVanced/revanced-patches/issues/3327)) ([0198a43](https://github.com/ReVanced/revanced-patches/commit/0198a436f97b127a2a5dd283644254f9a0ae3e43)) +* **YouTube Music:** Rename `Minimized playback` to `Remove background playback restrictions` ([#3315](https://github.com/ReVanced/revanced-patches/issues/3315)) ([3c31e55](https://github.com/ReVanced/revanced-patches/commit/3c31e55b13d9495e857f068f8cd2b4320112d763)) +* **YouTube:** Rename `Minimized playback` to `Remove background playback restrictions` ([#3314](https://github.com/ReVanced/revanced-patches/issues/3314)) ([37d415b](https://github.com/ReVanced/revanced-patches/commit/37d415b53af4771d9c97a8b1c153be32bf3ac2e0)) + + +### Features + +* Add `Change version code` patch ([#3338](https://github.com/ReVanced/revanced-patches/issues/3338)) ([685ef39](https://github.com/ReVanced/revanced-patches/commit/685ef39119daf1033a83262982519531c481c40f)) +* **Boost For Reddit:** Add `Fix /s/ links` patch ([#3154](https://github.com/ReVanced/revanced-patches/issues/3154)) ([5fa9fd2](https://github.com/ReVanced/revanced-patches/commit/5fa9fd2dfef43838d7311a967a3e805256a5d116)) +* **Boost for Reddit:** Add `Fix audio missing in video downloads` patch ([#3287](https://github.com/ReVanced/revanced-patches/issues/3287)) ([a9258d4](https://github.com/ReVanced/revanced-patches/commit/a9258d48d3ddf8552ab56219677a3b31ee553666)) +* **YouTube - Comments:** Add `Hide 'Create a Short' button` option ([#3333](https://github.com/ReVanced/revanced-patches/issues/3333)) ([be9e244](https://github.com/ReVanced/revanced-patches/commit/be9e24420fda80903e44e2e2278ea4904ecac4e1)) +* **YouTube - Comments:** Add `Hide Thanks button` and `Hide 'Comments by members' header` options ([#3317](https://github.com/ReVanced/revanced-patches/issues/3317)) ([9c4c4f0](https://github.com/ReVanced/revanced-patches/commit/9c4c4f05a762d745404101bbc3925ab4eba2deb8)) +* **YouTube - Miniplayer:** Rename `Tablet mini player` and allow selecting the style of the in-app miniplayer ([#3302](https://github.com/ReVanced/revanced-patches/issues/3302)) ([5511736](https://github.com/ReVanced/revanced-patches/commit/5511736b0c5dd409db6a68db0f85e389bb95be47)) + +# [4.10.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.9...v4.10.0-dev.10) (2024-06-18) + + +### Bug Fixes + +* Correct invalid string name ([b84494f](https://github.com/ReVanced/revanced-patches/commit/b84494f4e26e040ada69ed7a516f331f2d47da87)) + +# [4.10.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.8...v4.10.0-dev.9) (2024-06-17) + + +### Bug Fixes + +* **YouTube - Hide description components:** Replace `Hide game section` and `Hide music section` with `Hide attributes section` ([#3327](https://github.com/ReVanced/revanced-patches/issues/3327)) ([0198a43](https://github.com/ReVanced/revanced-patches/commit/0198a436f97b127a2a5dd283644254f9a0ae3e43)) + +# [4.10.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.7...v4.10.0-dev.8) (2024-06-17) + + +### Features + +* **YouTube - Comments:** Add `Hide 'Create a Short' button` option ([#3333](https://github.com/ReVanced/revanced-patches/issues/3333)) ([be9e244](https://github.com/ReVanced/revanced-patches/commit/be9e24420fda80903e44e2e2278ea4904ecac4e1)) + +# [4.10.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.6...v4.10.0-dev.7) (2024-06-15) + + +### Features + +* Add `Change version code` patch ([#3338](https://github.com/ReVanced/revanced-patches/issues/3338)) ([685ef39](https://github.com/ReVanced/revanced-patches/commit/685ef39119daf1033a83262982519531c481c40f)) + +# [4.10.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.5...v4.10.0-dev.6) (2024-06-09) + + +### Features + +* **YouTube - Comments:** Add `Hide Thanks button` and `Hide 'Comments by members' header` options ([#3317](https://github.com/ReVanced/revanced-patches/issues/3317)) ([9c4c4f0](https://github.com/ReVanced/revanced-patches/commit/9c4c4f05a762d745404101bbc3925ab4eba2deb8)) + +# [4.10.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.4...v4.10.0-dev.5) (2024-06-09) + + +### Bug Fixes + +* **YouTube - Client spoof:** Correctly play more livestreams using Android VR ([#3316](https://github.com/ReVanced/revanced-patches/issues/3316)) ([c05264a](https://github.com/ReVanced/revanced-patches/commit/c05264af3944cbfe8d9aa34fb0e0fddb05a1d42f)) + +# [4.10.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.3...v4.10.0-dev.4) (2024-06-08) + + +### Features + +* **Boost for Reddit:** Add `Fix audio missing in video downloads` patch ([#3287](https://github.com/ReVanced/revanced-patches/issues/3287)) ([a9258d4](https://github.com/ReVanced/revanced-patches/commit/a9258d48d3ddf8552ab56219677a3b31ee553666)) + +# [4.10.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.2...v4.10.0-dev.3) (2024-06-08) + + +### Features + +* **Boost For Reddit:** Add `Fix /s/ links` patch ([#3154](https://github.com/ReVanced/revanced-patches/issues/3154)) ([5fa9fd2](https://github.com/ReVanced/revanced-patches/commit/5fa9fd2dfef43838d7311a967a3e805256a5d116)) + +# [4.10.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.10.0-dev.1...v4.10.0-dev.2) (2024-06-08) + + +### Bug Fixes + +* **YouTube Music:** Rename `Minimized playback` to `Remove background playback restrictions` ([#3315](https://github.com/ReVanced/revanced-patches/issues/3315)) ([3c31e55](https://github.com/ReVanced/revanced-patches/commit/3c31e55b13d9495e857f068f8cd2b4320112d763)) +* **YouTube:** Rename `Minimized playback` to `Remove background playback restrictions` ([#3314](https://github.com/ReVanced/revanced-patches/issues/3314)) ([37d415b](https://github.com/ReVanced/revanced-patches/commit/37d415b53af4771d9c97a8b1c153be32bf3ac2e0)) + +# [4.10.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.9.0...v4.10.0-dev.1) (2024-06-07) + + +### Features + +* **YouTube - Miniplayer:** Rename `Tablet mini player` and allow selecting the style of the in-app miniplayer ([#3302](https://github.com/ReVanced/revanced-patches/issues/3302)) ([5511736](https://github.com/ReVanced/revanced-patches/commit/5511736b0c5dd409db6a68db0f85e389bb95be47)) + +# [4.9.0](https://github.com/ReVanced/revanced-patches/compare/v4.8.3...v4.9.0) (2024-06-02) + + +### Bug Fixes + +* **YouTube - Spoof client:** Allow swipe gestures to enter/exit fullscreen when spoofing with `Android VR` client ([#3259](https://github.com/ReVanced/revanced-patches/issues/3259)) ([5114900](https://github.com/ReVanced/revanced-patches/commit/5114900b1b5572c04ba6759eedab77f0a934b058)) +* **YouTube - Spoof client:** Restore playback speed menu when spoofing to an iOS client ([95f290f](https://github.com/ReVanced/revanced-patches/commit/95f290f1139cc8679beecac53c623847668f885e)) + + +### Features + +* **Messenger:** Add `Hide inbox subtabs` patch ([#3163](https://github.com/ReVanced/revanced-patches/issues/3163)) ([24e4ebd](https://github.com/ReVanced/revanced-patches/commit/24e4ebd77ad0f349b479926bf3983b72c2683496)) +* **YouTube - Hide layout components:** Disable like / subscribe button glow animation ([#3265](https://github.com/ReVanced/revanced-patches/issues/3265)) ([68d35ea](https://github.com/ReVanced/revanced-patches/commit/68d35eafc15513c23cd5220260023e7ec5b7978a)) +* **YouTube - Playback speed:** Add option to show speed dialog button in video player ([#3197](https://github.com/ReVanced/revanced-patches/issues/3197)) ([ad00305](https://github.com/ReVanced/revanced-patches/commit/ad00305ff57d5e8041de7375bea7d3ad6f18c4e2)) +* **YouTube Music:** Support version `7.03` ([#3272](https://github.com/ReVanced/revanced-patches/issues/3272)) ([d1ceca3](https://github.com/ReVanced/revanced-patches/commit/d1ceca39984f7933b28d81802d04bb3ead327595)) +* **YouTube:** Support version `19.12`, `19.13`, `19.14`, `19.15` and `19.16` ([#3239](https://github.com/ReVanced/revanced-patches/issues/3239)) ([99b07e0](https://github.com/ReVanced/revanced-patches/commit/99b07e0e18574668f36bb3c962c8d11222114be4)) + +# [4.9.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.9.0-dev.6...v4.9.0-dev.7) (2024-06-02) + + +### Features + +* **YouTube - Playback speed:** Add option to show speed dialog button in video player ([#3197](https://github.com/ReVanced/revanced-patches/issues/3197)) ([ad00305](https://github.com/ReVanced/revanced-patches/commit/ad00305ff57d5e8041de7375bea7d3ad6f18c4e2)) + +# [4.9.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.9.0-dev.5...v4.9.0-dev.6) (2024-06-02) + + +### Bug Fixes + +* **YouTube - Spoof client:** Restore playback speed menu when spoofing to an iOS client ([95f290f](https://github.com/ReVanced/revanced-patches/commit/95f290f1139cc8679beecac53c623847668f885e)) + +# [4.9.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.9.0-dev.4...v4.9.0-dev.5) (2024-06-01) + + +### Features + +* **YouTube:** Support version `19.12`, `19.13`, `19.14`, `19.15` and `19.16` ([#3239](https://github.com/ReVanced/revanced-patches/issues/3239)) ([99b07e0](https://github.com/ReVanced/revanced-patches/commit/99b07e0e18574668f36bb3c962c8d11222114be4)) + +# [4.9.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.9.0-dev.3...v4.9.0-dev.4) (2024-06-01) + + +### Features + +* **Messenger:** Add `Hide inbox subtabs` patch ([#3163](https://github.com/ReVanced/revanced-patches/issues/3163)) ([24e4ebd](https://github.com/ReVanced/revanced-patches/commit/24e4ebd77ad0f349b479926bf3983b72c2683496)) + +# [4.9.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.9.0-dev.2...v4.9.0-dev.3) (2024-06-01) + + +### Features + +* **YouTube Music:** Support version `7.03` ([#3272](https://github.com/ReVanced/revanced-patches/issues/3272)) ([d1ceca3](https://github.com/ReVanced/revanced-patches/commit/d1ceca39984f7933b28d81802d04bb3ead327595)) + +# [4.9.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.9.0-dev.1...v4.9.0-dev.2) (2024-06-01) + + +### Bug Fixes + +* **YouTube - Spoof client:** Allow swipe gestures to enter/exit fullscreen when spoofing with `Android VR` client ([#3259](https://github.com/ReVanced/revanced-patches/issues/3259)) ([5114900](https://github.com/ReVanced/revanced-patches/commit/5114900b1b5572c04ba6759eedab77f0a934b058)) + +# [4.9.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.3...v4.9.0-dev.1) (2024-05-31) + + +### Features + +* **YouTube - Hide layout components:** Disable like / subscribe button glow animation ([#3265](https://github.com/ReVanced/revanced-patches/issues/3265)) ([68d35ea](https://github.com/ReVanced/revanced-patches/commit/68d35eafc15513c23cd5220260023e7ec5b7978a)) + +## [4.8.3](https://github.com/ReVanced/revanced-patches/compare/v4.8.2...v4.8.3) (2024-05-31) + + +### Bug Fixes + +* **3rd-party Reddit apps:** Spoof user agent to work around Reddit API issues ([#3253](https://github.com/ReVanced/revanced-patches/issues/3253)) ([495e6d6](https://github.com/ReVanced/revanced-patches/commit/495e6d65e7cbae88baa71f8334b9afcf9819deaf)) +* **Reddit - Hide ads:** Constrain to last working version 2024.17.0 ([#3192](https://github.com/ReVanced/revanced-patches/issues/3192)) ([4fb3456](https://github.com/ReVanced/revanced-patches/commit/4fb3456e93ff7ec19030de8870e1cb9c1319faef)) +* **YouTube - Spoof client:** Clarify that only enter/exit fullscreen gesture does not work with Android VR spoof ([#3243](https://github.com/ReVanced/revanced-patches/issues/3243)) ([06d8f55](https://github.com/ReVanced/revanced-patches/commit/06d8f55e9b6ce27d8c550f202615689ac9c34cfa)) +* **YouTube - Spoof client:** Improve Android spoofing ([#3230](https://github.com/ReVanced/revanced-patches/issues/3230)) ([b688923](https://github.com/ReVanced/revanced-patches/commit/b688923c7e83805f2377a19b20a969b8cb749a9c)) + +## [4.8.3-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.8.3-dev.3...v4.8.3-dev.4) (2024-05-30) + + +### Bug Fixes + +* **3rd-party Reddit apps:** Spoof user agent to work around Reddit API issues ([#3253](https://github.com/ReVanced/revanced-patches/issues/3253)) ([495e6d6](https://github.com/ReVanced/revanced-patches/commit/495e6d65e7cbae88baa71f8334b9afcf9819deaf)) + +## [4.8.3-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.8.3-dev.2...v4.8.3-dev.3) (2024-05-30) + + +### Bug Fixes + +* **Reddit - Hide ads:** Constrain to last working version 2024.17.0 ([#3192](https://github.com/ReVanced/revanced-patches/issues/3192)) ([4fb3456](https://github.com/ReVanced/revanced-patches/commit/4fb3456e93ff7ec19030de8870e1cb9c1319faef)) + +## [4.8.3-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.8.3-dev.1...v4.8.3-dev.2) (2024-05-30) + + +### Bug Fixes + +* **YouTube - Spoof client:** Clarify that only enter/exit fullscreen gesture does not work with Android VR spoof ([#3243](https://github.com/ReVanced/revanced-patches/issues/3243)) ([06d8f55](https://github.com/ReVanced/revanced-patches/commit/06d8f55e9b6ce27d8c550f202615689ac9c34cfa)) + +## [4.8.3-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.2...v4.8.3-dev.1) (2024-05-25) + + +### Bug Fixes + +* **YouTube - Spoof client:** Improve Android spoofing ([#3230](https://github.com/ReVanced/revanced-patches/issues/3230)) ([b688923](https://github.com/ReVanced/revanced-patches/commit/b688923c7e83805f2377a19b20a969b8cb749a9c)) + +## [4.8.2](https://github.com/ReVanced/revanced-patches/compare/v4.8.1...v4.8.2) (2024-05-24) + + +### Bug Fixes + +* **YouTube - Client spoof:** Spoof iOS client model to fix various side effects ([#3220](https://github.com/ReVanced/revanced-patches/issues/3220)) ([9b5f4ce](https://github.com/ReVanced/revanced-patches/commit/9b5f4ce2b251c67e24cfcac3edae70c8a8aae230)) + +## [4.8.2-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.1...v4.8.2-dev.1) (2024-05-23) + + +### Bug Fixes + +* **YouTube - Client spoof:** Spoof iOS client model to fix various side effects ([#3220](https://github.com/ReVanced/revanced-patches/issues/3220)) ([9b5f4ce](https://github.com/ReVanced/revanced-patches/commit/9b5f4ce2b251c67e24cfcac3edae70c8a8aae230)) + +## [4.8.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.0...v4.8.1) (2024-05-21) + + +### Bug Fixes + +* Use UrlDecoder API available in older Android versions ([d42fbb1](https://github.com/ReVanced/revanced-patches/commit/d42fbb152126cf2177315c4706fb03bc89f5af1c)) + +## [4.8.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.8.0...v4.8.1-dev.1) (2024-05-21) + + +### Bug Fixes + +* Use UrlDecoder API available in older Android versions ([d42fbb1](https://github.com/ReVanced/revanced-patches/commit/d42fbb152126cf2177315c4706fb03bc89f5af1c)) + +# [4.8.0](https://github.com/ReVanced/revanced-patches/compare/v4.7.0...v4.8.0) (2024-05-21) + + +### Bug Fixes + +* Case patch option title correctly ([259c8b4](https://github.com/ReVanced/revanced-patches/commit/259c8b4e58df51d92d7e19417e13afa3848afc73)) +* Correctly handle patches jar path if it contains exclamation marks ([056e2d7](https://github.com/ReVanced/revanced-patches/commit/056e2d7dd5bbacb7dc6b109b3e2d44d55e7eb7d3)) +* Publicize abstract property ([b7c108e](https://github.com/ReVanced/revanced-patches/commit/b7c108ee201c84df31b079f3fecb6cc2f5eaf9f1)) +* **Reddit is Fun - Spoof client:** Fix login by updating the authorization subdomain from "old" to "ssl" ([b156cb1](https://github.com/ReVanced/revanced-patches/commit/b156cb1d8996c4314d59e3441c6b85d8f704cdff)) +* URL decode path to JAR containing spaces to get JAR manifest ([#3079](https://github.com/ReVanced/revanced-patches/issues/3079)) ([e1bbcb3](https://github.com/ReVanced/revanced-patches/commit/e1bbcb338dd7fce895b606440bd6f040d5486a64)) +* Use correct preference key ([3732b2c](https://github.com/ReVanced/revanced-patches/commit/3732b2ce6b617b4c1c6647397b614f8a040eece3)) +* **YouTube - Client spoof:** Spoof client to fix playback ([#3199](https://github.com/ReVanced/revanced-patches/issues/3199)) ([bec1eef](https://github.com/ReVanced/revanced-patches/commit/bec1eef10f2eb4e15696acb271357f1621543de1)) +* **YouTube - Hide ads:** Fix string typo ([ecc56d6](https://github.com/ReVanced/revanced-patches/commit/ecc56d643a0c4e5f25b933431f097a03d4bf2e69)) +* **YouTube - Hide Shorts components:** Rename option title to make it consistent ([4d6e34b](https://github.com/ReVanced/revanced-patches/commit/4d6e34b0540a3334bd77b2b48a1a5e10329171c8)) +* **YouTube - Hide video action buttons:** Remove obsolete `hide Shop button` ([#3057](https://github.com/ReVanced/revanced-patches/issues/3057)) ([b5e34f3](https://github.com/ReVanced/revanced-patches/commit/b5e34f3aabc1d9df8c41f92251618243caecdc9f)) +* **YouTube - Navigation buttons:** Adjust summary text of switch notification button ([#3130](https://github.com/ReVanced/revanced-patches/issues/3130)) ([cc8b4c9](https://github.com/ReVanced/revanced-patches/commit/cc8b4c913ed25d07fd4000cfd6318bb06a9d27c0)) +* **YouTube - Player flyout menu:** Remove obsolete `Hide report menu` ([d627d44](https://github.com/ReVanced/revanced-patches/commit/d627d44ad07fa32bb2f247ce24a3591ec5e1be0e)) +* **YouTube - Restore old video quality menu:** Show advanced quality menu in Shorts quality flyout ([#3155](https://github.com/ReVanced/revanced-patches/issues/3155)) ([c2b5bb7](https://github.com/ReVanced/revanced-patches/commit/c2b5bb723416e43a920817f97b9e0ee4ceab4f6b)) +* **YouTube - SponsorBlock:** Show correct segment times if video is over 24 hours in length ([#3138](https://github.com/ReVanced/revanced-patches/issues/3138)) ([6cdf697](https://github.com/ReVanced/revanced-patches/commit/6cdf697e8e47f6d53964497703dbe79fab3b1821)) +* **YouTube Music:** Make `Hide 'Get Music Premium' label` and `Remove upgrade button` compatible with latest version ([#3164](https://github.com/ReVanced/revanced-patches/issues/3164)) ([3ff20de](https://github.com/ReVanced/revanced-patches/commit/3ff20dee4aea49ca77dcd3fbe148287b55a2b5e3)) + + +### Features + +* **Photomath:** Support version `8.37.0` ([#3109](https://github.com/ReVanced/revanced-patches/issues/3109)) ([fb02b48](https://github.com/ReVanced/revanced-patches/commit/fb02b481e2be8c2bc4441dc5b3dc6a9df3a2a379)) +* **Piccoma:** Add `Disable tracking` patch ([#3143](https://github.com/ReVanced/revanced-patches/issues/3143)) ([8ab9e8f](https://github.com/ReVanced/revanced-patches/commit/8ab9e8f89d2bd014138e31dab7004f8ba77cae10)) +* **Piccoma:** Add `Spoof Android device ID` patch ([#3145](https://github.com/ReVanced/revanced-patches/issues/3145)) ([d953c6b](https://github.com/ReVanced/revanced-patches/commit/d953c6bdd4315d2ba44845fd569a3d12ac4d1af0)) +* **Tumblr:** Add `Disable Ad-Free Banner` patch ([#3091](https://github.com/ReVanced/revanced-patches/issues/3091)) ([54baf08](https://github.com/ReVanced/revanced-patches/commit/54baf08f777b7c975fa0b6508f0a4de19ac491f4)) +* **WarnWetter - Promo code unlock:** Constrain to last working version ([#3110](https://github.com/ReVanced/revanced-patches/issues/3110)) ([92fc8aa](https://github.com/ReVanced/revanced-patches/commit/92fc8aaad80f8fad35b75e6de032692986211536)) +* **YouTube - Comments:** Add option to hide timestamp and emoji buttons ([#3076](https://github.com/ReVanced/revanced-patches/issues/3076)) ([7efe5ae](https://github.com/ReVanced/revanced-patches/commit/7efe5aefb252a2ed908907ff218b879e2ad1a331)) +* **YouTube - Hide ads:** Add option to hide the 'Visit store' button on channel pages ([#3077](https://github.com/ReVanced/revanced-patches/issues/3077)) ([03d2cfa](https://github.com/ReVanced/revanced-patches/commit/03d2cfafbf977340456598a848858ac9452c853f)) +* **YouTube - Hide Shorts components:** Hide 'Buy super thanks' button ([#3176](https://github.com/ReVanced/revanced-patches/issues/3176)) ([89c1548](https://github.com/ReVanced/revanced-patches/commit/89c154861c8b3afa665542e97ff201c3e84410b2)) +* **YouTube - Hide Shorts components:** Hide like / dislike button in video ads ([#3062](https://github.com/ReVanced/revanced-patches/issues/3062)) ([1296985](https://github.com/ReVanced/revanced-patches/commit/12969853adfe530eb6006df38e1a5aa30b28fdf9)) +* **YouTube - Navigation buttons:** Add option to hide navigation button labels ([#3189](https://github.com/ReVanced/revanced-patches/issues/3189)) ([f9dc705](https://github.com/ReVanced/revanced-patches/commit/f9dc7050513b9fdb7766838a63a172f1478296f7)) +* **YT Music:** Add support for `7.01.52` ([#3177](https://github.com/ReVanced/revanced-patches/issues/3177)) ([e9bfb25](https://github.com/ReVanced/revanced-patches/commit/e9bfb25dfe85754fd7fa5c9db934bb4fc52e4694)) + +# [4.8.0-dev.24](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.23...v4.8.0-dev.24) (2024-05-21) + + +### Bug Fixes + +* **YouTube - Client spoof:** Spoof client to fix playback ([#3199](https://github.com/ReVanced/revanced-patches/issues/3199)) ([bec1eef](https://github.com/ReVanced/revanced-patches/commit/bec1eef10f2eb4e15696acb271357f1621543de1)) + +# [4.8.0-dev.23](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.22...v4.8.0-dev.23) (2024-05-18) + + +### Features + +* **Piccoma:** Add `Spoof Android device ID` patch ([#3145](https://github.com/ReVanced/revanced-patches/issues/3145)) ([d953c6b](https://github.com/ReVanced/revanced-patches/commit/d953c6bdd4315d2ba44845fd569a3d12ac4d1af0)) + +# [4.8.0-dev.22](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.21...v4.8.0-dev.22) (2024-05-18) + + +### Bug Fixes + +* Use correct preference key ([3732b2c](https://github.com/ReVanced/revanced-patches/commit/3732b2ce6b617b4c1c6647397b614f8a040eece3)) + +# [4.8.0-dev.21](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.20...v4.8.0-dev.21) (2024-05-18) + + +### Features + +* **Piccoma:** Add `Disable tracking` patch ([#3143](https://github.com/ReVanced/revanced-patches/issues/3143)) ([8ab9e8f](https://github.com/ReVanced/revanced-patches/commit/8ab9e8f89d2bd014138e31dab7004f8ba77cae10)) +* **YouTube - Navigation buttons:** Add option to hide navigation button labels ([#3189](https://github.com/ReVanced/revanced-patches/issues/3189)) ([f9dc705](https://github.com/ReVanced/revanced-patches/commit/f9dc7050513b9fdb7766838a63a172f1478296f7)) + +# [4.8.0-dev.20](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.19...v4.8.0-dev.20) (2024-05-16) + + +### Features + +* **YT Music:** Add support for `7.01.52` ([#3177](https://github.com/ReVanced/revanced-patches/issues/3177)) ([e9bfb25](https://github.com/ReVanced/revanced-patches/commit/e9bfb25dfe85754fd7fa5c9db934bb4fc52e4694)) + +# [4.8.0-dev.19](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.18...v4.8.0-dev.19) (2024-05-16) + + +### Features + +* **YouTube - Hide Shorts components:** Hide 'Buy super thanks' button ([#3176](https://github.com/ReVanced/revanced-patches/issues/3176)) ([89c1548](https://github.com/ReVanced/revanced-patches/commit/89c154861c8b3afa665542e97ff201c3e84410b2)) + +# [4.8.0-dev.18](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.17...v4.8.0-dev.18) (2024-05-14) + + +### Bug Fixes + +* **YouTube Music:** Make `Hide 'Get Music Premium' label` and `Remove upgrade button` compatible with latest version ([#3164](https://github.com/ReVanced/revanced-patches/issues/3164)) ([3ff20de](https://github.com/ReVanced/revanced-patches/commit/3ff20dee4aea49ca77dcd3fbe148287b55a2b5e3)) + +# [4.8.0-dev.17](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.16...v4.8.0-dev.17) (2024-05-12) + + +### Features + +* **Photomath:** Support version `8.37.0` ([#3109](https://github.com/ReVanced/revanced-patches/issues/3109)) ([fb02b48](https://github.com/ReVanced/revanced-patches/commit/fb02b481e2be8c2bc4441dc5b3dc6a9df3a2a379)) + +# [4.8.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.15...v4.8.0-dev.16) (2024-05-12) + + +### Features + +* **WarnWetter - Promo code unlock:** Constrain to last working version ([#3110](https://github.com/ReVanced/revanced-patches/issues/3110)) ([92fc8aa](https://github.com/ReVanced/revanced-patches/commit/92fc8aaad80f8fad35b75e6de032692986211536)) + +# [4.8.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.14...v4.8.0-dev.15) (2024-05-11) + + +### Bug Fixes + +* **YouTube - Restore old video quality menu:** Show advanced quality menu in Shorts quality flyout ([#3155](https://github.com/ReVanced/revanced-patches/issues/3155)) ([c2b5bb7](https://github.com/ReVanced/revanced-patches/commit/c2b5bb723416e43a920817f97b9e0ee4ceab4f6b)) + +# [4.8.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.13...v4.8.0-dev.14) (2024-05-08) + + +### Bug Fixes + +* **YouTube - SponsorBlock:** Show correct segment times if video is over 24 hours in length ([#3138](https://github.com/ReVanced/revanced-patches/issues/3138)) ([6cdf697](https://github.com/ReVanced/revanced-patches/commit/6cdf697e8e47f6d53964497703dbe79fab3b1821)) + +# [4.8.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.12...v4.8.0-dev.13) (2024-05-06) + + +### Bug Fixes + +* **YouTube - Navigation buttons:** Adjust summary text of switch notification button ([#3130](https://github.com/ReVanced/revanced-patches/issues/3130)) ([cc8b4c9](https://github.com/ReVanced/revanced-patches/commit/cc8b4c913ed25d07fd4000cfd6318bb06a9d27c0)) + +# [4.8.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.11...v4.8.0-dev.12) (2024-05-06) + + +### Bug Fixes + +* **YouTube - Player flyout menu:** Remove obsolete `Hide report menu` ([d627d44](https://github.com/ReVanced/revanced-patches/commit/d627d44ad07fa32bb2f247ce24a3591ec5e1be0e)) + +# [4.8.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.10...v4.8.0-dev.11) (2024-05-06) + + +### Bug Fixes + +* **Reddit is Fun - Spoof client:** Fix login by updating the authorization subdomain from "old" to "ssl" ([b156cb1](https://github.com/ReVanced/revanced-patches/commit/b156cb1d8996c4314d59e3441c6b85d8f704cdff)) + +# [4.8.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.9...v4.8.0-dev.10) (2024-05-02) + + +### Features + +* **Tumblr:** Add `Disable Ad-Free Banner` patch ([#3091](https://github.com/ReVanced/revanced-patches/issues/3091)) ([54baf08](https://github.com/ReVanced/revanced-patches/commit/54baf08f777b7c975fa0b6508f0a4de19ac491f4)) + +# [4.8.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.8...v4.8.0-dev.9) (2024-04-29) + + +### Bug Fixes + +* Correctly handle patches jar path if it contains exclamation marks ([056e2d7](https://github.com/ReVanced/revanced-patches/commit/056e2d7dd5bbacb7dc6b109b3e2d44d55e7eb7d3)) + +# [4.8.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.7...v4.8.0-dev.8) (2024-04-23) + + +### Bug Fixes + +* **YouTube - Hide ads:** Fix string typo ([ecc56d6](https://github.com/ReVanced/revanced-patches/commit/ecc56d643a0c4e5f25b933431f097a03d4bf2e69)) + +# [4.8.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.6...v4.8.0-dev.7) (2024-04-23) + + +### Bug Fixes + +* URL decode path to JAR containing spaces to get JAR manifest ([#3079](https://github.com/ReVanced/revanced-patches/issues/3079)) ([e1bbcb3](https://github.com/ReVanced/revanced-patches/commit/e1bbcb338dd7fce895b606440bd6f040d5486a64)) + +# [4.8.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.5...v4.8.0-dev.6) (2024-04-23) + + +### Features + +* **YouTube - Hide ads:** Add option to hide the 'Visit store' button on channel pages ([#3077](https://github.com/ReVanced/revanced-patches/issues/3077)) ([03d2cfa](https://github.com/ReVanced/revanced-patches/commit/03d2cfafbf977340456598a848858ac9452c853f)) + +# [4.8.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.4...v4.8.0-dev.5) (2024-04-23) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Rename option title to make it consistent ([4d6e34b](https://github.com/ReVanced/revanced-patches/commit/4d6e34b0540a3334bd77b2b48a1a5e10329171c8)) + +# [4.8.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.3...v4.8.0-dev.4) (2024-04-23) + + +### Features + +* **YouTube - Comments:** Add option to hide timestamp and emoji buttons ([#3076](https://github.com/ReVanced/revanced-patches/issues/3076)) ([7efe5ae](https://github.com/ReVanced/revanced-patches/commit/7efe5aefb252a2ed908907ff218b879e2ad1a331)) + +# [4.8.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.2...v4.8.0-dev.3) (2024-04-22) + + +### Bug Fixes + +* Publicize abstract property ([b7c108e](https://github.com/ReVanced/revanced-patches/commit/b7c108ee201c84df31b079f3fecb6cc2f5eaf9f1)) + +# [4.8.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.8.0-dev.1...v4.8.0-dev.2) (2024-04-21) + + +### Bug Fixes + +* Case patch option title correctly ([259c8b4](https://github.com/ReVanced/revanced-patches/commit/259c8b4e58df51d92d7e19417e13afa3848afc73)) + +# [4.8.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.7.0...v4.8.0-dev.1) (2024-04-21) + + +### Bug Fixes + +* **YouTube - Hide video action buttons:** Remove obsolete `hide Shop button` ([#3057](https://github.com/ReVanced/revanced-patches/issues/3057)) ([b5e34f3](https://github.com/ReVanced/revanced-patches/commit/b5e34f3aabc1d9df8c41f92251618243caecdc9f)) + + +### Features + +* **YouTube - Hide Shorts components:** Hide like / dislike button in video ads ([#3062](https://github.com/ReVanced/revanced-patches/issues/3062)) ([1296985](https://github.com/ReVanced/revanced-patches/commit/12969853adfe530eb6006df38e1a5aa30b28fdf9)) + +# [4.7.0](https://github.com/ReVanced/revanced-patches/compare/v4.6.0...v4.7.0) (2024-04-21) + + +### Bug Fixes + +* **Tumblr - Fix old versions:** Improve reliability by removing remnances of Tumblr Live ([#2988](https://github.com/ReVanced/revanced-patches/issues/2988)) ([897b4db](https://github.com/ReVanced/revanced-patches/commit/897b4dbce984270ae1fd7de5bd30bd05153e45f2)) +* **YouTube - GmsCore support:** Prompt to disable battery optimizations, if not done already ([#2958](https://github.com/ReVanced/revanced-patches/issues/2958)) ([82acb84](https://github.com/ReVanced/revanced-patches/commit/82acb84b5f6ff0722a2eb080b53da9dd3622502f)) +* **YouTube - Hide ads:** rename `Hide paid content` to `Hide paid promotion label` ([#3026](https://github.com/ReVanced/revanced-patches/issues/3026)) ([17e4ac9](https://github.com/ReVanced/revanced-patches/commit/17e4ac978a2f109fd62469a3163b636cd63c55ae)) +* **YouTube - Hide load more button:** Include patch with `Hide layout components`, and hide button only in search feed ([#2959](https://github.com/ReVanced/revanced-patches/issues/2959)) ([b007e8e](https://github.com/ReVanced/revanced-patches/commit/b007e8e06a3afad79b40bec1c6a14604f059049c)) +* **YouTube - Hide Shorts components:** Correctly hide Shorts if navigation tab is changed using device back button ([#3007](https://github.com/ReVanced/revanced-patches/issues/3007)) ([e5848e9](https://github.com/ReVanced/revanced-patches/commit/e5848e99c4cc838595164ef673a77fe60d28086b)) +* **YouTube - Player flyout menu:** Add hide Lock screen menu ([#2985](https://github.com/ReVanced/revanced-patches/issues/2985)) ([308de4a](https://github.com/ReVanced/revanced-patches/commit/308de4a63ca99b8d30d6b3242f98d6f0e2aefb37)) +* **YouTube - Spoof device dimensions:** Warn about potential performance issues ([#3039](https://github.com/ReVanced/revanced-patches/issues/3039)) ([9d6f305](https://github.com/ReVanced/revanced-patches/commit/9d6f305b7c923e62b89581d221fedbe1e3f81835)) +* **YouTube Music - Remove upgrade button:** Fix compatibility with latest versions ([#3045](https://github.com/ReVanced/revanced-patches/issues/3045)) ([80de996](https://github.com/ReVanced/revanced-patches/commit/80de99666555694670529bbfe2e0be7a14d66555)) + + +### Features + +* Add `Hex` patch ([#3034](https://github.com/ReVanced/revanced-patches/issues/3034)) ([3c95aac](https://github.com/ReVanced/revanced-patches/commit/3c95aac838693b354d3a7b0e3dc57c6da5adfa9e)) +* **Amazon:** Add `Always allow deep-linking` patch ([#3000](https://github.com/ReVanced/revanced-patches/issues/3000)) ([a92b7fb](https://github.com/ReVanced/revanced-patches/commit/a92b7fb43c8b1b45577360cdc6d883fe2815c2f2)) +* **Strava - Unlock subscription:** Remove compatible version constraint ([80a5599](https://github.com/ReVanced/revanced-patches/commit/80a55991683d7b22626224fa2935a5bf9bfcbfee)) +* **Twitter:** Add `Sanitize sharing links` patch ([#3003](https://github.com/ReVanced/revanced-patches/issues/3003)) ([186b887](https://github.com/ReVanced/revanced-patches/commit/186b8874157eef1b882b05d491ba1d4ca2809535)) +* **YouTube - Hide layout components:** Add option to hide horizontal shelves ([#2951](https://github.com/ReVanced/revanced-patches/issues/2951)) ([9ae0650](https://github.com/ReVanced/revanced-patches/commit/9ae0650c0005d882299996aa442410bab4261395)) +* **YouTube - Hide layout components:** Hide playables ([8423515](https://github.com/ReVanced/revanced-patches/commit/842351548baa33737db09be1cbca9f87c1951341)) +* **YouTube - Hide Shorts components:** Hide `Shop`, `Location` and `Save sound to playlist` buttons ([#3018](https://github.com/ReVanced/revanced-patches/issues/3018)) ([5210ac4](https://github.com/ReVanced/revanced-patches/commit/5210ac431c191987264865bf8e789ea9f3fdd360)) +* **YouTube - Hide Shorts components:** Hide tagged products, hide search suggestions ([#3019](https://github.com/ReVanced/revanced-patches/issues/3019)) ([e0d2fe5](https://github.com/ReVanced/revanced-patches/commit/e0d2fe5bd2e681b9a5252a8e4ad582cc019b1606)) +* **YouTube - Swipe controls:** Save and restore brightness and add auto-brightness toggle ([#2996](https://github.com/ReVanced/revanced-patches/issues/2996)) ([f6c3bc4](https://github.com/ReVanced/revanced-patches/commit/f6c3bc43190d33e06f49b74fc056d26da1bb014a)) +* **YouTube:** Add 'About' preference to settings menu ([#2981](https://github.com/ReVanced/revanced-patches/issues/2981)) ([5abf894](https://github.com/ReVanced/revanced-patches/commit/5abf89444a3e6a211ec03c242eb9a7847542b08c)) +* **YouTube:** Match overlay icons style to YouTube ([#3023](https://github.com/ReVanced/revanced-patches/issues/3023)) ([6849393](https://github.com/ReVanced/revanced-patches/commit/684939314be3d0d43482f229b2adb033e7aa492a)) +* **YouTube:** Support version `19.09.38`, `19.10.39` and `19.11.43` ([#2971](https://github.com/ReVanced/revanced-patches/issues/2971)) ([730f3e3](https://github.com/ReVanced/revanced-patches/commit/730f3e3a7e058b60f9a8130980ecb0a747fa0a8a)) +* **YT Music - Hide 'Get Music Premium' label:** Remove occurences of label in settings ([#3046](https://github.com/ReVanced/revanced-patches/issues/3046)) ([10e170a](https://github.com/ReVanced/revanced-patches/commit/10e170a7302fdb585efee663ca13c814aea46c54)) + +# [4.7.0-dev.16](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.15...v4.7.0-dev.16) (2024-04-21) + + +### Features + +* **YouTube - Swipe controls:** Save and restore brightness and add auto-brightness toggle ([#2996](https://github.com/ReVanced/revanced-patches/issues/2996)) ([f6c3bc4](https://github.com/ReVanced/revanced-patches/commit/f6c3bc43190d33e06f49b74fc056d26da1bb014a)) + +# [4.7.0-dev.15](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.14...v4.7.0-dev.15) (2024-04-20) + + +### Features + +* **YouTube:** Support version `19.09.38`, `19.10.39` and `19.11.43` ([#2971](https://github.com/ReVanced/revanced-patches/issues/2971)) ([730f3e3](https://github.com/ReVanced/revanced-patches/commit/730f3e3a7e058b60f9a8130980ecb0a747fa0a8a)) + +# [4.7.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.13...v4.7.0-dev.14) (2024-04-20) + + +### Features + +* **YT Music - Hide 'Get Music Premium' label:** Remove occurences of label in settings ([#3046](https://github.com/ReVanced/revanced-patches/issues/3046)) ([10e170a](https://github.com/ReVanced/revanced-patches/commit/10e170a7302fdb585efee663ca13c814aea46c54)) + +# [4.7.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.12...v4.7.0-dev.13) (2024-04-18) + + +### Bug Fixes + +* **YouTube Music - Remove upgrade button:** Fix compatibility with latest versions ([#3045](https://github.com/ReVanced/revanced-patches/issues/3045)) ([80de996](https://github.com/ReVanced/revanced-patches/commit/80de99666555694670529bbfe2e0be7a14d66555)) + +# [4.7.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.11...v4.7.0-dev.12) (2024-04-18) + + +### Features + +* Add `Hex` patch ([#3034](https://github.com/ReVanced/revanced-patches/issues/3034)) ([3c95aac](https://github.com/ReVanced/revanced-patches/commit/3c95aac838693b354d3a7b0e3dc57c6da5adfa9e)) + +# [4.7.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.10...v4.7.0-dev.11) (2024-04-18) + + +### Bug Fixes + +* **YouTube - Spoof device dimensions:** Warn about potential performance issues ([#3039](https://github.com/ReVanced/revanced-patches/issues/3039)) ([9d6f305](https://github.com/ReVanced/revanced-patches/commit/9d6f305b7c923e62b89581d221fedbe1e3f81835)) + +# [4.7.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.9...v4.7.0-dev.10) (2024-04-17) + + +### Features + +* **YouTube:** Add 'About' preference to settings menu ([#2981](https://github.com/ReVanced/revanced-patches/issues/2981)) ([5abf894](https://github.com/ReVanced/revanced-patches/commit/5abf89444a3e6a211ec03c242eb9a7847542b08c)) +* **YouTube:** Match overlay icons style to YouTube ([#3023](https://github.com/ReVanced/revanced-patches/issues/3023)) ([6849393](https://github.com/ReVanced/revanced-patches/commit/684939314be3d0d43482f229b2adb033e7aa492a)) + +# [4.7.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.8...v4.7.0-dev.9) (2024-04-15) + + +### Bug Fixes + +* **YouTube - Hide ads:** rename `Hide paid content` to `Hide paid promotion label` ([#3026](https://github.com/ReVanced/revanced-patches/issues/3026)) ([17e4ac9](https://github.com/ReVanced/revanced-patches/commit/17e4ac978a2f109fd62469a3163b636cd63c55ae)) + +# [4.7.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.7...v4.7.0-dev.8) (2024-04-14) + + +### Features + +* **YouTube - Hide Shorts components:** Hide tagged products, hide search suggestions ([#3019](https://github.com/ReVanced/revanced-patches/issues/3019)) ([e0d2fe5](https://github.com/ReVanced/revanced-patches/commit/e0d2fe5bd2e681b9a5252a8e4ad582cc019b1606)) + +# [4.7.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.6...v4.7.0-dev.7) (2024-04-14) + + +### Features + +* **YouTube - Hide layout components:** Hide playables ([8423515](https://github.com/ReVanced/revanced-patches/commit/842351548baa33737db09be1cbca9f87c1951341)) + +# [4.7.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.5...v4.7.0-dev.6) (2024-04-12) + + +### Features + +* **YouTube - Hide Shorts components:** Hide `Shop`, `Location` and `Save sound to playlist` buttons ([#3018](https://github.com/ReVanced/revanced-patches/issues/3018)) ([5210ac4](https://github.com/ReVanced/revanced-patches/commit/5210ac431c191987264865bf8e789ea9f3fdd360)) + +# [4.7.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.4...v4.7.0-dev.5) (2024-04-10) + + +### Bug Fixes + +* **YouTube - Hide Shorts components:** Correctly hide Shorts if navigation tab is changed using device back button ([#3007](https://github.com/ReVanced/revanced-patches/issues/3007)) ([e5848e9](https://github.com/ReVanced/revanced-patches/commit/e5848e99c4cc838595164ef673a77fe60d28086b)) + +# [4.7.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.3...v4.7.0-dev.4) (2024-04-09) + + +### Features + +* **Amazon:** Add `Always allow deep-linking` patch ([#3000](https://github.com/ReVanced/revanced-patches/issues/3000)) ([a92b7fb](https://github.com/ReVanced/revanced-patches/commit/a92b7fb43c8b1b45577360cdc6d883fe2815c2f2)) +* **Twitter:** Add `Sanitize sharing links` patch ([#3003](https://github.com/ReVanced/revanced-patches/issues/3003)) ([186b887](https://github.com/ReVanced/revanced-patches/commit/186b8874157eef1b882b05d491ba1d4ca2809535)) + +# [4.7.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.2...v4.7.0-dev.3) (2024-04-09) + + +### Bug Fixes + +* **Tumblr - Fix old versions:** Improve reliability by removing remnances of Tumblr Live ([#2988](https://github.com/ReVanced/revanced-patches/issues/2988)) ([897b4db](https://github.com/ReVanced/revanced-patches/commit/897b4dbce984270ae1fd7de5bd30bd05153e45f2)) + +# [4.7.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.7.0-dev.1...v4.7.0-dev.2) (2024-04-06) + + +### Features + +* **Strava - Unlock subscription:** Remove compatible version constraint ([80a5599](https://github.com/ReVanced/revanced-patches/commit/80a55991683d7b22626224fa2935a5bf9bfcbfee)) + +# [4.7.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.6.1-dev.3...v4.7.0-dev.1) (2024-04-06) + + +### Features + +* **YouTube - Hide layout components:** Add option to hide horizontal shelves ([#2951](https://github.com/ReVanced/revanced-patches/issues/2951)) ([9ae0650](https://github.com/ReVanced/revanced-patches/commit/9ae0650c0005d882299996aa442410bab4261395)) + +## [4.6.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.6.1-dev.2...v4.6.1-dev.3) (2024-04-06) + + +### Bug Fixes + +* **YouTube - GmsCore support:** Prompt to disable battery optimizations, if not done already ([#2958](https://github.com/ReVanced/revanced-patches/issues/2958)) ([82acb84](https://github.com/ReVanced/revanced-patches/commit/82acb84b5f6ff0722a2eb080b53da9dd3622502f)) + +## [4.6.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.6.1-dev.1...v4.6.1-dev.2) (2024-04-04) + + +### Bug Fixes + +* **YouTube - Hide load more button:** Include patch with `Hide layout components`, and hide button only in search feed ([#2959](https://github.com/ReVanced/revanced-patches/issues/2959)) ([b007e8e](https://github.com/ReVanced/revanced-patches/commit/b007e8e06a3afad79b40bec1c6a14604f059049c)) + +## [4.6.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.6.0...v4.6.1-dev.1) (2024-04-04) + + +### Bug Fixes + +* **YouTube - Player flyout menu:** Add hide Lock screen menu ([#2985](https://github.com/ReVanced/revanced-patches/issues/2985)) ([308de4a](https://github.com/ReVanced/revanced-patches/commit/308de4a63ca99b8d30d6b3242f98d6f0e2aefb37)) + +# [4.6.0](https://github.com/ReVanced/revanced-patches/compare/v4.5.0...v4.6.0) (2024-04-02) + + +### Bug Fixes + +* **Mi Fitness - Fix login:** Patch correct register ([#2942](https://github.com/ReVanced/revanced-patches/issues/2942)) ([dc96942](https://github.com/ReVanced/revanced-patches/commit/dc969422b5d50f21e6ea7a64b67dfc650fee6e36)) +* **Tumblr:** Restore compatibility with latest versions ([#2955](https://github.com/ReVanced/revanced-patches/issues/2955)) ([2954ba7](https://github.com/ReVanced/revanced-patches/commit/2954ba78d21d77308404961f79234bbec606d42e)) + + +### Features + +* **Tumblr:** Add `Fix old versions` patch ([#2954](https://github.com/ReVanced/revanced-patches/issues/2954)) ([2fde60e](https://github.com/ReVanced/revanced-patches/commit/2fde60eceb0a96fa857c32cd55c1fd7fe776a679)) + # [4.6.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.5.1-dev.2...v4.6.0-dev.1) (2024-03-31) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70d6ebad5b..af03405ac1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ This document describes how to contribute to ReVanced Patches. ## 📖 Resources to help you get started -* The [documentation](https://github.com/ReVanced/revanced-patcher/tree/docs/docs) contains the fundamentals +* The [documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs) contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches * [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on * [Issues](https://github.com/ReVanced/revanced-patches/issues) are where we keep track of bugs and feature requests @@ -83,6 +83,10 @@ Features can be requested by opening an issue using the If you encounter a bug while using ReVanced Patches, open an issue using the [Bug report issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+). +## 🌐 Submitting translations + + You can contribute translations at [translate.revanced.app](https://translate.revanced.app). + ## 🧑‍⚖️ Guidelines for requesting or contributing patches To maintain a high-quality and ethical collection of patches, the following guidelines for requesting @@ -107,7 +111,6 @@ are unaffected by this change. * Payment circumvention: We do not accept patches that exist solely to bypass payment for the app or any of its features * Malicious patches: Patches that are malicious in nature are not allowed - ## 📝 How to contribute 1. Before contributing, it is recommended to open an issue to discuss your change @@ -115,11 +118,11 @@ with the maintainers of ReVanced Patches. This will help you determine whether y and whether it is worth your time to implement it 2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev` 3. Commit your changes. In case you are contributing a new patch, make sure to follow the conventions for patches -described in the [documentation](https://github.com/ReVanced/revanced-patches/tree/docs/docs) +described in the [ReVanced Patcher documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs) 4. Submit a pull request to the `dev` branch of the repository and reference issues that your pull request closes in the description of your pull request 5. Our team will review your pull request and provide feedback. Once your pull request is approved, it will be merged into the `dev` branch and will be included in the next release of ReVanced Patches - + ❤️ Thank you for considering contributing to ReVanced Patches, ReVanced 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/api/revanced-patches.api b/api/revanced-patches.api deleted file mode 100644 index 4bebe5e6eb..0000000000 --- a/api/revanced-patches.api +++ /dev/null @@ -1,1887 +0,0 @@ -public final class app/revanced/generator/MainKt { - public static synthetic fun main ([Ljava/lang/String;)V -} - -public final class app/revanced/patches/all/activity/exportall/ExportAllActivitiesPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/activity/exportall/ExportAllActivitiesPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/connectivity/wifi/spoof/SpoofWifiPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/connectivity/wifi/spoof/SpoofWifiPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/all/interaction/gestures/PredictiveBackGesturePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/interaction/gestures/PredictiveBackGesturePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/misc/network/OverrideCertificatePinningPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/all/misc/packagename/ChangePackageNamePatch; - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun setOrGetFallbackPackageName (Ljava/lang/String;)Ljava/lang/String; -} - -public final class app/revanced/patches/all/misc/resources/AddResourcesPatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable, java/util/Map, kotlin/jvm/internal/markers/KMutableMap { - public static final field INSTANCE Lapp/revanced/patches/all/misc/resources/AddResourcesPatch; - public fun clear ()V - public fun close ()V - public final fun containsKey (Ljava/lang/Object;)Z - public fun containsKey (Ljava/lang/String;)Z - public final fun containsValue (Ljava/lang/Object;)Z - public fun containsValue (Ljava/util/Set;)Z - public final fun entrySet ()Ljava/util/Set; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object; - public final fun get (Ljava/lang/Object;)Ljava/util/Set; - public fun get (Ljava/lang/String;)Ljava/util/Set; - public fun getEntries ()Ljava/util/Set; - public fun getKeys ()Ljava/util/Set; - public fun getSize ()I - public fun getValues ()Ljava/util/Collection; - public final fun invoke (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z - public final fun invoke (Ljava/lang/String;Ljava/lang/Iterable;)Z - public final fun invoke (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Z - public final fun invoke (Ljava/lang/String;Ljava/util/List;)Z - public final fun invoke (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)Z - public static synthetic fun invoke$default (Lapp/revanced/patches/all/misc/resources/AddResourcesPatch;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Z - public static synthetic fun invoke$default (Lapp/revanced/patches/all/misc/resources/AddResourcesPatch;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Z - public fun isEmpty ()Z - public final fun keySet ()Ljava/util/Set; - public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; - public fun put (Ljava/lang/String;Ljava/util/Set;)Ljava/util/Set; - public fun putAll (Ljava/util/Map;)V - public final synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object; - public final fun remove (Ljava/lang/Object;)Ljava/util/Set; - public fun remove (Ljava/lang/String;)Ljava/util/Set; - public final fun size ()I - public final fun values ()Ljava/util/Collection; -} - -public abstract class app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public abstract fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public final fun findPatchIndices (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;)Lkotlin/sequences/Sequence; - public abstract fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V -} - -public abstract interface class app/revanced/patches/all/misc/transformation/IMethodCall { - public abstract fun getDefinedClassName ()Ljava/lang/String; - public abstract fun getMethodName ()Ljava/lang/String; - public abstract fun getMethodParams ()[Ljava/lang/String; - public abstract fun getReturnType ()Ljava/lang/String; - public abstract fun replaceInvokeVirtualWithIntegrations (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V -} - -public final class app/revanced/patches/all/misc/transformation/IMethodCall$DefaultImpls { - public static fun replaceInvokeVirtualWithIntegrations (Lapp/revanced/patches/all/misc/transformation/IMethodCall;Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V -} - -public final class app/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall : java/lang/Enum, app/revanced/patches/all/misc/transformation/IMethodCall { - public static final field SetAllowedCapturePolicyGlobal Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; - public static final field SetAllowedCapturePolicySingle Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; - public fun getDefinedClassName ()Ljava/lang/String; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public fun getMethodName ()Ljava/lang/String; - public fun getMethodParams ()[Ljava/lang/String; - public fun getReturnType ()Ljava/lang/String; - public fun replaceInvokeVirtualWithIntegrations (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; - public static fun values ()[Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; -} - -public final class app/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall : java/lang/Enum, app/revanced/patches/all/misc/transformation/IMethodCall { - public static final field AddFlags Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; - public static final field SetFlags Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; - public fun getDefinedClassName ()Ljava/lang/String; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public fun getMethodName ()Ljava/lang/String; - public fun getMethodParams ()[Ljava/lang/String; - public fun getReturnType ()Ljava/lang/String; - public fun replaceInvokeVirtualWithIntegrations (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; - public static fun values ()[Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; -} - -public final class app/revanced/patches/all/telephony/sim/spoof/SpoofSimCountryPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/telephony/sim/spoof/SpoofSimCountryPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Pair; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Pair;)V -} - -public final class app/revanced/patches/backdrops/misc/pro/ProUnlockPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/backdrops/misc/pro/ProUnlockPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/candylinkvpn/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/candylinkvpn/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecksPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/cieid/restrictions/root/BypassRootChecksPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/facebook/ads/story/HideStoryAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/facebook/ads/story/HideStoryAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/finanzonline/detection/root/RootDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/finanzonline/detection/root/RootDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictions : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictions; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/hexeditor/ad/DisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/hexeditor/ad/DisableAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/iconpackstudio/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/iconpackstudio/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/idaustria/detection/root/RootDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/idaustria/detection/root/RootDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/inshorts/ad/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/inshorts/ad/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/instagram/patches/ads/timeline/HideTimelineAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/instagram/patches/ads/timeline/HideTimelineAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/irplus/ad/RemoveAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/irplus/ad/RemoveAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/memegenerator/detection/license/LicenseValidationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/memegenerator/detection/license/LicenseValidationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/messenger/ads/inbox/patch/HideInboxAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/messenger/ads/inbox/patch/HideInboxAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/messenger/inputfield/patch/DisableSwitchingEmojiToStickerPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/messenger/inputfield/patch/DisableSwitchingEmojiToStickerPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/messenger/inputfield/patch/DisableTypingIndicatorPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/messenger/inputfield/patch/DisableTypingIndicatorPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/mifitness/misc/login/FixLoginPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/mifitness/misc/login/FixLoginPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/moneymanager/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/moneymanager/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/ad/video/HideMusicVideoAds : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/ad/video/HideMusicVideoAds; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/ad/video/MusicVideoAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/ad/video/MusicVideoAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/audio/codecs/CodecsUnlockPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/audio/codecs/CodecsUnlockPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/audio/exclusiveaudio/ExclusiveAudioPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/audio/exclusiveaudio/ExclusiveAudioPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/interaction/permanentshuffle/PermanentShuffleTogglePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/interaction/permanentshuffle/PermanentShuffleTogglePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/compactheader/CompactHeaderPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/compactheader/CompactHeaderPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/compactheader/HideCategoryBar : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/compactheader/HideCategoryBar; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/minimizedplayback/MinimizedPlaybackPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/minimizedplayback/MinimizedPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/premium/HideGetPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/premium/HideGetPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/gms/Constants { - public static final field INSTANCE Lapp/revanced/patches/music/misc/gms/Constants; -} - -public final class app/revanced/patches/music/misc/gms/GmsCoreSupportPatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/music/misc/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/gms/GmsCoreSupportResourcePatch; -} - -public final class app/revanced/patches/music/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/music/premium/backgroundplay/BackgroundPlayPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/premium/backgroundplay/BackgroundPlayPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/myexpenses/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/myexpenses/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/myfitnesspal/ads/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/myfitnesspal/ads/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/myfitnesspal/ads/fingerprints/IsPremiumUseCaseImplFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/myfitnesspal/ads/fingerprints/IsPremiumUseCaseImplFingerprint; -} - -public final class app/revanced/patches/myfitnesspal/ads/fingerprints/MainActivityNavigateToNativePremiumUpsellFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/myfitnesspal/ads/fingerprints/MainActivityNavigateToNativePremiumUpsellFingerprint; -} - -public final class app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/nfctoolsse/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/nyx/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/nyx/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/openinghours/misc/fix/crash/FixCrashPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/detection/signature/SignatureDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/detection/signature/SignatureDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/pixiv/ads/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/pixiv/ads/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/ad/banner/HideBannerPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/banner/HideBannerPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/reddit/ad/comments/HideCommentAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/comments/HideCommentAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/ad/general/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/general/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/reddit/customclients/BaseSpoofClientPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getClientId ()Ljava/lang/String; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchMiscellaneous (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchUserAgent (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public final fun setClientId (Ljava/lang/String;)V -} - -public final class app/revanced/patches/reddit/customclients/Constants { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/Constants; - public static final field OAUTH_USER_AGENT Ljava/lang/String; -} - -public abstract class app/revanced/patches/reddit/customclients/ads/BaseDisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/redditisfun/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/redditisfun/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchUserAgent (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchMiscellaneous (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/slide/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/slide/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforlemmy/ads/DisableAdsPatch : app/revanced/patches/reddit/customclients/ads/BaseDisableAdsPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforlemmy/ads/DisableAdsPatch; -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/ads/DisableAdsPatch : app/revanced/patches/reddit/customclients/ads/BaseDisableAdsPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/ads/DisableAdsPatch; -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchMiscellaneous (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/detection/piracy/DisablePiracyDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/detection/piracy/DisablePiracyDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/fix/slink/FixSLinksPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/fix/slink/FixSLinksPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/scbeasy/detection/debugging/RemoveDebuggingDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/scbeasy/detection/debugging/RemoveDebuggingDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/serviceportalbund/detection/root/RootDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/serviceportalbund/detection/root/RootDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/util/Set;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lkotlin/reflect/KClass;Lapp/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/util/Set;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lkotlin/reflect/KClass;Lapp/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - protected final fun getGmsCoreVendor ()Ljava/lang/String; - protected final fun getGmsCoreVendorGroupId ()Ljava/lang/String; -} - -public abstract class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/util/Set;)V - public fun (Ljava/util/Set;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun invoke (Ljava/lang/String;)V -} - -public abstract interface class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver : kotlin/jvm/functions/Function1 { - public abstract fun invoke (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public final class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver$DefaultImpls { - public static fun invoke (Lapp/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver;Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public abstract interface class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver : kotlin/jvm/functions/Function1 { - public abstract fun invoke (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public final class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver$DefaultImpls { - public static fun invoke (Lapp/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver;Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public final class app/revanced/patches/shared/misc/mapping/ResourceMappingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement { - public fun (Ljava/lang/String;Ljava/lang/String;J)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()J - public final fun copy (Ljava/lang/String;Ljava/lang/String;J)Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement; - public static synthetic fun copy$default (Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement;Ljava/lang/String;Ljava/lang/String;JILjava/lang/Object;)Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()J - public final fun getName ()Ljava/lang/String; - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public abstract class app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable, java/util/Set, kotlin/jvm/internal/markers/KMutableSet { - public fun ()V - public fun (Lkotlin/Pair;Ljava/util/Set;)V - public synthetic fun (Lkotlin/Pair;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun add (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)Z - public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public fun close ()V - public fun contains (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)Z - public final fun contains (Ljava/lang/Object;)Z - public fun containsAll (Ljava/util/Collection;)Z - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public fun getSize ()I - public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public fun remove (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)Z - public final fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun retainAll (Ljava/util/Collection;)Z - public final fun size ()I - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; -} - -public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreference { - public static final field Companion Lapp/revanced/patches/shared/misc/settings/preference/BasePreference$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getKey ()Ljava/lang/String; - public final fun getSummaryKey ()Ljava/lang/String; - public final fun getTag ()Ljava/lang/String; - public final fun getTitleKey ()Ljava/lang/String; - public fun hashCode ()I - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/BasePreference$Companion { - public final fun addSummary (Lorg/w3c/dom/Element;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/SummaryType;)V - public static synthetic fun addSummary$default (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference$Companion;Lorg/w3c/dom/Element;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/SummaryType;ILjava/lang/Object;)V -} - -public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen : java/io/Closeable { - public fun ()V - public fun (Ljava/util/Set;)V - public synthetic fun (Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close ()V - public abstract fun commit (Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen;)V -} - -public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getKey ()Ljava/lang/String; - public final fun getPreferences ()Ljava/util/Set; - public final fun getTitleKey ()Ljava/lang/String; - public abstract fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; -} - -public class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { - public fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;)V - public synthetic fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun addPreferences ([Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V - public final fun getCategories ()Ljava/util/Set; - public synthetic fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; - public fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen; -} - -public class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen$Category : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { - public fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun addPreferences ([Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V - public synthetic fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; - public fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/PreferenceCategory; -} - -public final class app/revanced/patches/shared/misc/settings/preference/InputType : java/lang/Enum { - public static final field NUMBER Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static final field TEXT Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static final field TEXT_CAP_CHARACTERS Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static final field TEXT_MULTI_LINE Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getType ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/InputType; -} - -public final class app/revanced/patches/shared/misc/settings/preference/IntentPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getIntent ()Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; - public fun hashCode ()I - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent { - public fun (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; - public static synthetic fun copy$default (Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; - public fun equals (Ljava/lang/Object;)Z - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class app/revanced/patches/shared/misc/settings/preference/ListPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/util/resource/ArrayResource;Lapp/revanced/util/resource/ArrayResource;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/util/resource/ArrayResource;Lapp/revanced/util/resource/ArrayResource;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getEntries ()Lapp/revanced/util/resource/ArrayResource; - public final fun getEntriesKey ()Ljava/lang/String; - public final fun getEntryValues ()Lapp/revanced/util/resource/ArrayResource; - public final fun getEntryValuesKey ()Ljava/lang/String; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getSelectable ()Z - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public class app/revanced/patches/shared/misc/settings/preference/PreferenceCategory : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getPreferences ()Ljava/util/Set; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public class app/revanced/patches/shared/misc/settings/preference/PreferenceScreen : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getPreferences ()Ljava/util/Set; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting : java/lang/Enum { - public static final field BY_KEY Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static final field BY_TITLE Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static final field UNSORTED Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getKeySuffix ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; -} - -public final class app/revanced/patches/shared/misc/settings/preference/SummaryType : java/lang/Enum { - public static final field DEFAULT Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static final field OFF Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static final field ON Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getType ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; -} - -public final class app/revanced/patches/shared/misc/settings/preference/SwitchPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getSummaryOffKey ()Ljava/lang/String; - public final fun getSummaryOnKey ()Ljava/lang/String; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/TextPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/InputType;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/InputType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getInputType ()Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/songpal/badge/BadgeTabPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field ACTIVITY_TAB_DESCRIPTOR Ljava/lang/String; - public static final field INSTANCE Lapp/revanced/patches/songpal/badge/BadgeTabPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/songpal/badge/RemoveNotificationBadgePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/songpal/badge/RemoveNotificationBadgePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/spotify/layout/theme/CustomThemePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/layout/theme/CustomThemePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/spotify/lite/ondemand/OnDemandPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/lite/ondemand/OnDemandPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/navbar/PremiumNavbarTabPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/navbar/PremiumNavbarTabResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/strava/subscription/UnlockSubscriptionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/ticktick/misc/themeunlock/UnlockThemesPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/ticktick/misc/themeunlock/UnlockThemesPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/feedfilter/FeedFilterPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/feedfilter/FeedFilterPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/downloads/DownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/downloads/DownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/settings/SettingsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/settings/SettingsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/trakt/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/trakt/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tudortmund/lockscreen/patch/ShowOnLockscreenPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tudortmund/lockscreen/patch/ShowOnLockscreenPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/ads/DisableDashboardAds : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/ads/DisableDashboardAds; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/fixes/FixOldVersionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/fixes/FixOldVersionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/fixes/fingerprints/HttpPathParserFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/tumblr/fixes/fingerprints/HttpPathParserFingerprint; -} - -public final class app/revanced/patches/tumblr/live/DisableTumblrLivePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/live/DisableTumblrLivePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/timelinefilter/TimelineFilterPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/timelinefilter/TimelineFilterPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/ad/audio/AudioAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/ad/audio/AudioAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/twitch/ad/shared/util/BaseAdPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - protected final fun blockMethods (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;[Ljava/lang/String;Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod;)Z - public static synthetic fun blockMethods$default (Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch;Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;[Ljava/lang/String;Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod;ILjava/lang/Object;)Z - protected final fun createConditionInstructions (Ljava/lang/String;)Ljava/lang/String; - public static synthetic fun createConditionInstructions$default (Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; - public final fun getConditionCall ()Ljava/lang/String; - public final fun getSkipLabelName ()Ljava/lang/String; -} - -protected final class app/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod { - public fun ()V - public fun (CLjava/lang/String;)V - public synthetic fun (CLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()C - public final fun component2 ()Ljava/lang/String; - public final fun copy (CLjava/lang/String;)Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod; - public static synthetic fun copy$default (Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod;CLjava/lang/String;ILjava/lang/Object;)Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod; - public fun equals (Ljava/lang/Object;)Z - public final fun getReturnType ()C - public final fun getValue ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class app/revanced/patches/twitch/ad/video/VideoAdsPatch : app/revanced/patches/twitch/ad/shared/util/BaseAdPatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/ad/video/VideoAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/debug/DebugModePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/debug/DebugModePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/twitch/misc/settings/SettingsPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/twitch/misc/settings/SettingsPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/misc/settings/SettingsResourcePatch : app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/misc/settings/SettingsResourcePatch; -} - -public final class app/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/layout/viewcount/HideViewCountPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/layout/viewcount/HideViewCountPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/twitter/misc/hook/json/JsonHookPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/hook/json/JsonHookPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/twitter/misc/hook/patch/BaseHookPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/misc/hook/patch/ads/HideAdsHookPatch : app/revanced/patches/twitter/misc/hook/patch/BaseHookPatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/hook/patch/ads/HideAdsHookPatch; -} - -public final class app/revanced/patches/twitter/misc/hook/patch/recommendation/HideRecommendedUsersPatch : app/revanced/patches/twitter/misc/hook/patch/BaseHookPatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/hook/patch/recommendation/HideRecommendedUsersPatch; -} - -public final class app/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/vsco/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/vsco/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/windyapp/misc/unlockpro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/windyapp/misc/unlockpro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ad/general/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/general/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ad/general/HideAdsResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/general/HideAdsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ad/video/VideoAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/video/VideoAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/downloads/DownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/downloads/DownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/branding/CustomBrandingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/CustomBrandingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/branding/header/PremiumHeadingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/header/PremiumHeadingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/autoplay/HideAutoplayButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/autoplay/HideAutoplayButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/captions/HideCaptionsButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/captions/HideCaptionsButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/cast/HideCastButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/cast/HideCastButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/player/hide/HidePlayerButtonsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/player/hide/HidePlayerButtonsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/albumcards/AlbumCardsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/albumcards/AlbumCardsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/breakingnews/BreakingNewsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/breakingnews/BreakingNewsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/comments/CommentsPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/comments/CommentsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/crowdfundingbox/CrowdfundingBoxPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/crowdfundingbox/CrowdfundingBoxPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/filterbar/HideFilterBarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/filterbar/HideFilterBarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/floatingmicrophone/HideFloatingMicrophoneButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/floatingmicrophone/HideFloatingMicrophoneButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/infocards/HideInfocardsResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/infocards/HideInfocardsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/loadmorebutton/HideLoadMoreButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/loadmorebutton/HideLoadMoreButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/time/HideTimestampPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/seekbar/RestoreOldSeekbarThumbnailsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/seekbar/RestoreOldSeekbarThumbnailsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/sponsorblock/SponsorBlockBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/startpage/fingerprints/StartActivityFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/startpage/fingerprints/StartActivityFingerprint; -} - -public final class app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/startupshortsreset/fingerprints/UserWasInShortsFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/startupshortsreset/fingerprints/UserWasInShortsFingerprint; -} - -public final class app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/tabletminiplayer/TabletMiniPlayerPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/tabletminiplayer/TabletMiniPlayerPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/theme/ThemeBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/theme/ThemeBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/announcements/AnnouncementsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/debugging/DebuggingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/debugging/DebuggingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/ClientSpoofPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/ClientSpoofPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/SpoofSignaturePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/SpoofSignaturePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/SpoofSignatureResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/SpoofSignatureResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/gms/GmsCoreSupportResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Pair; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Pair;)V -} - -public final class app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/minimizedplayback/MinimizedPlaybackPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/minimizedplayback/MinimizedPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getHookNavigationButtonCreated ()Lkotlin/jvm/functions/Function1; -} - -public final class app/revanced/patches/youtube/misc/playercontrols/BottomControlsResourcePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playercontrols/BottomControlsResourcePatch; - public final fun addControls (Ljava/lang/String;)V - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/playercontrols/PlayerControlsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playercontrols/PlayerControlsBytecodePatch; - public static field showPlayerControlsFingerprintResult Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getShowPlayerControlsFingerprintResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public final fun initializeControl (Ljava/lang/String;)V - public final fun injectVisibilityCheckCall (Ljava/lang/String;)V - public final fun setShowPlayerControlsFingerprintResult (Lapp/revanced/patcher/fingerprint/MethodFingerprintResult;)V -} - -public final class app/revanced/patches/youtube/misc/playeroverlay/PlayerOverlaysHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playeroverlay/PlayerOverlaysHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/settings/SettingsPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/settings/SettingsPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun newIntent (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; -} - -public final class app/revanced/patches/youtube/misc/settings/SettingsPatch$PreferenceScreen : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/settings/SettingsPatch$PreferenceScreen; - public fun commit (Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen;)V - public final fun getADS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getALTERNATIVE_THUMBNAILS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getFEED ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; -} - -public final class app/revanced/patches/youtube/misc/settings/SettingsResourcePatch : app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/settings/SettingsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/hdrbrightness/HDRBrightnessPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/hdrbrightness/HDRBrightnessPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/information/VideoInformationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/information/VideoInformationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable, java/util/Set, kotlin/jvm/internal/markers/KMutableSet { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch; - public fun add (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public fun close ()V - public fun contains (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public final fun contains (Ljava/lang/Object;)Z - public fun containsAll (Ljava/util/Collection;)Z - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun getSize ()I - public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public fun remove (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public final fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun retainAll (Ljava/util/Collection;)Z - public final fun size ()I - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; -} - -public final class app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/quality/RememberVideoQualityPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/speed/PlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/videoid/VideoIdPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/videoid/VideoIdPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun hookBackgroundPlayVideoId (Ljava/lang/String;)V - public final fun hookPlayerResponseVideoId (Ljava/lang/String;)V - public final fun hookVideoId (Ljava/lang/String;)V -} - -public final class app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtubevanced/ad/general/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtubevanced/ad/general/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/util/BytecodeUtilsKt { - public static final fun containsWideLiteralInstructionValue (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z - public static final fun findIndexForIdResource (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I - public static final fun findMutableMethodOf (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static final fun getException (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/patch/PatchException; - public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)I - public static final fun indexOfFirstWideLiteralInstructionValue (Lcom/android/tools/smali/dexlib2/iface/Method;J)I - public static final fun injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V - public static final fun resultOrThrow (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public static final fun returnEarly (Ljava/util/List;Z)V - public static synthetic fun returnEarly$default (Ljava/util/List;ZILjava/lang/Object;)V - public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V - public static final fun traverseClassHierarchy (Lapp/revanced/patcher/data/BytecodeContext;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V -} - -public final class app/revanced/util/ResourceGroup { - public fun (Ljava/lang/String;[Ljava/lang/String;)V - public final fun getResourceDirectoryName ()Ljava/lang/String; - public final fun getResources ()[Ljava/lang/String; -} - -public final class app/revanced/util/ResourceUtilsKt { - public static final fun asSequence (Lorg/w3c/dom/NodeList;)Lkotlin/sequences/Sequence; - public static final fun childElementsSequence (Lorg/w3c/dom/Node;)Lkotlin/sequences/Sequence; - public static final fun copyResources (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;)V - public static final fun copyXmlNode (Ljava/lang/String;Lapp/revanced/patcher/util/DomFileEditor;Lapp/revanced/patcher/util/DomFileEditor;)Ljava/lang/AutoCloseable; - public static final fun doRecursively (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V - public static final fun forEachChildElement (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V - public static final fun iterateXmlNodeChildren (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V -} - -public abstract class app/revanced/util/patch/LiteralValueFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class app/revanced/util/resource/ArrayResource : app/revanced/util/resource/BaseResource { - public static final field Companion Lapp/revanced/util/resource/ArrayResource$Companion; - public fun (Ljava/lang/String;Ljava/util/List;)V - public final fun getItems ()Ljava/util/List; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/util/resource/ArrayResource$Companion { - public final fun fromNode (Lorg/w3c/dom/Node;)Lapp/revanced/util/resource/ArrayResource; -} - -public abstract class app/revanced/util/resource/BaseResource { - public fun (Ljava/lang/String;Ljava/lang/String;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getName ()Ljava/lang/String; - public final fun getTag ()Ljava/lang/String; - public fun hashCode ()I - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; - public static synthetic fun serialize$default (Lapp/revanced/util/resource/BaseResource;Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/util/resource/StringResource : app/revanced/util/resource/BaseResource { - public static final field Companion Lapp/revanced/util/resource/StringResource$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Z)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getFormatted ()Z - public final fun getValue ()Ljava/lang/String; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/util/resource/StringResource$Companion { - public final fun fromNode (Lorg/w3c/dom/Node;)Lapp/revanced/util/resource/StringResource; -} - diff --git a/build.gradle.kts b/build.gradle.kts index 3da402dadc..fcaeeb54cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,155 +1,3 @@ -import org.gradle.kotlin.dsl.support.listFilesOrdered -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - plugins { - alias(libs.plugins.kotlin) - alias(libs.plugins.ktlint) - alias(libs.plugins.binary.compatibility.validator) - `maven-publish` - signing -} - -group = "app.revanced" - -repositories { - mavenCentral() - mavenLocal() - google() - maven { - // A repository must be speficied for some reason. "registry" is a dummy. - url = uri("https://maven.pkg.github.com/revanced/registry") - credentials { - username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") - } - } -} - -dependencies { - implementation(libs.revanced.patcher) - implementation(libs.smali) - // TODO: Required because build fails without it. Find a way to remove this dependency. - implementation(libs.guava) - // Used in JsonGenerator. - implementation(libs.gson) -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -java { - targetCompatibility = JavaVersion.VERSION_11 -} - -tasks { - withType(Jar::class) { - exclude("app/revanced/meta") - - manifest { - attributes["Name"] = "ReVanced Patches" - attributes["Description"] = "Patches for ReVanced." - attributes["Version"] = version - attributes["Timestamp"] = System.currentTimeMillis().toString() - attributes["Source"] = "git@github.com:revanced/revanced-patches.git" - attributes["Author"] = "ReVanced" - attributes["Contact"] = "contact@revanced.app" - attributes["Origin"] = "https://revanced.app" - attributes["License"] = "GNU General Public License v3.0" - } - } - - register("buildDexJar") { - description = "Build and add a DEX to the JAR file" - group = "build" - - dependsOn(build) - - doLast { - val d8 = - File(System.getenv("ANDROID_HOME")).resolve("build-tools") - .listFilesOrdered().last().resolve("d8").absolutePath - - val patchesJar = configurations.archives.get().allArtifacts.files.files.first().absolutePath - val workingDirectory = layout.buildDirectory.dir("libs").get().asFile - - exec { - workingDir = workingDirectory - commandLine = listOf(d8, "--release", patchesJar) - } - - exec { - workingDir = workingDirectory - commandLine = listOf("zip", "-u", patchesJar, "classes.dex") - } - } - } - - register("generatePatchesFiles") { - description = "Generate patches files" - - dependsOn(build) - - classpath = sourceSets["main"].runtimeClasspath - mainClass.set("app.revanced.generator.MainKt") - } - - // Needed by gradle-semantic-release-plugin. - // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - publish { - dependsOn("buildDexJar") - dependsOn("generatePatchesFiles") - } -} - -publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/revanced/revanced-patches") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - } - - publications { - create("revanced-patches-publication") { - from(components["java"]) - - pom { - name = "ReVanced Patches" - description = "Patches for ReVanced." - url = "https://revanced.app" - - licenses { - license { - name = "GNU General Public License v3.0" - url = "https://www.gnu.org/licenses/gpl-3.0.en.html" - } - } - developers { - developer { - id = "ReVanced" - name = "ReVanced" - email = "contact@revanced.app" - } - } - scm { - connection = "scm:git:git://github.com/revanced/revanced-patches.git" - developerConnection = "scm:git:git@github.com:revanced/revanced-patches.git" - url = "https://github.com/revanced/revanced-patches" - } - } - } - } -} - -signing { - useGpgCmd() - - sign(publishing.publications["revanced-patches-publication"]) + alias(libs.plugins.android.library) apply false } diff --git a/crowdin.yml b/crowdin.yml index 4ac3cb98b3..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: src/main/resources/addresources/values/strings.xml - translation: src/main/resources/addresources/values-%android_code%/strings.xml + - source: patches/src/main/resources/addresources/values/strings.xml + 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 new file mode 100644 index 0000000000..c269c9862f --- /dev/null +++ b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/build.gradle.kts @@ -0,0 +1,9 @@ +android { + defaultConfig { + minSdk = 23 + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/AndroidManifest.xml b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0bfc2a5326 --- /dev/null +++ b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/misc/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 new file mode 100644 index 0000000000..9c1702f1bc --- /dev/null +++ b/extensions/all/misc/connectivity/wifi/spoof/spoof-wifi/src/main/java/app/revanced/extension/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.java @@ -0,0 +1,424 @@ +package app.revanced.extension.all.misc.connectivity.wifi.spoof; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.os.Build; +import android.os.Handler; + +import androidx.annotation.RequiresApi; + +@SuppressWarnings({"deprecation", "unused"}) +public class SpoofWifiPatch { + + // Used to check what the (real or fake) active network is (take a look at `hasTransport`). + private static ConnectivityManager CONNECTIVITY_MANAGER; + + // If Wifi is not enabled, these are types that would pretend to be Wifi for android.net.Network (lower index = higher priority). + // This does not apply to android.net.NetworkInfo, because we can pretend that Wifi is always active there. + // + // VPN should be a fallback, because Reverse Tethering uses VPN. + private static final int[] FAKE_FALLBACK_NETWORKS = { NetworkCapabilities.TRANSPORT_ETHERNET, NetworkCapabilities.TRANSPORT_VPN }; + + // In order to initialize our own ConnectivityManager, if it isn't initialized yet. + public static Object getSystemService(Context context, String name) { + Object result = context.getSystemService(name); + if (CONNECTIVITY_MANAGER == null) { + if (Context.CONNECTIVITY_SERVICE.equals(name)) { + CONNECTIVITY_MANAGER = (ConnectivityManager) result; + } else { + CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + } + return result; + } + + // In order to initialize our own ConnectivityManager, if it isn't initialized yet. + public static Object getSystemService(Context context, Class serviceClass) { + Object result = context.getSystemService(serviceClass); + if (CONNECTIVITY_MANAGER == null) { + if (serviceClass == ConnectivityManager.class) { + CONNECTIVITY_MANAGER = (ConnectivityManager) result; + } else { + CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + } + return result; + } + + // Simply always return Wifi as active network. + public static NetworkInfo getActiveNetworkInfo(ConnectivityManager connectivityManager) { + for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return networkInfo; + } + } + return connectivityManager.getActiveNetworkInfo(); + } + + // Pretend Wifi is always connected. + public static boolean isConnected(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return true; + } + return networkInfo.isConnected(); + } + + // Pretend Wifi is always connected. + public static boolean isConnectedOrConnecting(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return true; + } + return networkInfo.isConnectedOrConnecting(); + } + + // Pretend Wifi is always available. + public static boolean isAvailable(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return true; + } + return networkInfo.isAvailable(); + } + + // Pretend Wifi is always connected. + public static NetworkInfo.State getState(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return NetworkInfo.State.CONNECTED; + } + return networkInfo.getState(); + } + + // Pretend Wifi is always connected. + public static NetworkInfo.DetailedState getDetailedState(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return NetworkInfo.DetailedState.CONNECTED; + } + return networkInfo.getDetailedState(); + } + + // Pretend Wifi is enabled, so connection isn't metered. + public static boolean isActiveNetworkMetered(ConnectivityManager connectivityManager) { + return false; + } + + // Returns the Wifi network, if Wifi is enabled. + // Otherwise if one of our fallbacks has a connection, return them. + // And as a last resort, return the default active network. + public static Network getActiveNetwork(ConnectivityManager connectivityManager) { + Network[] prioritizedNetworks = new Network[FAKE_FALLBACK_NETWORKS.length]; + for (Network network : connectivityManager.getAllNetworks()) { + NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network); + if (networkCapabilities == null) { + continue; + } + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return network; + } + if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + for (int i = 0; i < FAKE_FALLBACK_NETWORKS.length; i++) { + int transportType = FAKE_FALLBACK_NETWORKS[i]; + if (networkCapabilities.hasTransport(transportType)) { + prioritizedNetworks[i] = network; + break; + } + } + } + } + for (Network network : prioritizedNetworks) { + if (network != null) { + return network; + } + } + return connectivityManager.getActiveNetwork(); + } + + // If the given network is a real or fake Wifi connection, return a Wifi network. + // Otherwise fallback to default implementation. + public static NetworkInfo getNetworkInfo(ConnectivityManager connectivityManager, Network network) { + NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network); + if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) { + for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return networkInfo; + } + } + } + return connectivityManager.getNetworkInfo(network); + } + + // If we are checking if the NetworkCapabilities use Wifi, return yes if + // - it is a real Wifi connection, + // - or the NetworkCapabilities are from a network pretending being a Wifi network. + // Otherwise fallback to default implementation. + public static boolean hasTransport(NetworkCapabilities networkCapabilities, int transportType) { + if (transportType == NetworkCapabilities.TRANSPORT_WIFI) { + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return true; + } + if (CONNECTIVITY_MANAGER != null) { + Network activeNetwork = getActiveNetwork(CONNECTIVITY_MANAGER); + NetworkCapabilities activeNetworkCapabilities = CONNECTIVITY_MANAGER.getNetworkCapabilities(activeNetwork); + if (activeNetworkCapabilities != null) { + for (int fallbackTransportType : FAKE_FALLBACK_NETWORKS) { + if (activeNetworkCapabilities.hasTransport(fallbackTransportType) && networkCapabilities.hasTransport(fallbackTransportType)) { + return true; + } + } + } + } + } + return networkCapabilities.hasTransport(transportType); + } + + // If the given network is a real or fake Wifi connection, pretend it has a connection (and some other things). + public static boolean hasCapability(NetworkCapabilities networkCapabilities, int capability) { + if (hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI) && ( + capability == NetworkCapabilities.NET_CAPABILITY_INTERNET + || capability == NetworkCapabilities.NET_CAPABILITY_FOREGROUND + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_METERED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_VPN + || capability == NetworkCapabilities.NET_CAPABILITY_TRUSTED + || capability == NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + return true; + } + return networkCapabilities.hasCapability(capability); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.S) + public static void registerBestMatchingNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.registerBestMatchingNetworkCallback(request, networkCallback, handler) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.N) + public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) { + Utils.networkCallback( + connectivityManager, + Utils.Option.empty(), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.registerDefaultNetworkCallback(networkCallback) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.empty(), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.registerDefaultNetworkCallback(networkCallback, handler) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.registerNetworkCallback(request, networkCallback) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately. + public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.empty(), + Utils.Option.of(operation), + Utils.Option.empty(), + () -> connectivityManager.registerNetworkCallback(request, operation) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.registerNetworkCallback(request, networkCallback, handler) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.requestNetwork(request, networkCallback) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, int timeoutMs) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.requestNetwork(request, networkCallback, timeoutMs) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.requestNetwork(request, networkCallback, handler) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately. + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.empty(), + Utils.Option.of(operation), + Utils.Option.empty(), + () -> connectivityManager.requestNetwork(request, operation) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler, int timeoutMs) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.requestNetwork(request, networkCallback, handler, timeoutMs) + ); + } + + public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) { + try { + connectivityManager.unregisterNetworkCallback(networkCallback); + } catch (IllegalArgumentException ignore) { + // ignore: NetworkCallback was not registered + } + } + + public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, PendingIntent operation) { + try { + connectivityManager.unregisterNetworkCallback(operation); + } catch (IllegalArgumentException ignore) { + // ignore: PendingIntent was not registered + } + } + + private static class Utils { + private static class Option { + private final T value; + private final boolean isPresent; + + private Option(T value, boolean isPresent) { + this.value = value; + this.isPresent = isPresent; + } + + private static Option of(T value) { + return new Option<>(value, true); + } + + private static Option empty() { + return new Option<>(null, false); + } + } + + private static void networkCallback( + ConnectivityManager connectivityManager, + Option request, + Option networkCallback, + Option operation, + Option handler, + Runnable fallback + ) { + if(!request.isPresent || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && request.value != null && requestsWifiNetwork(request.value))) { + Runnable runnable = null; + if (networkCallback.isPresent && networkCallback.value != null) { + Network network = activeWifiNetwork(connectivityManager); + if (network != null) { + runnable = () -> networkCallback.value.onAvailable(network); + } + } else if (operation.isPresent && operation.value != null) { + runnable = () -> { + try { + operation.value.send(); + } catch (PendingIntent.CanceledException ignore) {} + }; + } + if (runnable != null) { + if (handler.isPresent) { + if (handler.value != null) { + handler.value.post(runnable); + return; + } + } else { + runnable.run(); + return; + } + } + } + fallback.run(); + } + + // Returns an active (maybe fake) Wifi network if there is one, otherwise null. + private static Network activeWifiNetwork(ConnectivityManager connectivityManager) { + Network network = getActiveNetwork(connectivityManager); + NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network); + if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) { + return network; + } + return null; + } + + // Whether a Wifi network with connection is requested. + @RequiresApi(api = Build.VERSION_CODES.P) + private static boolean requestsWifiNetwork(NetworkRequest request) { + return request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + && (request.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + || request.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)); + } + } +} 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 new file mode 100644 index 0000000000..42eb9984c0 --- /dev/null +++ b/extensions/all/misc/screencapture/remove-screen-capture-restriction/build.gradle.kts @@ -0,0 +1,9 @@ +android { + defaultConfig { + minSdk = 21 + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/AndroidManifest.xml b/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/misc/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 new file mode 100644 index 0000000000..653d4d3942 --- /dev/null +++ b/extensions/all/misc/screencapture/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/misc/screencapture/removerestriction/RemoveScreenCaptureRestrictionPatch.java @@ -0,0 +1,22 @@ +package app.revanced.extension.all.misc.screencapture.removerestriction; + +import android.media.AudioAttributes; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +@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) { + builder.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL); + + return builder; + } + + // Member of AudioManager static class + public static void setAllowedCapturePolicy(final int capturePolicy) { + // Ignore request + } +} diff --git a/extensions/all/misc/screenshot/remove-screenshot-restriction/build.gradle.kts b/extensions/all/misc/screenshot/remove-screenshot-restriction/build.gradle.kts new file mode 100644 index 0000000000..42eb9984c0 --- /dev/null +++ b/extensions/all/misc/screenshot/remove-screenshot-restriction/build.gradle.kts @@ -0,0 +1,9 @@ +android { + defaultConfig { + minSdk = 21 + } +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/AndroidManifest.xml b/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/misc/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java b/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/misc/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java new file mode 100644 index 0000000000..22254f6285 --- /dev/null +++ b/extensions/all/misc/screenshot/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/misc/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java @@ -0,0 +1,16 @@ +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) { + window.addFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE); + } + + public static void setFlags(Window window, int flags, int mask) { + window.setFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE, mask & ~WindowManager.LayoutParams.FLAG_SECURE); + } +} 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 new file mode 100644 index 0000000000..d84b488441 --- /dev/null +++ b/extensions/boostforreddit/build.gradle.kts @@ -0,0 +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/AndroidManifest.xml b/extensions/boostforreddit/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/boostforreddit/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + 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/src/main/java/app/revanced/extension/boostforreddit/FixSLinksPatch.java b/extensions/boostforreddit/src/main/java/app/revanced/extension/boostforreddit/FixSLinksPatch.java new file mode 100644 index 0000000000..1d6089737e --- /dev/null +++ b/extensions/boostforreddit/src/main/java/app/revanced/extension/boostforreddit/FixSLinksPatch.java @@ -0,0 +1,26 @@ +package app.revanced.extension.boostforreddit; + +import com.rubenmayayo.reddit.ui.activities.WebViewActivity; + +import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch; + +/** + * @noinspection unused + */ +public class FixSLinksPatch extends BaseFixSLinksPatch { + static { + INSTANCE = new FixSLinksPatch(); + } + + private FixSLinksPatch() { + webViewActivityClass = WebViewActivity.class; + } + + public static boolean patchResolveSLink(String link) { + return INSTANCE.resolveSLink(link); + } + + public static void patchSetAccessToken(String accessToken) { + INSTANCE.setAccessToken(accessToken); + } +} diff --git a/extensions/boostforreddit/stub/build.gradle.kts b/extensions/boostforreddit/stub/build.gradle.kts new file mode 100644 index 0000000000..b4bee8809f --- /dev/null +++ b/extensions/boostforreddit/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/boostforreddit/stub/src/main/AndroidManifest.xml b/extensions/boostforreddit/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/boostforreddit/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/boostforreddit/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java b/extensions/boostforreddit/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java new file mode 100644 index 0000000000..d0c5850729 --- /dev/null +++ b/extensions/boostforreddit/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java @@ -0,0 +1,6 @@ +package com.rubenmayayo.reddit.ui.activities; + +import android.app.Activity; + +public class WebViewActivity extends Activity { +} 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/proguard-rules.pro b/extensions/proguard-rules.pro new file mode 100644 index 0000000000..8f804140d6 --- /dev/null +++ b/extensions/proguard-rules.pro @@ -0,0 +1,9 @@ +-dontobfuscate +-dontoptimize +-keepattributes * +-keep class app.revanced.** { + *; +} +-keep class com.google.** { + *; +} diff --git a/extensions/reddit/build.gradle.kts b/extensions/reddit/build.gradle.kts new file mode 100644 index 0000000000..75c8d7a179 --- /dev/null +++ b/extensions/reddit/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + compileOnly(project(":extensions:reddit:stub")) +} + +android { + defaultConfig { + minSdk = 28 + } +} diff --git a/extensions/reddit/src/main/AndroidManifest.xml b/extensions/reddit/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..9b65eb06cf --- /dev/null +++ b/extensions/reddit/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java b/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java new file mode 100644 index 0000000000..12cdc88345 --- /dev/null +++ b/extensions/reddit/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java @@ -0,0 +1,27 @@ +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) { + final List filteredList = new ArrayList<>(); + + for (Object item : links) { + if (item instanceof ILink && ((ILink) item).getPromoted()) continue; + + filteredList.add(item); + } + + return filteredList; + } +} diff --git a/extensions/reddit/stub/build.gradle.kts b/extensions/reddit/stub/build.gradle.kts new file mode 100644 index 0000000000..b4bee8809f --- /dev/null +++ b/extensions/reddit/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/reddit/stub/src/main/AndroidManifest.xml b/extensions/reddit/stub/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/reddit/stub/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/reddit/stub/src/main/java/com/reddit/domain/model/ILink.java b/extensions/reddit/stub/src/main/java/com/reddit/domain/model/ILink.java new file mode 100644 index 0000000000..f9cbb955cb --- /dev/null +++ b/extensions/reddit/stub/src/main/java/com/reddit/domain/model/ILink.java @@ -0,0 +1,7 @@ +package com.reddit.domain.model; + +public class ILink { + public boolean getPromoted() { + throw new UnsupportedOperationException("Stub"); + } +} 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 new file mode 100644 index 0000000000..3eb6ff48c7 --- /dev/null +++ b/extensions/shared/build.gradle.kts @@ -0,0 +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 new file mode 100644 index 0000000000..8215e513ad --- /dev/null +++ b/extensions/shared/library/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.android.library) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 23 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + compileOnly(libs.annotation) + compileOnly(libs.okhttp) + compileOnly(libs.protobuf.javalite) + implementation(project(":extensions:shared:protobuf", configuration = "shadowRuntimeElements")) +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java new file mode 100644 index 0000000000..c91de4a7aa --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/ByteTrieSearch.java @@ -0,0 +1,43 @@ +package app.revanced.extension.shared; + +import java.nio.charset.StandardCharsets; + +public final class ByteTrieSearch extends TrieSearch { + + private static final class ByteTrieNode extends TrieNode { + ByteTrieNode() { + super(); + } + ByteTrieNode(char nodeCharacterValue) { + super(nodeCharacterValue); + } + @Override + TrieNode createNode(char nodeCharacterValue) { + return new ByteTrieNode(nodeCharacterValue); + } + @Override + char getCharValue(byte[] text, int index) { + return (char) text[index]; + } + @Override + int getTextLength(byte[] text) { + return text.length; + } + } + + /** + * Helper method for the common usage of converting Strings to raw UTF-8 bytes. + */ + public static byte[][] convertStringsToBytes(String... strings) { + final int length = strings.length; + byte[][] replacement = new byte[length][]; + for (int i = 0; i < length; i++) { + replacement[i] = strings[i].getBytes(StandardCharsets.UTF_8); + } + return replacement; + } + + 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 new file mode 100644 index 0000000000..fb7e68963a --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java @@ -0,0 +1,412 @@ +package app.revanced.extension.shared; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.app.SearchManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.provider.Settings; +import android.util.Pair; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.ui.CustomDialog; + +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Locale; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.requests.Route.Method.GET; + +@SuppressWarnings("unused") +public class GmsCoreSupport { + private static GmsCore gmsCore = GmsCore.UNKNOWN; + + static { + for (GmsCore core : GmsCore.values()) { + if (core.getGroupId().equals(getGmsCoreVendorGroupId())) { + GmsCoreSupport.gmsCore = core; + break; + } + } + } + + /** + * Injection point. + */ + public static void checkGmsCore(Activity context) { + gmsCore.check(context); + } + + private static String getOriginalPackageName() { + return null; // Modified during patching. + } + + private static String getGmsCoreVendorGroupId() { + return "app.revanced"; // Modified during patching. + } + + + /** + * @return If the current package name is the same as the original unpatched app. + * If `GmsCore support` was not included during patching, this returns true; + */ + 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 { + 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; + } + + Utils.runOnBackgroundThread(() -> { + try { + PackageManager manager = context.getPackageManager(); + var installedVersion = manager.getPackageInfo(packageName, 0).versionName; + + // 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"; + } + + open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER); + } + + /** + * @return If GmsCore is not whitelisted from battery optimizations. + */ + private boolean batteryOptimizationsEnabled(Context context) { + //noinspection ObsoleteSdkInt + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Android 5.0 does not have battery optimization settings. + return false; + } + var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return !powerManager.isIgnoringBatteryOptimizations(packageName); + } + + private boolean isAndroidAutomotive(Context context) { + return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); + } + } + + @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 new file mode 100644 index 0000000000..610cd3414f --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Logger.java @@ -0,0 +1,214 @@ +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 java.io.PrintWriter; +import java.io.StringWriter; + +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 { + + /** + * Log messages using lambdas. + */ + @FunctionalInterface + public interface LogMessage { + /** + * @return Logger string message. This method is only called if logging is enabled. + */ + @NonNull + String buildMessageString(); + } + + private enum LogLevel { + DEBUG, + INFO, + ERROR + } + + /** + * 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; + } + } + + 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 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. + */ + 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. + */ + 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(LogMessage message) { + printInfo(message, null); + } + + /** + * Logs information messages using the outer class name of the code calling this method. + */ + 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(LogMessage message) { + printException(message, null); + } + + /** + * Logs exceptions under the outer class name of the code calling this method. + *

+ * 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(LogMessage message, @Nullable Throwable ex) { + logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast()); + } +} 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 new file mode 100644 index 0000000000..c1c2c90d14 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringRef.java @@ -0,0 +1,122 @@ +package app.revanced.extension.shared; + +import android.content.Context; +import android.content.res.Resources; + +import androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class StringRef { + private static Resources resources; + private static String packageName; + + // must use a thread safe map, as this class is used both on and off the main thread + private static final Map strings = Collections.synchronizedMap(new HashMap<>()); + + /** + * Returns a cached instance. + * Should be used if the same String could be loaded more than once. + * + * @param id string resource name/id + * @see #sf(String) + */ + @NonNull + public static StringRef sfc(@NonNull String id) { + StringRef ref = strings.get(id); + if (ref == null) { + ref = new StringRef(id); + strings.put(id, ref); + } + return ref; + } + + /** + * Creates a new instance, but does not cache the value. + * Should be used for Strings that are loaded exactly once. + * + * @param id string resource name/id + * @see #sfc(String) + */ + @NonNull + public static StringRef sf(@NonNull String id) { + return new StringRef(id); + } + + /** + * Gets string value by string id, shorthand for sfc(id).toString() + * + * @param id string resource name/id + * @return String value from string.xml + */ + @NonNull + public static String str(@NonNull String id) { + return sfc(id).toString(); + } + + /** + * Gets string value by string id, shorthand for sfc(id).toString() and formats the string + * with given args. + * + * @param id string resource name/id + * @param args the args to format the string with + * @return String value from string.xml formatted with given args + */ + @NonNull + public static String str(@NonNull String id, Object... args) { + return String.format(str(id), args); + } + + /** + * Creates a StringRef object that'll not change its value + * + * @param value value which toString() method returns when invoked on returned object + * @return Unique StringRef instance, its value will never change + */ + @NonNull + public static StringRef constant(@NonNull String value) { + final StringRef ref = new StringRef(value); + ref.resolved = true; + return ref; + } + + /** + * Shorthand for constant("") + * Its value always resolves to empty string + */ + @NonNull + public static final StringRef empty = constant(""); + + @NonNull + private String value; + private boolean resolved; + + public StringRef(@NonNull String resName) { + this.value = resName; + } + + @Override + @NonNull + public String toString() { + if (!resolved) { + if (resources == null || packageName == null) { + var context = Utils.getContext(); + resources = context.getResources(); + packageName = context.getPackageName(); + } + resolved = true; + if (resources != null) { + final int identifier = resources.getIdentifier(value, "string", packageName); + if (identifier == 0) + Logger.printException(() -> "Resource not found: " + value); + else + value = resources.getString(identifier); + } else { + Logger.printException(() -> "Could not resolve resources!"); + } + } + return value; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java new file mode 100644 index 0000000000..9c7b882138 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/StringTrieSearch.java @@ -0,0 +1,32 @@ +package app.revanced.extension.shared; + +/** + * Text pattern searching using a prefix tree (trie). + */ +public final class StringTrieSearch extends TrieSearch { + + private static final class StringTrieNode extends TrieNode { + StringTrieNode() { + super(); + } + StringTrieNode(char nodeCharacterValue) { + super(nodeCharacterValue); + } + @Override + TrieNode createNode(char nodeValue) { + return new StringTrieNode(nodeValue); + } + @Override + char getCharValue(String text, int index) { + return text.charAt(index); + } + @Override + int getTextLength(String text) { + return text.length(); + } + } + + public StringTrieSearch(String... patterns) { + super(new StringTrieNode(), patterns); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java new file mode 100644 index 0000000000..97fa4605d8 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/TrieSearch.java @@ -0,0 +1,425 @@ +package app.revanced.extension.shared; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Searches for a group of different patterns using a trie (prefix tree). + * Can significantly speed up searching for multiple patterns. + */ +public abstract class TrieSearch { + + public interface TriePatternMatchedCallback { + /** + * Called when a pattern is matched. + * + * @param textSearched Text that was searched. + * @param matchedStartIndex Start index of the search text, where the pattern was matched. + * @param matchedLength Length of the match. + * @param callbackParameter Optional parameter passed into {@link TrieSearch#matches(Object, Object)}. + * @return True, if the search should stop here. + * If false, searching will continue to look for other matches. + */ + boolean patternMatched(T textSearched, int matchedStartIndex, int matchedLength, Object callbackParameter); + } + + /** + * Represents a compressed tree path for a single pattern that shares no sibling nodes. + * + * For example, if a tree contains the patterns: "foobar", "football", "feet", + * it would contain 3 compressed paths of: "bar", "tball", "eet". + * + * And the tree would contain children arrays only for the first level containing 'f', + * the second level containing 'o', + * and the third level containing 'o'. + * + * This is done to reduce memory usage, which can be substantial if many long patterns are used. + */ + private static final class TrieCompressedPath { + final T pattern; + final int patternStartIndex; + final int patternLength; + final TriePatternMatchedCallback callback; + + TrieCompressedPath(T pattern, int patternStartIndex, int patternLength, TriePatternMatchedCallback callback) { + this.pattern = pattern; + this.patternStartIndex = patternStartIndex; + this.patternLength = patternLength; + this.callback = callback; + } + boolean matches(TrieNode enclosingNode, // Used only for the get character method. + T searchText, int searchTextLength, int searchTextIndex, Object callbackParameter) { + 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); + } + } + + static abstract class TrieNode { + /** + * Dummy value used for root node. Value can be anything as it's never referenced. + */ + private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character. + + /** + * How much to expand the children array when resizing. + */ + private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2; + + /** + * Character this node represents. + * This field is ignored for the root node (which does not represent any character). + */ + private final char nodeValue; + + /** + * A compressed graph path that represents the remaining pattern characters of a single child node. + * + * If present then child array is always null, although callbacks for other + * end of patterns can also exist on this same node. + */ + @Nullable + private TrieCompressedPath leaf; + + /** + * All child nodes. Only present if no compressed leaf exist. + * + * Array is dynamically increased in size as needed, + * and uses perfect hashing for the elements it contains. + * + * So if the array contains a given character, + * the character will always map to the node with index: (character % arraySize). + * + * Elements not contained can collide with elements the array does contain, + * so must compare the nodes character value. + * + /* + * Alternatively, this could be implemented as a sorted, densely packed array + * with lookups performed via binary search. + * This approach would save a small amount of memory by eliminating null + * child entries. However, it would result in a worst-case lookup time of + * O(n log m), where: + * - n is the number of characters in the input text, and + * - m is the maximum size of the sorted character arrays. + * In contrast, using a hash-based array guarantees O(n) lookup time. + * Given that the total memory usage is already very small (all Litho filters + * together use approximately 10KB), the hash-based implementation is preferred + * for its superior performance. + */ + @Nullable + private TrieNode[] children; + + /** + * Callbacks for all patterns that end at this node. + */ + @Nullable + private List> endOfPatternCallback; + + TrieNode() { + this.nodeValue = ROOT_NODE_CHARACTER_VALUE; + } + TrieNode(char nodeCharacterValue) { + this.nodeValue = nodeCharacterValue; + } + + /** + * @param pattern Pattern to add. + * @param patternIndex Current recursive index of the pattern. + * @param patternLength Length of the pattern. + * @param callback Callback, where a value of NULL indicates to always accept a pattern match. + */ + private void addPattern(T pattern, int patternIndex, int patternLength, + @Nullable TriePatternMatchedCallback callback) { + if (patternIndex == patternLength) { // Reached the end of the pattern. + if (endOfPatternCallback == null) { + endOfPatternCallback = new ArrayList<>(1); + } + 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. + if (children != null) throw new IllegalStateException(); + //noinspection unchecked + children = new TrieNode[1]; + TrieCompressedPath temp = leaf; + leaf = null; + addPattern(temp.pattern, temp.patternStartIndex, temp.patternLength, temp.callback); + // Continue onward and add the parameter pattern. + } else if (children == null) { + 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]; + if (child == null) { + child = createNode(character); + children[arrayIndex] = child; + } else if (child.nodeValue != character) { + // Hash collision. Resize the table until perfect hashing is found. + child = createNode(character); + expandChildArray(child); + } + child.addPattern(pattern, patternIndex + 1, patternLength, callback); + } + + /** + * Resizes the children table until all nodes hash to exactly one array index. + */ + private void expandChildArray(TrieNode child) { + int replacementArraySize = Objects.requireNonNull(children).length; + while (true) { + replacementArraySize += CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT; + //noinspection unchecked + TrieNode[] replacement = new TrieNode[replacementArraySize]; + addNodeToArray(replacement, child); + + boolean collision = false; + for (TrieNode existingChild : children) { + if (existingChild != null) { + if (!addNodeToArray(replacement, existingChild)) { + collision = true; + break; + } + } + } + if (collision) { + continue; + } + + children = replacement; + return; + } + } + + private static boolean addNodeToArray(TrieNode[] array, TrieNode childToAdd) { + final int insertIndex = hashIndexForTableSize(array.length, childToAdd.nodeValue); + if (array[insertIndex] != null ) { + return false; // Collision. + } + array[insertIndex] = childToAdd; + return true; + } + + private static int hashIndexForTableSize(int arraySize, char nodeValue) { + return nodeValue % arraySize; + } + + /** + * This method is static and uses a loop to avoid all recursion. + * This is done for performance since the JVM does not optimize tail recursion. + * + * @param startNode Node to start the search from. + * @param searchText Text to search for patterns in. + * @param searchTextIndex Start index, inclusive. + * @param searchTextEndIndex End index, exclusive. + * @return If any pattern matches, and it's associated callback halted the search. + */ + private static boolean matches(final TrieNode startNode, final T searchText, + int searchTextIndex, final int searchTextEndIndex, + final Object callbackParameter) { + TrieNode node = startNode; + int currentMatchLength = 0; + + while (true) { + TrieCompressedPath leaf = node.leaf; + 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; + for (@Nullable TriePatternMatchedCallback callback : endOfPatternCallback) { + if (callback == null) { + return true; // No callback and all matches are valid. + } + if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) { + return true; // Callback confirmed the match. + } + } + } + + TrieNode[] children = node.children; + if (children == null) { + return false; // Reached a graph end point and there's no further patterns to search. + } + if (searchTextIndex == searchTextEndIndex) { + return false; // Reached end of the search text and found no matches. + } + + // Use the start node to reduce VM method lookup, since all nodes are the same class type. + final char character = startNode.getCharValue(searchText, searchTextIndex); + final int arrayIndex = hashIndexForTableSize(children.length, character); + TrieNode child = children[arrayIndex]; + if (child == null || child.nodeValue != character) { + return false; + } + + node = child; + searchTextIndex++; + currentMatchLength++; + } + } + + /** + * Gives an approximate memory usage. + * + * @return Estimated number of memory pointers used, starting from this node and including all children. + */ + private int estimatedNumberOfPointersUsed() { + int numberOfPointers = 4; // Number of fields in this class. + 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) { + if (child != null) { + numberOfPointers += child.estimatedNumberOfPointersUsed(); + } + } + } + return numberOfPointers; + } + + abstract TrieNode createNode(char nodeValue); + abstract char getCharValue(T text, int index); + abstract int getTextLength(T text); + } + + /** + * Root node, and it's children represent the first pattern characters. + */ + private final TrieNode root; + + /** + * Patterns to match. + */ + private final List patterns = new ArrayList<>(); + + @SafeVarargs + TrieSearch(TrieNode root, T... patterns) { + this.root = Objects.requireNonNull(root); + addPatterns(patterns); + } + + @SafeVarargs + public final void addPatterns(T... patterns) { + for (T pattern : patterns) { + addPattern(pattern); + } + } + + /** + * Adds a pattern that will always return a positive match if found. + * + * @param pattern Pattern to add. Calling this with a zero length pattern does nothing. + */ + public void addPattern(T pattern) { + addPattern(pattern, root.getTextLength(pattern), null); + } + + /** + * @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(T pattern, TriePatternMatchedCallback callback) { + addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(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(T textToSearch) { + return matches(textToSearch, 0); + } + + public boolean matches(T textToSearch, Object callbackParameter) { + return matches(textToSearch, 0, root.getTextLength(textToSearch), + Objects.requireNonNull(callbackParameter)); + } + + public boolean matches(T textToSearch, int startIndex) { + return matches(textToSearch, startIndex, root.getTextLength(textToSearch)); + } + + public final boolean matches(T textToSearch, int startIndex, int endIndex) { + return matches(textToSearch, startIndex, endIndex, null); + } + + /** + * Searches through text, looking for any substring that matches any pattern in this tree. + * + * @param textToSearch Text to search through. + * @param startIndex Index to start searching, inclusive value. + * @param endIndex Index to stop matching, exclusive value. + * @param callbackParameter Optional parameter passed to the callbacks. + * @return If any pattern matched, and it's callback halted searching. + */ + public boolean matches(T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) { + return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter); + } + + private boolean matches(T textToSearch, int textToSearchLength, int startIndex, int endIndex, + @Nullable Object callbackParameter) { + if (endIndex > textToSearchLength) { + throw new IllegalArgumentException("endIndex: " + endIndex + + " is greater than texToSearchLength: " + textToSearchLength); + } + if (patterns.isEmpty()) { + return false; // No patterns were added. + } + for (int i = startIndex; i < endIndex; i++) { + if (TrieNode.matches(root, textToSearch, i, endIndex, callbackParameter)) return true; + } + return false; + } + + /** + * @return Estimated memory size (in kilobytes) of this instance. + */ + public int getEstimatedMemorySize() { + if (patterns.isEmpty()) { + return 0; + } + // Assume the device has less than 32GB of ram (and can use pointer compression), + // or the device is 32-bit. + final int numberOfBytesPerPointer = 4; + return (int) Math.ceil((numberOfBytesPerPointer * root.estimatedNumberOfPointersUsed()) / 1024.0); + } + + public int numberOfPatterns() { + return patterns.size(); + } + + public List getPatterns() { + return Collections.unmodifiableList(patterns); + } +} 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 new file mode 100644 index 0000000000..cf65db8a4c --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -0,0 +1,1222 @@ +package app.revanced.extension.shared; + +import android.annotation.SuppressLint; +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; +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.Toast; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.text.Bidi; +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") + 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 + + /** + * Injection point. + * + * @return The manifest 'Version' entry of the patches.jar used during patching. + */ + @SuppressWarnings("SameReturnValue") + public static String getPatchesReleaseVersion() { + return ""; // Value is replaced during patching. + } + + 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 20.13.41 + */ + public static String getAppVersionName() { + if (versionName == null) { + try { + versionName = getPackageInfo().versionName; + } catch (Exception ex) { + Logger.printException(() -> "Failed to get package info", ex); + versionName = "Unknown"; + } + } + + 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 setting The setting to check for hiding the view. + * @param view The view to hide. + */ + public static void hideViewBy0dpUnderCondition(BooleanSetting setting, View view) { + if (hideViewBy0dpUnderCondition(setting.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + setting); + } + } + + /** + * Hide a view by setting its layout height and width to 0dp. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) { + if (condition) { + hideViewBy0dp(view); + return true; + } + + return false; + } + + /** + * Hide a view by setting its layout params to 0x0 + * @param view The view to hide. + */ + public static void hideViewBy0dp(View view) { + 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 setting The setting to check for hiding the view. + * @param view The view to hide. + */ + public static void hideViewUnderCondition(BooleanSetting setting, View view) { + if (hideViewUnderCondition(setting.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + setting); + } + } + + /** + * Hide a view by setting its visibility as GONE. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static boolean hideViewUnderCondition(boolean condition, View view) { + if (condition) { + view.setVisibility(View.GONE); + return true; + } + + return false; + } + + public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting setting, View view) { + if (hideViewByRemovingFromParentUnderCondition(setting.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + setting); + } + } + + public static boolean hideViewByRemovingFromParentUnderCondition(boolean condition, View view) { + if (condition) { + ViewParent parent = view.getParent(); + if (parent instanceof ViewGroup parentGroup) { + parentGroup.removeView(view); + return true; + } + } + + return false; + } + + /** + * General purpose pool for network calls and other background tasks. + * All tasks run at max thread priority. + */ + private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor( + 3, // 3 threads always ready to go. + Integer.MAX_VALUE, + 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle. + TimeUnit.SECONDS, + new SynchronousQueue<>(), + r -> { // ThreadFactory + Thread t = new Thread(r); + t.setPriority(Thread.MAX_PRIORITY); // Run at max priority. + return t; + }); + + public static void runOnBackgroundThread(Runnable task) { + backgroundThreadPool.execute(task); + } + + public static Future submitOnBackgroundThread(Callable call) { + return backgroundThreadPool.submit(call); + } + + /** + * Simulates a delay by doing meaningless calculations. + * Used for debugging to verify UI timeout logic. + */ + @SuppressWarnings("UnusedReturnValue") + public static long doNothingForDuration(long amountOfTimeToWaste) { + final long timeCalculationStarted = System.currentTimeMillis(); + Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms"); + + long meaninglessValue = 0; + while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) { + // Could do a thread sleep, but that will trigger an exception if the thread is interrupted. + meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random())); + } + // Return the value, otherwise the compiler or VM might optimize and remove the meaningless time-wasting work, + // leaving an empty loop that hammers on the System.currentTimeMillis native call. + return meaninglessValue; + } + + public static boolean containsAny(String value, String... targets) { + return indexOfFirstFound(value, targets) >= 0; + } + + 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. + */ + @SuppressLint("DiscouragedApi") + public static int getResourceIdentifier(Context context, @Nullable ResourceType type, String resourceIdentifierName) { + return context.getResources().getIdentifier(resourceIdentifierName, + type == null ? null : type.value, context.getPackageName()); + } + + public static int 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. + * @see #getResourceIdentifierOrThrow(ResourceType, String) + */ + public static int getResourceIdentifier(@Nullable ResourceType type, String resourceIdentifierName) { + return getResourceIdentifier(getContext(), type, resourceIdentifierName); + } + + /** + * @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); + } + + public static String getResourceString(int id) throws Resources.NotFoundException { + return getContext().getResources().getString(id); + } + + public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getInteger(getResourceIdentifierOrThrow(ResourceType.INTEGER, resourceIdentifierName)); + } + + 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(getResourceIdentifierOrThrow(ResourceType.COLOR, resourceIdentifierName)); + } + + public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(ResourceType.DIMEN, resourceIdentifierName)); + } + + 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 { + boolean matches(T object); + } + + /** + * Includes sub children. + */ + public static R getChildViewByResourceName(View view, String str) { + var child = view.findViewById(Utils.getResourceIdentifierOrThrow(ResourceType.ID, str)); + //noinspection unchecked + return (R) child; + } + + /** + * @param searchRecursively If children ViewGroups should also be + * recursively searched using depth first search. + * @return The first child view that matches the filter. + */ + @Nullable + public static T getChildView(ViewGroup viewGroup, boolean searchRecursively, + MatchFilter filter) { + for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) { + View childAt = viewGroup.getChildAt(i); + + if (filter.matches(childAt)) { + //noinspection unchecked + return (T) childAt; + } + // Must do recursive after filter check, in case the filter is looking for a ViewGroup. + if (searchRecursively && childAt instanceof ViewGroup) { + T match = getChildView((ViewGroup) childAt, true, filter); + if (match != null) return match; + } + } + + return null; + } + + @Nullable + public static ViewParent getParentView(View view, int nthParent) { + ViewParent parent = view.getParent(); + + int currentDepth = 0; + while (++currentDepth < nthParent && parent != null) { + parent = parent.getParent(); + } + + if (currentDepth == nthParent) { + return parent; + } + + final int currentDepthLog = currentDepth; + Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent + + " and instead found at: " + currentDepthLog + " view: " + view); + return null; + } + + public static void restartApp(Context context) { + String packageName = context.getPackageName(); + 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 + mainIntent.setPackage(packageName); + context.startActivity(mainIntent); + System.exit(0); + } + + public static Context getContext() { + if (context == 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; + + 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(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; + } + + @Nullable + private static Boolean isRightToLeftTextLayout; + + /** + * @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 isRightToLeftLocale() { + if (isRightToLeftTextLayout == null) { + 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. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + 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)) { + return true; + } + index += Character.charCount(codePoint); + } + + return false; + } + + /** + * Ignore this class. It must be public to satisfy Android requirements. + */ + @SuppressWarnings("deprecation") + public static final class DialogFragmentWrapper extends DialogFragment { + + private Dialog dialog; + @Nullable + private DialogFragmentOnStartAction onStartAction; + + @Override + public void onSaveInstanceState(Bundle outState) { + // Do not call super method to prevent state saving. + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return dialog; + } + + @Override + public void onStart() { + try { + super.onStart(); + + if (onStartAction != null) { + onStartAction.onStart(dialog); + } + } catch (Exception ex) { + Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex); + } + } + } + + /** + * Interface for {@link #showDialog(Activity, Dialog, boolean, DialogFragmentOnStartAction)}. + */ + @FunctionalInterface + public interface DialogFragmentOnStartAction { + void onStart(Dialog dialog); + } + + public static void showDialog(Activity activity, Dialog dialog) { + showDialog(activity, dialog, true, null); + } + + /** + * 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 Dialog#show()} on the dialog. + */ + @SuppressWarnings("deprecation") + public static void showDialog(Activity activity, + Dialog dialog, + boolean isCancelable, + @Nullable DialogFragmentOnStartAction onStartAction) { + verifyOnMainThread(); + + DialogFragmentWrapper fragment = new DialogFragmentWrapper(); + fragment.dialog = dialog; + fragment.onStartAction = onStartAction; + fragment.setCancelable(isCancelable); + + fragment.show(activity.getFragmentManager(), null); + } + + /** + * Safe to call from any thread. + */ + public static void showToastShort(String messageToToast) { + showToast(messageToToast, Toast.LENGTH_SHORT); + } + + /** + * Safe to call from any thread. + */ + public static void showToastLong(String messageToToast) { + showToast(messageToToast, Toast.LENGTH_LONG); + } + + /** + * 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(() -> { + 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; + } + + /** + * Automatically logs any exceptions the runnable throws. + * + * @see #runOnMainThreadNowOrLater(Runnable) + */ + public static void runOnMainThread(Runnable runnable) { + runOnMainThreadDelayed(runnable, 0); + } + + /** + * Automatically logs any exceptions the runnable throws. + */ + public static void runOnMainThreadDelayed(Runnable runnable, long delayMillis) { + Runnable loggingRunnable = () -> { + try { + runnable.run(); + } catch (Exception ex) { + Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex); + } + }; + new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis); + } + + /** + * If called from the main thread, the code is run immediately. + * If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}. + */ + public static void runOnMainThreadNowOrLater(Runnable runnable) { + if (isCurrentlyOnMainThread()) { + runnable.run(); + } else { + runOnMainThread(runnable); + } + } + + /** + * @return if the calling thread is on the main thread. + */ + public static boolean isCurrentlyOnMainThread() { + return Looper.getMainLooper().isCurrentThread(); + } + + /** + * @throws IllegalStateException if the calling thread is _off_ the main thread. + */ + public static void verifyOnMainThread() throws IllegalStateException { + if (!isCurrentlyOnMainThread()) { + throw new IllegalStateException("Must call _on_ the main thread"); + } + } + + /** + * @throws IllegalStateException if the calling thread is _on_ the main thread. + */ + public static void verifyOffMainThread() throws IllegalStateException { + if (isCurrentlyOnMainThread()) { + throw new IllegalStateException("Must call _off_ the main thread"); + } + } + + public 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; + } + + /** + * 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) { + return NetworkType.NONE; + } + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + var networkInfo = cm.getActiveNetworkInfo(); + + if (networkInfo == null || !networkInfo.isConnected()) { + return NetworkType.NONE; + } + var type = networkInfo.getType(); + return (type == ConnectivityManager.TYPE_MOBILE) + || (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER; + } + + /** + * 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(@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 { + params.width = 0; + params.height = 0; + } + + 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)); + } + + /** + * {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles. + */ + private enum Sort { + /** + * Sort by the localized preference title. + */ + BY_TITLE("_sort_by_title"), + + /** + * Sort by the preference keys. + */ + BY_KEY("_sort_by_key"), + + /** + * Unspecified sorting. + */ + UNSORTED("_sort_by_unsorted"); + + final String keySuffix; + + Sort(String keySuffix) { + this.keySuffix = keySuffix; + } + + static Sort fromKey(@Nullable String key, Sort defaultSort) { + if (key != null) { + for (Sort sort : values()) { + if (key.endsWith(sort.keySuffix)) { + return sort; + } + } + } + return defaultSort; + } + } + + /** + * Removes punctuation and converts text to lowercase. Returns an empty string if input is null. + */ + public static String removePunctuationToLowercase(@Nullable CharSequence original) { + if (original == null) return ""; + return PUNCTUATION_PATTERN.matcher(original).replaceAll("") + .toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale()); + } + + /** + * 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.). + * + * @param group the {@link PreferenceGroup} to sort + */ + @SuppressWarnings("deprecation") + public static void sortPreferenceGroups(PreferenceGroup group) { + Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); + 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 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. + // Used to force a preference to the top or bottom of a group. + preferenceSort = Sort.fromKey(preference.getKey(), groupSort); + } + + final String sortValue; + switch (preferenceSort) { + case BY_TITLE: + sortValue = removePunctuationToLowercase(preference.getTitle()); + break; + case BY_KEY: + sortValue = preference.getKey(); + break; + case UNSORTED: + continue; // Keep original sorting. + default: + throw new IllegalStateException(); + } + + 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 (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) { + // Any arbitrary large number. + order -= 1000; + } + + pref.setOrder(order); + } + } + + /** + * Set all preferences to multiline titles if the device is not using an English variant. + * The English strings are heavily scrutinized and all titles fit on screen + * except 2 or 3 preference strings and those do not affect readability. + *

+ * Allowing multiline for those 2 or 3 English preferences looks weird and out of place, + * and visually it looks better to clip the text and keep all titles 1 line. + */ + @SuppressWarnings("deprecation") + public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + + String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage(); + if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) { + return; + } + + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference pref = group.getPreference(i); + pref.setSingleLineTitle(false); + + if (pref instanceof PreferenceGroup subGroup) { + setPreferenceTitlesToMultiLineIfNeeded(subGroup); + } + } + } + + /** + * Parse a color resource or hex code to an int representation of the color. + */ + @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 new file mode 100644 index 0000000000..bde66a043c --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/Check.java @@ -0,0 +1,215 @@ +package app.revanced.extension.shared.checks; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Intent; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.os.Build; +import android.text.Html; +import android.util.Pair; +import android.view.Gravity; +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; + + private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15; + private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10; + + private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app"); + + /** + * @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed. + */ + @Nullable + protected abstract Boolean check(); + + protected abstract String failureReason(); + + /** + * Specifies a sorting order for displaying the checks that failed. + * A lower value indicates to show first before other checks. + */ + public abstract int uiSortingValue(); + + /** + * For debugging and development only. + * Forces all checks to be performed and the check failed dialog to be shown. + * Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED} + * set to -1. + */ + static boolean debugAlwaysShowWarning() { + final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0; + if (alwaysShowWarning) { + Logger.printInfo(() -> "Debug forcing environment check warning to show"); + } + + return alwaysShowWarning; + } + + static boolean shouldRun() { + return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() + < NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING; + } + + static void disableForever() { + Logger.printInfo(() -> "Environment checks disabled forever"); + + BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE); + } + + static void issueWarning(Activity activity, Collection failedChecks) { + final var reasons = new StringBuilder(); + + reasons.append("

    "); + for (var check : failedChecks) { + // Add a non breaking space to fix bullet points spacing issue. + reasons.append("
  •  ").append(check.failureReason()); + } + reasons.append("
"); + + var message = Html.fromHtml( + str("revanced_check_environment_failed_message", reasons.toString()), + FROM_HTML_MODE_COMPACT + ); + + Utils.runOnMainThreadDelayed(() -> { + // 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); + }, + 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. + ); + + // Get the dialog and main layout. + Dialog dialog = dialogPair.first; + LinearLayout mainLayout = dialogPair.second; + + // 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(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) { + return; + } + hasRun = true; + + // 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); + + // 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 ignoreButton, Button openWebsiteButton) { + return new Runnable() { + private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON; + + @Override + public void run() { + Utils.verifyOnMainThread(); + + if (secondsRemaining > 0) { + if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) { + openWebsiteButton.setVisibility(View.VISIBLE); + openWebsiteButton.setEnabled(true); + } + secondsRemaining--; + Utils.runOnMainThreadDelayed(this, 1000); + } else { + 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 new file mode 100644 index 0000000000..e54ab27f74 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java @@ -0,0 +1,347 @@ +package app.revanced.extension.shared.checks; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.checks.Check.debugAlwaysShowWarning; +import static app.revanced.extension.shared.checks.PatchInfo.Build.*; + +/** + * This class is used to check if the app was patched by the user + * and not downloaded pre-patched, because pre-patched apps are difficult to trust. + *
+ * 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(); + + private enum InstallationType { + /** + * CLI patching, manual installation of a previously patched using adb, + * or root installation if stock app is first installed using adb. + */ + ADB((String) null), + ROOT_MOUNT_ON_APP_STORE("com.android.vending"), + MANAGER("app.revanced.manager.flutter", + "app.revanced.manager.flutter.debug", + "app.revanced.manager", + "app.revanced.manager.debug"); + + @Nullable + static InstallationType installTypeFromPackageName(@Nullable String packageName) { + for (InstallationType type : values()) { + for (String installPackageName : type.packageNames) { + if (Objects.equals(installPackageName, packageName)) { + return type; + } + } + } + + return null; + } + + /** + * Array elements can be null. + */ + final String[] packageNames; + + InstallationType(String... packageNames) { + this.packageNames = packageNames; + } + } + + /** + * Check if the app is installed by the manager, the app store, or through adb/CLI. + *
+ * Does not conclusively + * If the app is installed by the manager or the app store, it is likely, the app was patched using the manager, + * or installed manually via ADB (in the case of ReVanced CLI for example). + *
+ * If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched + * and installed by the browser or another unknown app. + */ + private static class CheckExpectedInstaller extends Check { + @Nullable + InstallationType installerFound; + + @NonNull + @Override + protected Boolean check() { + final var context = Utils.getContext(); + + final var installerPackageName = + context.getPackageManager().getInstallerPackageName(context.getPackageName()); + + Logger.printInfo(() -> "Installed by: " + installerPackageName); + + installerFound = InstallationType.installTypeFromPackageName(installerPackageName); + final boolean passed = (installerFound != null); + + Logger.printInfo(() -> passed + ? "Apk was not installed from an unknown source" + : "Apk was installed from an unknown source"); + + return passed; + } + + @Override + protected String failureReason() { + return str("revanced_check_environment_manager_not_expected_installer"); + } + + @Override + public int uiSortingValue() { + return -100; // Show first. + } + } + + /** + * Check if the build properties are the same as during the patch. + *
+ * If the build properties are the same as during the patch, it is likely, the app was patched on the same device. + *
+ * If the build properties are different, the app was likely downloaded pre-patched or patched on another device. + */ + private static class CheckWasPatchedOnSameDevice extends Check { + @SuppressLint("HardwareIds") + @Override + protected Boolean check() { + if (PATCH_BOARD.isEmpty()) { + // Did not patch with Manager, and cannot conclusively say where this was from. + Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device"); + return null; + } + + //noinspection deprecation + final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) & + buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) & + buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) & + buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) & + buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) & + buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) & + buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) & + buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) & + buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) & + buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) & + buildFieldEqualsHash("ID", Build.ID, PATCH_ID) & + buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) & + buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) & + buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) & + buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) & + buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) & + buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) & + buildFieldEqualsHash("USER", Build.USER, PATCH_USER); + + Logger.printInfo(() -> passed + ? "Device hardware signature matches current device" + : "Device hardware signature does not match current device"); + + return passed; + } + + @Override + protected String failureReason() { + return str("revanced_check_environment_not_same_patching_device"); + } + + @Override + public int uiSortingValue() { + return 0; // Show in the middle. + } + } + + /** + * Check if the app was installed within the last 30 minutes after being patched. + *
+ * If the app was installed within the last 30 minutes, it is likely, the app was patched by the user. + *
+ * If the app was installed much later than the patch time, it is likely the app was + * downloaded pre-patched or the user waited too long to install the app. + */ + private static class CheckIsNearPatchTime extends Check { + /** + * How soon after patching the app must be installed to pass. + */ + static final int INSTALL_AFTER_PATCHING_DURATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes. + + /** + * Milliseconds between the time the app was patched, and when it was installed/updated. + */ + long durationBetweenPatchingAndInstallation; + + @NonNull + @Override + protected Boolean check() { + try { + Context context = Utils.getContext(); + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); + + // Duration since initial install or last update, whichever is sooner. + durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME; + Logger.printInfo(() -> "App was installed/updated: " + + (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching")); + + if (durationBetweenPatchingAndInstallation < 0) { + // Patch time is in the future and clearly wrong. + return false; + } + + if (durationBetweenPatchingAndInstallation < INSTALL_AFTER_PATCHING_DURATION_THRESHOLD) { + return true; + } + } catch (PackageManager.NameNotFoundException ex) { + Logger.printException(() -> "Package name not found exception", ex); // Will never happen. + } + + // User installed more than 30 minutes after patching. + return false; + } + + @Override + protected String failureReason() { + if (durationBetweenPatchingAndInstallation < 0) { + // Could happen if the user has their device clock incorrectly set in the past, + // but assume that isn't the case and the apk was patched on a device with the wrong system time. + return str("revanced_check_environment_not_near_patch_time_invalid"); + } + + // If patched over 1 day ago, show how old this pre-patched apk is. + // Showing the age can help convey it's better to patch yourself and know it's the latest. + final long oneDay = 24 * 60 * 60 * 1000; + final long daysSincePatching = durationBetweenPatchingAndInstallation / oneDay; + if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings. + return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching); + } + + return str("revanced_check_environment_not_near_patch_time"); + } + + @Override + public int uiSortingValue() { + return 100; // Show last. + } + } + + /** + * Injection point. + */ + public static void check(Activity context) { + // If the warning was already issued twice, or if the check was successful in the past, + // do not run the checks again. + if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + Logger.printDebug(() -> "Environment checks are disabled"); + return; + } + + Utils.runOnBackgroundThread(() -> { + try { + Logger.printInfo(() -> "Running environment checks"); + List failedChecks = new ArrayList<>(); + + CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice(); + Boolean hardwareCheckPassed = sameHardware.check(); + if (hardwareCheckPassed != null) { + if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Patched on the same device using Manager, + // and no further checks are needed. + Check.disableForever(); + return; + } + + failedChecks.add(sameHardware); + } + + CheckExpectedInstaller installerCheck = new CheckExpectedInstaller(); + if (installerCheck.check() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // If the installer package is Manager but this code is reached, + // that means it must not be the right Manager otherwise the hardware hash + // signatures would be present and this check would not have run. + if (installerCheck.installerFound == InstallationType.MANAGER) { + failedChecks.add(installerCheck); + // Also could not have been patched on this device. + failedChecks.add(sameHardware); + } else if (failedChecks.isEmpty()) { + // ADB install of CLI build. Allow even if patched a long time ago. + Check.disableForever(); + return; + } + } else { + failedChecks.add(installerCheck); + } + + CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); + Boolean timeCheckPassed = nearPatchTime.check(); + if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Allow installing recently patched APKs, + // even if the installation source is not Manager or ADB. + Check.disableForever(); + return; + } else { + failedChecks.add(nearPatchTime); + } + + if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Show all failures for debugging layout. + failedChecks = Arrays.asList( + sameHardware, + nearPatchTime, + installerCheck + ); + } + + //noinspection ComparatorCombinators + Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue()); + + Check.issueWarning( + context, + failedChecks + ); + } catch (Exception ex) { + Logger.printException(() -> "check failure", ex); + } + }); + } + + private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) { + try { + final var sha1 = MessageDigest.getInstance("SHA-1") + .digest(buildFieldValue.getBytes(StandardCharsets.UTF_8)); + + // Must be careful to use same base64 encoding Kotlin uses. + String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1); + final boolean equals = runtimeHash.equals(hash); + if (!equals) { + Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue + + "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'"); + } + + return equals; + } catch (NoSuchAlgorithmException ex) { + Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen. + + return false; + } + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java new file mode 100644 index 0000000000..cceb34f779 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java @@ -0,0 +1,32 @@ +package app.revanced.extension.shared.checks; + +/** + * Fields are set by the patch. Do not modify. + * Fields are not final, because the compiler is inlining them. + * + * @noinspection CanBeFinal + */ +final class PatchInfo { + static long PATCH_TIME = 0L; + + final static class Build { + static String PATCH_BOARD = ""; + static String PATCH_BOOTLOADER = ""; + static String PATCH_BRAND = ""; + static String PATCH_CPU_ABI = ""; + static String PATCH_CPU_ABI2 = ""; + static String PATCH_DEVICE = ""; + static String PATCH_DISPLAY = ""; + static String PATCH_FINGERPRINT = ""; + static String PATCH_HARDWARE = ""; + static String PATCH_HOST = ""; + static String PATCH_ID = ""; + static String PATCH_MANUFACTURER = ""; + static String PATCH_MODEL = ""; + static String PATCH_PRODUCT = ""; + static String PATCH_RADIO = ""; + static String PATCH_TAGS = ""; + static String PATCH_TYPE = ""; + static String PATCH_USER = ""; + } +} 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/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java new file mode 100644 index 0000000000..a8a6bf504e --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java @@ -0,0 +1,208 @@ +package app.revanced.extension.shared.fixes.slink; + + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.NonNull; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.Objects; + +import static app.revanced.extension.shared.Utils.getContext; + + +/** + * Base class to implement /s/ link resolution in 3rd party Reddit apps. + *
+ *
+ * Usage: + *
+ *
+ * An implementation of this class must have two static methods that are called by the app: + *
    + *
  • public static boolean patchResolveSLink(String link)
  • + *
  • public static void patchSetAccessToken(String accessToken)
  • + *
+ * The static methods must call the instance methods of the base class. + *
+ * The singleton pattern can be used to access the instance of the class: + *
+ * {@code
+ * {
+ *     INSTANCE = new FixSLinksPatch();
+ * }
+ * }
+ * 
+ * Set the app's web view activity class as a fallback to open /s/ links if the resolution fails: + *
+ * {@code
+ * private FixSLinksPatch() {
+ *     webViewActivityClass = WebViewActivity.class;
+ * }
+ * }
+ * 
+ * Hook the app's navigation handler to call this method before doing any of its own resolution: + *
+ * {@code
+ * public static boolean patchResolveSLink(Context context, String link) {
+ *     return INSTANCE.resolveSLink(context, link);
+ * }
+ * }
+ * 
+ * If this method returns true, the app should early return and not do any of its own resolution. + *
+ *
+ * Hook the app's access token so that this class can use it to resolve /s/ links: + *
+ * {@code
+ * public static void patchSetAccessToken(String accessToken) {
+ *     INSTANCE.setAccessToken(access_token);
+ * }
+ * }
+ * 
+ */ +public abstract class BaseFixSLinksPatch { + /** + * The class of the activity used to open links in a web view if resolving them fails. + */ + protected Class webViewActivityClass; + + /** + * The access token used to resolve the /s/ link. + */ + protected String accessToken; + + /** + * The URL that was trying to be resolved before the access token was set. + * If this is not null, the URL will be resolved right after the access token is set. + */ + protected String pendingUrl; + + /** + * The singleton instance of the class. + */ + protected static BaseFixSLinksPatch INSTANCE; + + public boolean resolveSLink(String link) { + switch (resolveLink(link)) { + case ACCESS_TOKEN_START: { + pendingUrl = link; + return true; + } + case DO_NOTHING: + return true; + default: + return false; + } + } + + private ResolveResult resolveLink(String link) { + Context context = getContext(); + if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) { + // A link ends with #bypass if it failed to resolve below. + // resolveLink is called with the same link again but this time with #bypass + // so that the link is opened in the app browser instead of trying to resolve it again. + if (link.endsWith("#bypass")) { + openInAppBrowser(context, link); + + return ResolveResult.DO_NOTHING; + } + + Logger.printDebug(() -> "Resolving " + link); + + if (accessToken == null) { + // This is not optimal. + // However, an accessToken is necessary to make an authenticated request to Reddit. + // in case Reddit has banned the IP - e.g. VPN. + Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + context.startActivity(startIntent); + + return ResolveResult.ACCESS_TOKEN_START; + } + + + Utils.runOnBackgroundThread(() -> { + String bypassLink = link + "#bypass"; + + String finalLocation = bypassLink; + try { + HttpURLConnection connection = getHttpURLConnection(link, accessToken); + connection.connect(); + String location = connection.getHeaderField("location"); + connection.disconnect(); + + Objects.requireNonNull(location, "Location is null"); + + finalLocation = location; + Logger.printDebug(() -> "Resolved " + link + " to " + location); + } catch (SocketTimeoutException e) { + Logger.printException(() -> "Timeout when trying to resolve " + link, e); + finalLocation = bypassLink; + } catch (Exception e) { + Logger.printException(() -> "Failed to resolve " + link, e); + finalLocation = bypassLink; + } finally { + Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation)); + startIntent.setPackage(context.getPackageName()); + startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(startIntent); + } + }); + + return ResolveResult.DO_NOTHING; + } + + return ResolveResult.CONTINUE; + } + + public void setAccessToken(String accessToken) { + Logger.printDebug(() -> "Setting access token"); + + this.accessToken = accessToken; + + // In case a link was trying to be resolved before access token was set. + // The link is resolved now, after the access token is set. + if (pendingUrl != null) { + String link = pendingUrl; + pendingUrl = null; + + Logger.printDebug(() -> "Opening pending URL"); + + resolveLink(link); + } + } + + private void openInAppBrowser(Context context, String link) { + Intent intent = new Intent(context, webViewActivityClass); + intent.putExtra("url", link); + context.startActivity(intent); + } + + @NonNull + private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException { + URL url = new URL(link); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setRequestMethod("HEAD"); + connection.setConnectTimeout(2000); + connection.setReadTimeout(2000); + + if (accessToken != null) { + Logger.printDebug(() -> "Setting access token to make /s/ request"); + + connection.setRequestProperty("Authorization", "Bearer " + accessToken); + } else { + Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null"); + } + + return connection; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java new file mode 100644 index 0000000000..8026c20585 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java @@ -0,0 +1,10 @@ +package app.revanced.extension.shared.fixes.slink; + +public enum ResolveResult { + // Let app handle rest of stuff + CONTINUE, + // Start app, to make it cache its access_token + ACCESS_TOKEN_START, + // Don't do anything - we started resolving + DO_NOTHING +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java new file mode 100644 index 0000000000..d12eabc0bd --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/CheckWatchHistoryDomainNameResolutionPatch.java @@ -0,0 +1,94 @@ +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.shared.settings.BaseSettings; +import app.revanced.extension.shared.ui.CustomDialog; + +@SuppressWarnings("unused") +public class CheckWatchHistoryDomainNameResolutionPatch { + + private static final String HISTORY_TRACKING_ENDPOINT = "s.youtube.com"; + + private static final String SINKHOLE_IPV4 = "0.0.0.0"; + private static final String SINKHOLE_IPV6 = "::"; + + private static boolean domainResolvesToValidIP(String host) { + try { + InetAddress address = InetAddress.getByName(host); + String hostAddress = address.getHostAddress(); + + if (address.isLoopbackAddress()) { + Logger.printDebug(() -> host + " resolves to localhost"); + } else if (SINKHOLE_IPV4.equals(hostAddress) || SINKHOLE_IPV6.equals(hostAddress)) { + Logger.printDebug(() -> host + " resolves to sinkhole ip"); + } else { + return true; // Domain is not blocked. + } + } catch (UnknownHostException e) { + Logger.printDebug(() -> host + " failed to resolve"); + } + + return false; + } + + /** + * Injection point. + * + * Checks if YouTube watch history endpoint cannot be reached. + */ + public static void checkDnsResolver(Activity context) { + if (!Utils.isNetworkConnected() || !BaseSettings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return; + + Utils.runOnBackgroundThread(() -> { + try { + // If the user has a flaky DNS server, or they just lost internet connectivity + // and the isNetworkConnected() check has not detected it yet (it can take a few + // seconds after losing connection), then the history tracking endpoint will + // show a resolving error but it's actually an internet connection problem. + // + // 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. + 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(() -> { + 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, 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/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java new file mode 100644 index 0000000000..beb623a799 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/CustomFilter.java @@ -0,0 +1,203 @@ +package app.revanced.extension.shared.patches.litho; + +import static app.revanced.extension.shared.StringRef.str; + +import androidx.annotation.NonNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +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.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") +public final class CustomFilter extends Filter { + + private static void showInvalidSyntaxToast(String expression) { + Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression)); + } + + private static class CustomFilterGroup extends StringFilterGroup { + /** + * Optional character for the path that indicates the custom filter path must match the start. + * Must be the first character of the expression. + */ + public static final String SYNTAX_STARTS_WITH = "^"; + + /** + * 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 = "$"; + + /** + * @return the parsed objects + */ + @NonNull + @SuppressWarnings("ConstantConditions") + static Collection parseCustomFilterGroups() { + String rawCustomFilterText = YouTubeAndMusicSettings.CUSTOM_FILTER_STRINGS.get(); + if (rawCustomFilterText.isBlank()) { + return Collections.emptyList(); + } + + // 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. + // 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; + + Matcher matcher = pattern.matcher(expression); + if (!matcher.find()) { + showInvalidSyntaxToast(expression); + continue; + } + + final String mapKey = matcher.group(1); + final boolean pathStartsWith = !matcher.group(2).isEmpty(); + final String path = matcher.group(3); + final String accessibility = matcher.group(4); // null if not present + final String buffer = matcher.group(5); // null if not present + + if (path.isBlank() + || (accessibility != null && accessibility.isEmpty()) + || (buffer != null && buffer.isEmpty())) { + showInvalidSyntaxToast(expression); + continue; + } + + // Use one group object for all expressions with the same path. + // This ensures the buffer is searched exactly once + // when multiple paths are used with different buffer strings. + CustomFilterGroup group = result.get(mapKey); + if (group == null) { + group = new CustomFilterGroup(pathStartsWith, path); + result.put(mapKey, group); + } + + if (accessibility != null) { + group.addAccessibilityString(accessibility); + } + + if (buffer != null) { + group.addBufferString(buffer); + } + } + + return result.values(); + } + + final boolean startsWith; + StringTrieSearch accessibilitySearch; + ByteTrieSearch bufferSearch; + + CustomFilterGroup(boolean startsWith, String path) { + super(YouTubeAndMusicSettings.CUSTOM_FILTER, path); + this.startsWith = startsWith; + } + + void addAccessibilityString(String accessibilityString) { + if (accessibilitySearch == null) { + accessibilitySearch = new StringTrieSearch(); + } + accessibilitySearch.addPattern(accessibilityString); + } + + void addBufferString(String bufferString) { + if (bufferSearch == null) { + bufferSearch = new ByteTrieSearch(); + } + bufferSearch.addPattern(bufferString.getBytes()); + } + + @NonNull + @Override + 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]); + + if (bufferSearch != null) { + String delimitingCharacter = "❙"; + builder.append(", bufferStrings="); + builder.append(delimitingCharacter); + for (byte[] bufferString : bufferSearch.getPatterns()) { + builder.append(new String(bufferString)); + builder.append(delimitingCharacter); + } + } + builder.append("}"); + return builder.toString(); + } + } + + public CustomFilter() { + Collection groups = CustomFilterGroup.parseCustomFilterGroups(); + + if (!groups.isEmpty()) { + CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]); + Logger.printDebug(()-> "Using Custom filters: " + Arrays.toString(groupsArray)); + addPathCallbacks(groupsArray); + } + } + + @Override + 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; + } + + // Check accessibility string if specified. + if (custom.accessibilitySearch != null && !custom.accessibilitySearch.matches(accessibility)) { + return false; + } + + // Check buffer if specified. + if (custom.bufferSearch != null && !custom.bufferSearch.matches(buffer)) { + return false; + } + + return true; // All custom filter conditions passed. + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java new file mode 100644 index 0000000000..b34ca9bdd7 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/patches/litho/Filter.java @@ -0,0 +1,80 @@ +package app.revanced.extension.shared.patches.litho; + +import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Filters litho based components. + * + * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} + * and {@link #addPathCallbacks(StringFilterGroup...)}. + * + * 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, String, byte[], StringFilterGroup, FilterContentType, int)} + * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) + * or a {@link FilterGroupList.ByteArrayFilterGroupList} (if searching for more than 1 pattern). + * + * All callbacks must be registered before the constructor completes. + */ +public abstract class Filter { + + public enum FilterContentType { + IDENTIFIER, + PATH, + ACCESSIBILITY, + PROTOBUFFER + } + + /** + * Identifier callbacks. Do not add to this instance, + * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}. + */ + public final List identifierCallbacks = new ArrayList<>(); + /** + * Path callbacks. Do not add to this instance, + * and instead use {@link #addPathCallbacks(StringFilterGroup...)}. + */ + public final List pathCallbacks = new ArrayList<>(); + + /** + * 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) { + identifierCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * 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) { + pathCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Called after an enabled filter has been matched. + * Default implementation is to always filter the matched component and log the action. + * Subclasses can perform additional or different checks if needed. + *

+ * 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. + */ + public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + 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 new file mode 100644 index 0000000000..2e5c457f7b --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Requester.java @@ -0,0 +1,145 @@ +package app.revanced.extension.shared.requests; + +import app.revanced.extension.shared.Utils; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public class Requester { + private Requester() { + } + + public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException { + return getConnectionFromCompiledRoute(apiUrl, route.compile(params)); + } + + public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException { + String url = apiUrl + route.getCompiledRoute(); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + // 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") + + "; ReVanced/" + Utils.getAppVersionName() + + " (" + Utils.getPatchesReleaseVersion() + ")"; + connection.setRequestProperty("User-Agent", agentString); + + return connection; + } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + */ + private static String parseInputStreamAndClose(InputStream inputStream) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + StringBuilder jsonBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + jsonBuilder.append(line); + jsonBuilder.append('\n'); + } + return jsonBuilder.toString(); + } + } + + /** + * Parse the {@link HttpURLConnection} response as a String. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}. + */ + public static String parseString(HttpURLConnection connection) throws IOException { + return parseInputStreamAndClose(connection.getInputStream()); + } + + /** + * Parse the {@link HttpURLConnection} response as a String, and disconnect. + * + * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseString(HttpURLConnection) + */ + public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseString(connection); + connection.disconnect(); + return result; + } + + /** + * Parse the {@link HttpURLConnection} error stream as a String. + * If the server sent no error response data, this returns an empty string. + */ + public static String parseErrorString(HttpURLConnection connection) throws IOException { + InputStream errorStream = connection.getErrorStream(); + if (errorStream == null) { + return ""; + } + return parseInputStreamAndClose(errorStream); + } + + /** + * Parse the {@link HttpURLConnection} error stream as a String, and disconnect. + * If the server sent no error response data, this returns an empty string. + * + * Should only be used if other requests to the server are unlikely in the near future. + * + * @see #parseErrorString(HttpURLConnection) + */ + public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseErrorString(connection); + connection.disconnect(); + return result; + } + + /** + * Parse the {@link HttpURLConnection} response into a JSONObject. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}. + */ + public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException { + return new JSONObject(parseString(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * + * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseJSONObject(HttpURLConnection) + */ + public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONObject object = parseJSONObject(connection); + connection.disconnect(); + return object; + } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}. + */ + public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException { + return new JSONArray(parseString(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * + * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseJSONArray(HttpURLConnection) + */ + public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONArray array = parseJSONArray(connection); + connection.disconnect(); + return array; + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..74428224a7 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/requests/Route.java @@ -0,0 +1,66 @@ +package app.revanced.extension.shared.requests; + +public class Route { + private final String route; + private final Method method; + private final int paramCount; + + public Route(Method method, String route) { + this.method = method; + this.route = route; + this.paramCount = countMatches(route, '{'); + + if (paramCount != countMatches(route, '}')) + throw new IllegalArgumentException("Not enough parameters"); + } + + public Method getMethod() { + return method; + } + + public CompiledRoute compile(String... params) { + if (params.length != paramCount) + throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " + + "Expected: " + paramCount + ", provided: " + params.length); + + StringBuilder compiledRoute = new StringBuilder(route); + for (int i = 0; i < paramCount; i++) { + int paramStart = compiledRoute.indexOf("{"); + int paramEnd = compiledRoute.indexOf("}"); + compiledRoute.replace(paramStart, paramEnd + 1, params[i]); + } + return new CompiledRoute(this, compiledRoute.toString()); + } + + public static class CompiledRoute { + private final Route baseRoute; + private final String compiledRoute; + + private CompiledRoute(Route baseRoute, String compiledRoute) { + this.baseRoute = baseRoute; + this.compiledRoute = compiledRoute; + } + + public String getCompiledRoute() { + return compiledRoute; + } + + public Method getMethod() { + return baseRoute.method; + } + } + + private int countMatches(CharSequence seq, char c) { + int count = 0; + for (int i = 0, length = seq.length(); i < length; i++) { + if (seq.charAt(i) == c) + count++; + } + return count; + } + + public enum Method { + GET, + POST + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..5fc4418366 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -0,0 +1,70 @@ +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. + */ +public class BaseSettings { + public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE); + public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG)); + public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message"); + + public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false); + + 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 new file mode 100644 index 0000000000..c67ebabf96 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java @@ -0,0 +1,81 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class BooleanSetting extends Setting { + public BooleanSetting(String key, Boolean defaultValue) { + super(key, defaultValue); + } + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + public BooleanSetting(String key, Boolean defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + /** + * Sets, but does _not_ persistently save the value. + * This method is only to be used by the Settings preference code. + * + * This intentionally is a static method to deter + * accidental usage when {@link #save(Boolean)} was intended. + */ + public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) { + setting.value = Objects.requireNonNull(newValue); + + if (setting.isSetToDefault()) { + setting.removeFromPreferences(); + } + } + + @Override + protected void load() { + value = preferences.getBoolean(key, defaultValue); + } + + @Override + protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getBoolean(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Boolean.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void saveToPreferences() { + preferences.saveBoolean(key, value); + } + + @NonNull + @Override + public Boolean get() { + return value; + } +} 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 new file mode 100644 index 0000000000..2c2cb6a3a8 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java @@ -0,0 +1,122 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Locale; +import java.util.Objects; + +import app.revanced.extension.shared.Logger; + +/** + * If an Enum value is removed or changed, any saved or imported data using the + * non-existent value will be reverted to the default value + * (the event is logged, but no user error is displayed). + * + * All saved JSON text is converted to lowercase to keep the output less obnoxious. + */ +@SuppressWarnings("unused") +public class EnumSetting> extends Setting { + public EnumSetting(String key, T defaultValue) { + super(key, defaultValue); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + public EnumSetting(String key, T defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + public EnumSetting(String key, T defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getEnum(key, defaultValue); + } + + @Override + protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException { + String enumName = json.getString(importExportKey); + try { + return getEnumFromString(enumName); + } catch (IllegalArgumentException ex) { + // Info level to allow removing enum values in the future without showing any user errors. + Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex); + return defaultValue; + } + } + + @Override + protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException { + // Use lowercase to keep the output less ugly. + json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH)); + } + + /** + * @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 + return (T) value; + } + } + + throw new IllegalArgumentException("Unknown enum value: " + enumName); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = getEnumFromString(Objects.requireNonNull(newValue)); + } + + @Override + public void saveToPreferences() { + preferences.saveEnumAsString(key, value); + } + + @NonNull + @Override + public T get() { + return value; + } + + /** + * Availability based on if this setting is currently set to any of the provided types. + */ + @SafeVarargs + public final Setting.Availability availability(T... types) { + Objects.requireNonNull(types); + + return () -> { + T currentEnumType = get(); + for (T enumType : types) { + if (currentEnumType == enumType) return true; + } + return false; + }; + } +} 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 new file mode 100644 index 0000000000..59846e037f --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java @@ -0,0 +1,67 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class FloatSetting extends Setting { + + public FloatSetting(String key, Float defaultValue) { + super(key, defaultValue); + } + public FloatSetting(String key, Float defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + public FloatSetting(String key, Float defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + public FloatSetting(String key, Float defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getFloatString(key, defaultValue); + } + + @Override + protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return (float) json.getDouble(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Float.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void saveToPreferences() { + preferences.saveFloatString(key, value); + } + + @NonNull + @Override + public Float get() { + return value; + } +} 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 new file mode 100644 index 0000000000..ccf128dfdd --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java @@ -0,0 +1,67 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class IntegerSetting extends Setting { + + public IntegerSetting(String key, Integer defaultValue) { + super(key, defaultValue); + } + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + public IntegerSetting(String key, Integer defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getIntegerString(key, defaultValue); + } + + @Override + protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getInt(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Integer.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void saveToPreferences() { + preferences.saveIntegerString(key, value); + } + + @NonNull + @Override + public Integer get() { + return value; + } +} 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 new file mode 100644 index 0000000000..ea3adcebac --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/LongSetting.java @@ -0,0 +1,67 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class LongSetting extends Setting { + + public LongSetting(String key, Long defaultValue) { + super(key, defaultValue); + } + public LongSetting(String key, Long defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + public LongSetting(String key, Long defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + public LongSetting(String key, Long defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getLongString(key, defaultValue); + } + + @Override + protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getLong(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Long.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void saveToPreferences() { + preferences.saveLongString(key, value); + } + + @NonNull + @Override + public Long get() { + return value; + } +} 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 new file mode 100644 index 0000000000..53a980e3c2 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -0,0 +1,504 @@ +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; + +public abstract class Setting { + + /** + * Indicates if a {@link Setting} is available to edit and use. + * Typically this is dependent upon other BooleanSetting(s) set to 'true', + * but this can be used to call into extension code and check other conditions. + */ + public interface Availability { + boolean isAvailable(); + + /** + * @return parent settings (dependencies) of this availability. + */ + default List> getParentSettings() { + return Collections.emptyList(); + } + } + + /** + * Availability based on a single parent setting being enabled. + */ + 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. + */ + 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)); + } + }; + } + + /** + * Availability based on any parent being enabled. + */ + 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)); + } + }; + } + + /** + * Callback for importing/exporting settings. + */ + public interface ImportExportCallback { + /** + * Called after all settings have been imported. + */ + void settingsImported(@Nullable Context context); + + /** + * Called after all settings have been exported. + */ + void settingsExported(@Nullable Context context); + } + + private static final List importExportCallbacks = new ArrayList<>(); + + /** + * Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}. + */ + public static void addImportExportCallback(ImportExportCallback callback) { + importExportCallbacks.add(Objects.requireNonNull(callback)); + } + + /** + * All settings that were instantiated. + * When a new setting is created, it is automatically added to this list. + */ + private static final List> SETTINGS = new ArrayList<>(); + + /** + * Map of setting path to setting object. + */ + private static final Map> PATH_TO_SETTINGS = new HashMap<>(); + + /** + * Preference all instances are saved to. + */ + public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs"); + + @Nullable + public static Setting getSettingFromPath(String str) { + return PATH_TO_SETTINGS.get(str); + } + + /** + * @return All settings that have been created. + */ + public static List> allLoadedSettings() { + return Collections.unmodifiableList(SETTINGS); + } + + /** + * @return All settings that have been created, sorted by keys. + */ + private static List> allLoadedSettingsSorted() { + //noinspection ComparatorCombinators + Collections.sort(SETTINGS, (Setting o1, Setting o2) -> o1.key.compareTo(o2.key)); + return allLoadedSettings(); + } + + /** + * The key used to store the value in the shared preferences. + */ + public final String key; + + /** + * The default value of the setting. + */ + public final T defaultValue; + + /** + * If the app should be rebooted, if this setting is changed + */ + public final boolean rebootApp; + + /** + * If this setting should be included when importing/exporting settings. + */ + public final boolean includeWithImportExport; + + /** + * If this setting is available to edit and use. + * Not to be confused with it's status returned from {@link #get()}. + */ + @Nullable + private final Availability availability; + + /** + * Confirmation message to display, if the user tries to change the setting from the default value. + */ + @Nullable + public final StringRef userDialogMessage; + + // Must be volatile, as some settings are read/write from different threads. + // Of note, the object value is persistently stored using SharedPreferences (which is thread safe). + /** + * The value of the setting. + */ + protected volatile T value; + + public Setting(String key, T defaultValue) { + this(key, defaultValue, false, true, null, null); + } + public Setting(String key, T defaultValue, boolean rebootApp) { + this(key, defaultValue, rebootApp, true, null, null); + } + public Setting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) { + this(key, defaultValue, rebootApp, includeWithImportExport, null, null); + } + public Setting(String key, T defaultValue, String userDialogMessage) { + this(key, defaultValue, false, true, userDialogMessage, null); + } + public Setting(String key, T defaultValue, Availability availability) { + this(key, defaultValue, false, true, null, availability); + } + public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) { + this(key, defaultValue, rebootApp, true, userDialogMessage, null); + } + public Setting(String key, T defaultValue, boolean rebootApp, Availability availability) { + this(key, defaultValue, rebootApp, true, null, availability); + } + public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + this(key, defaultValue, rebootApp, true, userDialogMessage, availability); + } + + /** + * A setting backed by a shared preference. + * + * @param key The key used to store the value in the shared preferences. + * @param defaultValue The default value of the setting. + * @param rebootApp If the app should be rebooted, if this setting is changed. + * @param includeWithImportExport If this setting should be shown in the import/export dialog. + * @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value. + * @param availability Condition that must be true, for this setting to be available to configure. + */ + public Setting(String key, + T defaultValue, + boolean rebootApp, + boolean includeWithImportExport, + @Nullable String userDialogMessage, + @Nullable Availability availability + ) { + this.key = Objects.requireNonNull(key); + this.value = this.defaultValue = Objects.requireNonNull(defaultValue); + this.rebootApp = rebootApp; + this.includeWithImportExport = includeWithImportExport; + this.userDialogMessage = (userDialogMessage == null) ? null : new StringRef(userDialogMessage); + this.availability = availability; + + SETTINGS.add(this); + if (PATH_TO_SETTINGS.put(key, this) != null) { + Logger.printException(() -> this.getClass().getSimpleName() + + " error: Duplicate Setting key found: " + key); + } + + load(); + } + + /** + * 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(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(String newValue); + + /** + * Load and set the value of {@link #value}. + */ + protected abstract void load(); + + /** + * Persistently saves the value. + */ + 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 T resetToDefault() { + save(defaultValue); + return defaultValue; + } + + /** + * @return if this setting can be configured and used. + */ + public boolean isAvailable() { + return availability == null || availability.isAvailable(); + } + + /** + * 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); + } + + @NonNull + @Override + public String toString() { + return key + "=" + get(); + } + + // region Import / export + + /** + * If a setting path has this prefix, then remove it before importing/exporting. + */ + private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_"; + + /** + * The path, minus any 'revanced' prefix to keep json concise. + */ + private String getImportExportKey() { + if (key.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) { + return key.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length()); + } + return key; + } + + /** + * @param importExportKey The JSON key. The JSONObject parameter will contain data for this key. + * @return the value stored using the import/export key. Do not set any values in this method. + */ + protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException; + + /** + * Saves this instance to JSON. + *

+ * To keep the JSON simple and readable, + * subclasses should not write out any embedded types (such as JSON Array or Dictionaries). + *

+ * If this instance is not a type supported natively by JSON (ie: it's not a String/Integer/Float/Long), + * then subclasses can override this method and write out a String value representing the value. + */ + protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException { + json.put(importExportKey, value); + } + + public static String exportToJson(@Nullable Context alertDialogContext) { + try { + JSONObject json = new JSONObject(); + for (Setting setting : allLoadedSettingsSorted()) { + String importExportKey = setting.getImportExportKey(); + if (json.has(importExportKey)) { + throw new IllegalArgumentException("duplicate key found: " + importExportKey); + } + + final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI. + //noinspection ConstantValue + if (setting.includeWithImportExport && (!setting.isSetToDefault() || exportDefaultValues)) { + setting.writeToJSON(json, importExportKey); + } + } + + for (ImportExportCallback callback : importExportCallbacks) { + callback.settingsExported(alertDialogContext); + } + + if (json.length() == 0) { + return ""; + } + + String export = json.toString(0); + + // Remove the outer JSON braces to make the output more compact, + // and leave less chance of the user forgetting to copy it + return export.substring(2, export.length() - 2); + } catch (JSONException e) { + Logger.printException(() -> "Export failure", e); // should never happen + return ""; + } + } + + /** + * @return if any settings that require a reboot were changed. + */ + public static boolean importFromJSON(Context alertDialogContext, String settingsJsonString) { + try { + if (!settingsJsonString.matches("[\\s\\S]*\\{")) { + settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces + } + JSONObject json = new JSONObject(settingsJsonString); + + boolean rebootSettingChanged = false; + int numberOfSettingsImported = 0; + //noinspection rawtypes + for (Setting setting : SETTINGS) { + String key = setting.getImportExportKey(); + if (json.has(key)) { + Object value = setting.readFromJSON(json, key); + if (!setting.get().equals(value)) { + rebootSettingChanged |= setting.rebootApp; + //noinspection unchecked + setting.save(value); + } + numberOfSettingsImported++; + } else if (setting.includeWithImportExport && !setting.isSetToDefault()) { + Logger.printDebug(() -> "Resetting to default: " + setting); + rebootSettingChanged |= setting.rebootApp; + setting.resetToDefault(); + } + } + + for (ImportExportCallback callback : importExportCallbacks) { + callback.settingsImported(alertDialogContext); + } + + // 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) { + Utils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage())); + Logger.printInfo(() -> "", ex); + } catch (Exception ex) { + Logger.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen + } + return false; + } + + // End import / export + +} 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 new file mode 100644 index 0000000000..adb9beaa18 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/StringSetting.java @@ -0,0 +1,67 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class StringSetting extends Setting { + + public StringSetting(String key, String defaultValue) { + super(key, defaultValue); + } + public StringSetting(String key, String defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + public StringSetting(String key, String defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + public StringSetting(String key, String defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getString(key, defaultValue); + } + + @Override + protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getString(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Objects.requireNonNull(newValue); + } + + @Override + public void saveToPreferences() { + preferences.saveString(key, value); + } + + @NonNull + @Override + public String get() { + return value; + } +} 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 new file mode 100644 index 0000000000..a515471a00 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -0,0 +1,360 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +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. + */ + public static boolean settingImportInProgress; + + /** + * Prevents recursive calls during preference <-> UI syncing from showing extra dialogs. + */ + private static boolean updatingPreference; + + /** + * Used to prevent showing reboot dialog, if user cancels a setting user dialog. + */ + 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 { + if (updatingPreference) { + Logger.printDebug(() -> "Ignoring preference change as sync is in progress"); + return; + } + + Setting setting = Setting.getSettingFromPath(Objects.requireNonNull(str)); + if (setting == null) { + return; + } + Preference pref = findPreference(str); + if (pref == null) { + return; + } + Logger.printDebug(() -> "Preference changed: " + setting.key); + + 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. + *

+ * To ensure all {@link Setting} instances are correctly synced to the UI, + * it is important that subclasses make a call or otherwise reference their Settings class bundle + * so all app specific {@link Setting} instances are loaded before this method returns. + */ + protected void initialize() { + 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); + + PreferenceScreen screen = getPreferenceScreen(); + Utils.sortPreferenceGroups(screen); + Utils.setPreferenceTitlesToMultiLineIfNeeded(screen); + } + + 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; + + 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); + } + }, + () -> { + // 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); + } + + /** + * Updates Preferences availability only using the status of {@link Setting}. + */ + protected void updateUIAvailability() { + 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 PreferenceGroup group, + boolean syncSettingValue, + boolean applySettingToPreference) { + // 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 = 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); + + if (setting != null) { + updatePreference(pref, setting, syncSettingValue, applySettingToPreference); + } else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference + || pref instanceof EditTextPreference || pref instanceof ListPreference)) { + // Probably a typo in the patches preference declaration. + Logger.printException(() -> "Preference key has no setting: " + key); + } + } + } + } + + /** + * Handles syncing a UI Preference with the {@link Setting} that backs it. + * If needed, subclasses can override this to handle additional UI Preference types. + * + * @param applySettingToPreference If true, then apply {@link Setting} -> Preference. + * If false, then apply {@link Setting} <- Preference. + */ + protected void syncSettingWithPreference(@NonNull Preference pref, + @NonNull Setting setting, + boolean applySettingToPreference) { + if (pref instanceof SwitchPreference switchPref) { + BooleanSetting boolSetting = (BooleanSetting) setting; + if (applySettingToPreference) { + switchPref.setChecked(boolSetting.get()); + } else { + BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked()); + } + } else if (pref instanceof EditTextPreference editPreference) { + if (applySettingToPreference) { + editPreference.setText(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, editPreference.getText()); + } + } else if (pref instanceof ListPreference listPref) { + if (applySettingToPreference) { + listPref.setValue(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, listPref.getValue()); + } + updateListPreferenceSummary(listPref, setting); + } 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); + } + } + + /** + * Updates a UI Preference with the {@link Setting} that backs it. + * + * @param syncSetting If the UI should be synced {@link Setting} <-> Preference + * @param applySettingToPreference If true, then apply {@link Setting} -> Preference. + * If false, then apply {@link Setting} <- Preference. + */ + private void updatePreference(@NonNull Preference pref, @NonNull Setting setting, + boolean syncSetting, boolean applySettingToPreference) { + if (!syncSetting && applySettingToPreference) { + throw new IllegalArgumentException(); + } + + if (syncSetting) { + syncSettingWithPreference(pref, setting, applySettingToPreference); + } + + updatePreferenceAvailability(pref, setting); + } + + protected void updatePreferenceAvailability(@NonNull Preference pref, @NonNull Setting setting) { + pref.setEnabled(setting.isAvailable()); + } + + protected void updateListPreferenceSummary(ListPreference listPreference, Setting setting) { + String objectStringValue = setting.get().toString(); + final int entryIndex = listPreference.findIndexOfValue(objectStringValue); + if (entryIndex >= 0) { + listPreference.setSummary(listPreference.getEntries()[entryIndex]); + } else { + // Value is not an available option. + // User manually edited import data, or options changed and current selection is no longer available. + // Still show the value in the summary, so it's clear that something is selected. + listPreference.setSummary(objectStringValue); + } + } + + public static void showRestartDialog(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"); + } + + 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") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + PreferenceManager preferenceManager = getPreferenceManager(); + preferenceManager.setSharedPreferencesName(Setting.preferences.name); + + // Must initialize before adding change listener, + // otherwise the syncing of Setting -> UI + // causes a callback to the listener even though nothing changed. + initialize(); + updateUIToSettingValues(); + + preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener); + } catch (Exception ex) { + Logger.printException(() -> "onCreate() failure", ex); + } + } + + @Override + public void onDestroy() { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener); + super.onDestroy(); + } +} 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 new file mode 100644 index 0000000000..1044ba424e --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java @@ -0,0 +1,133 @@ +package app.revanced.extension.shared.settings.preference; + +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.Pair; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.LinearLayout; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +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 { + + private String existingSettings; + + private void init() { + setSelectable(true); + + EditText editText = getEditText(); + editText.setTextIsSelectable(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + editText.setAutofillHints((String) null); + } + editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + editText.setTextSize(14); + + setOnPreferenceClickListener(this); + } + + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + public ImportExportPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + public ImportExportPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + try { + // Must set text before showing dialog, + // otherwise text is non-selectable if this preference is later reopened. + existingSettings = Setting.exportToJson(getContext()); + getEditText().setText(existingSettings); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + return true; + } + + @Override + protected void showDialog(Bundle state) { + try { + Context context = getContext(); + EditText editText = getEditText(); + + // 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(() -> "showDialog failure", ex); + } + } + + private void importSettings(Context context, String replacementSettings) { + try { + if (replacementSettings.equals(existingSettings)) { + return; + } + AbstractPreferenceFragment.settingImportInProgress = true; + + final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings); + if (rebootNeeded) { + AbstractPreferenceFragment.showRestartDialog(context); + } + } catch (Exception ex) { + Logger.printException(() -> "importSettings failure", ex); + } finally { + AbstractPreferenceFragment.settingImportInProgress = false; + } + } +} 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 new file mode 100644 index 0000000000..0d4003b913 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java @@ -0,0 +1,377 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.requests.Route.Method.GET; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +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; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.ui.Dim; + +/** + * Opens a dialog showing official links. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class ReVancedAboutPreference extends Preference { + + private static String useNonBreakingHyphens(String text) { + // Replace any dashes with non breaking dashes, so the English text 'pre-release' + // and the dev release number does not break and cover two lines. + return text.replace("-", "‑"); // #8209 = non breaking hyphen. + } + + /** + * Apps that do not support bundling resources must override this. + * + * @return A localized string to display for the key. + */ + protected String getString(String key, Object ... args) { + return str(key, args); + } + + private String createDialogHtml(WebLink[] aboutLinks) { + final boolean isNetworkConnected = Utils.isNetworkConnected(); + + StringBuilder builder = new StringBuilder(); + builder.append(""); + builder.append(""); + + String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor()); + String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor()); + // Apply light/dark mode colors. + builder.append(String.format( + "", + backgroundColorHex, foregroundColorHex, foregroundColorHex)); + + if (isNetworkConnected) { + builder.append(""); + } + + String patchesVersion = Utils.getPatchesReleaseVersion(); + + // Add the title. + builder.append("

") + .append("ReVanced") + .append("

"); + + builder.append("

") + // Replace hyphens with non breaking dashes so the version number does not break lines. + .append(useNonBreakingHyphens(getString("revanced_settings_about_links_body", patchesVersion))) + .append("

"); + + // Add a disclaimer if using a dev release. + if (patchesVersion.contains("dev")) { + builder.append("

") + // English text 'Pre-release' can break lines. + .append(useNonBreakingHyphens(getString("revanced_settings_about_links_dev_header"))) + .append("

"); + + builder.append("

") + .append(getString("revanced_settings_about_links_dev_body")) + .append("

"); + } + + builder.append("

") + .append(getString("revanced_settings_about_links_header")) + .append("

"); + + builder.append("
"); + for (WebLink link : aboutLinks) { + builder.append("
"); + builder.append(String.format("%s", link.url, link.name)); + builder.append("
"); + } + builder.append("
"); + + builder.append(""); + return builder.toString(); + } + + { + 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. + final long delayToShowProgressSpinner = 500; + ProgressDialog progress = new ProgressDialog(getContext()); + progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); + + Handler handler = new Handler(Looper.getMainLooper()); + Runnable showDialogRunnable = progress::show; + handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner); + + Utils.runOnBackgroundThread(() -> + fetchLinksAndShowDialog(context, handler, showDialogRunnable, progress)); + } else { + // No network call required and can run now. + fetchLinksAndShowDialog(context, null, null, null); + } + + return false; + }); + } + + private void fetchLinksAndShowDialog(Context context, + @Nullable Handler handler, + Runnable showDialogRunnable, + @Nullable ProgressDialog progress) { + WebLink[] links = AboutLinksRoutes.fetchAboutLinks(); + String htmlDialog = createDialogHtml(links); + + // Enable to randomly force a delay to debug the spinner logic. + final boolean debugSpinnerDelayLogic = false; + //noinspection ConstantConditions + if (debugSpinnerDelayLogic && handler != null && Math.random() < 0.5f) { + Utils.doNothingForDuration((long) (Math.random() * 4000)); + } + + Utils.runOnMainThreadNowOrLater(() -> { + if (handler != null) { + handler.removeCallbacks(showDialogRunnable); + } + + // 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(); + }); + } + + public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ReVancedAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ReVancedAboutPreference(Context context) { + super(context); + } +} + +/** + * Displays html content as a dialog. Any links a user taps on are opened in an external browser. + */ +class WebViewDialog extends Dialog { + + private final String htmlContent; + + public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) { + super(context); + this.htmlContent = htmlContent; + } + + // JS required to hide any broken images. No remote javascript is ever loaded. + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); // 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); + + // 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 { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + getContext().startActivity(intent); + } catch (Exception ex) { + Logger.printException(() -> "Open link failure", ex); + } + // Dismiss the about dialog using a delay, + // otherwise without a delay the UI looks hectic with the dialog dismissing + // to show the settings while simultaneously a web browser is opening. + Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500); + return true; + } + } +} + +class WebLink { + final boolean preferred; + String name; + final String url; + + WebLink(JSONObject json) throws JSONException { + this(json.getBoolean("preferred"), + json.getString("name"), + json.getString("url") + ); + } + + WebLink(boolean preferred, String name, String url) { + this.preferred = preferred; + this.name = name; + this.url = url; + } + + @NonNull + @Override + public String toString() { + return "WebLink{" + + "preferred=" + preferred + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + '}'; + } +} + +class AboutLinksRoutes { + /** + * Backup icon url if the API call fails. + */ + public static volatile String aboutLogoUrl = "https://revanced.app/favicon.ico"; + + /** + * Links to use if fetch links api call fails. + */ + private static final WebLink[] NO_CONNECTION_STATIC_LINKS = { + new WebLink(true, "ReVanced.app", "https://revanced.app") + }; + + private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v4"; + private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/about").compile(); + + @Nullable + private static volatile WebLink[] fetchedLinks; + + static boolean hasFetchedLinks() { + return fetchedLinks != null; + } + + static WebLink[] fetchAboutLinks() { + try { + if (hasFetchedLinks()) return fetchedLinks; + + // Check if there is no internet connection. + if (!Utils.isNetworkConnected()) return NO_CONNECTION_STATIC_LINKS; + + HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(SOCIAL_LINKS_PROVIDER, GET_SOCIAL); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + Logger.printDebug(() -> "Fetching social links from: " + connection.getURL()); + + // Do not show an exception toast if the server is down + final int responseCode = connection.getResponseCode(); + if (responseCode != 200) { + Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode); + return NO_CONNECTION_STATIC_LINKS; + } + + JSONObject json = Requester.parseJSONObjectAndDisconnect(connection); + aboutLogoUrl = json.getJSONObject("branding").getString("logo"); + + List links = new ArrayList<>(); + + JSONArray donations = json.getJSONObject("donations").getJSONArray("links"); + for (int i = 0, length = donations.length(); i < length; i++) { + WebLink link = new WebLink(donations.getJSONObject(i)); + if (link.preferred) { + // This could be localized, but TikTok does not support localized resources. + // All link names returned by the api are also non localized. + link.name = "Donate"; + links.add(link); + } + } + + JSONArray socials = json.getJSONArray("socials"); + for (int i = 0, length = socials.length(); i < length; i++) { + WebLink link = new WebLink(socials.getJSONObject(i)); + links.add(link); + } + + Logger.printDebug(() -> "links: " + links); + + return fetchedLinks = links.toArray(new WebLink[0]); + + } catch (SocketTimeoutException ex) { + Logger.printInfo(() -> "Could not fetch social links", ex); // No toast. + } catch (JSONException ex) { + Logger.printException(() -> "Could not parse about information", ex); + } catch (Exception ex) { + Logger.printException(() -> "Failed to get about information", ex); + } + + return NO_CONNECTION_STATIC_LINKS; + } +} 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 new file mode 100644 index 0000000000..c6f323ceb4 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java @@ -0,0 +1,105 @@ +package app.revanced.extension.shared.settings.preference; + +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.util.Pair; +import android.widget.EditText; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import java.util.Objects; + +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); + } + public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ResettableEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ResettableEditTextPreference(Context context) { + super(context); + } + + public void setSetting(@Nullable Setting setting) { + this.setting = setting; + } + + @Override + protected void showDialog(Bundle state) { + try { + Context context = getContext(); + EditText editText = getEditText(); + + // 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 new file mode 100644 index 0000000000..ed5db6b235 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java @@ -0,0 +1,190 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceFragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +import java.util.Objects; + +/** + * Shared categories, and helper methods. + * + * The various save methods store numbers as Strings, + * which is required if using {@link PreferenceFragment}. + * + * If saved numbers will not be used with a preference fragment, + * then store the primitive numbers using the {@link #preferences} itself. + */ +public class SharedPrefCategory { + @NonNull + public final String name; + @NonNull + public final SharedPreferences preferences; + + public SharedPrefCategory(@NonNull String name) { + this.name = Objects.requireNonNull(name); + preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE); + } + + private void removeConflictingPreferenceKeyValue(@NonNull String key) { + Logger.printException(() -> "Found conflicting preference: " + key); + removeKey(key); + } + + private void saveObjectAsString(@NonNull String key, @Nullable Object value) { + preferences.edit().putString(key, (value == null ? null : value.toString())).commit(); + } + + /** + * Removes any preference data type that has the specified key. + */ + public void removeKey(@NonNull String key) { + preferences.edit().remove(Objects.requireNonNull(key)).commit(); + } + + public void saveBoolean(@NonNull String key, boolean value) { + preferences.edit().putBoolean(key, value).commit(); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveEnumAsString(@NonNull String key, @Nullable Enum value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveIntegerString(@NonNull String key, @Nullable Integer value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveLongString(@NonNull String key, @Nullable Long value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveFloatString(@NonNull String key, @Nullable Float value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveString(@NonNull String key, @Nullable String value) { + saveObjectAsString(key, value); + } + + @NonNull + public String getString(@NonNull String key, @NonNull String _default) { + Objects.requireNonNull(_default); + try { + return preferences.getString(key, _default); + } catch (ClassCastException ex) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + return _default; + } + } + + @NonNull + public > T getEnum(@NonNull String key, @NonNull T _default) { + Objects.requireNonNull(_default); + try { + String enumName = preferences.getString(key, null); + if (enumName != null) { + try { + // noinspection unchecked + return (T) Enum.valueOf(_default.getClass(), enumName); + } catch (IllegalArgumentException ex) { + // Info level to allow removing enum values in the future without showing any user errors. + Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName); + removeKey(key); + } + } + } catch (ClassCastException ex) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + } + return _default; + } + + public boolean getBoolean(@NonNull String key, boolean _default) { + try { + return preferences.getBoolean(key, _default); + } catch (ClassCastException ex) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + return _default; + } + } + + @NonNull + public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) { + try { + String value = preferences.getString(key, null); + if (value != null) { + return Integer.valueOf(value); + } + } catch (ClassCastException | NumberFormatException ex) { + try { + // Old data previously stored as primitive. + return preferences.getInt(key, _default); + } catch (ClassCastException ex2) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + } + } + return _default; + } + + @NonNull + public Long getLongString(@NonNull String key, @NonNull Long _default) { + try { + String value = preferences.getString(key, null); + if (value != null) { + return Long.valueOf(value); + } + } catch (ClassCastException | NumberFormatException ex) { + try { + return preferences.getLong(key, _default); + } catch (ClassCastException ex2) { + removeConflictingPreferenceKeyValue(key); + } + } + return _default; + } + + @NonNull + public Float getFloatString(@NonNull String key, @NonNull Float _default) { + try { + String value = preferences.getString(key, null); + if (value != null) { + return Float.valueOf(value); + } + } catch (ClassCastException | NumberFormatException ex) { + try { + return preferences.getFloat(key, _default); + } catch (ClassCastException ex2) { + removeConflictingPreferenceKeyValue(key); + } + } + return _default; + } + + @NonNull + @Override + public String toString() { + return name; + } +} 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