refactor: Bump ReVanced Patcher & merge integrations by using ReVanced Patches Gradle plugin

BREAKING CHANGE: ReVanced Patcher >= 21 required
This commit is contained in:
inotia00 2024-12-07 22:13:39 +09:00
parent f074c3ecc5
commit b31865afbe
2706 changed files with 64970 additions and 30705 deletions

3
.editorconfig Normal file
View File

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

View File

@ -1,49 +0,0 @@
name: 🐞 Bug report
description: Report a bug or an issue.
title: 'bug: '
labels: ['Bug report']
body:
- type: markdown
attributes:
value: |
# ReVanced Patches bug report
Please check for existing bug reports
[here](https://github.com/ReVanced/revanced-patches/labels/Bug%20report)
before creating a new one.
- type: textarea
attributes:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
- List used patches if applicable
validations:
required: true
- type: textarea
attributes:
label: Error logs
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
render: shell
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution to the bug.
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you don't follow the checklist below.
options:
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

110
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,110 @@
name: 🐞 Bug report
description: Report a bug or an issue.
title: 'bug: '
labels: ['Bug report']
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# ReVanced Patches bug report
Before creating a new bug report, please keep the following in mind:
- **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).
- **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:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
- List used patches if applicable
validations:
required: true
- type: textarea
attributes:
label: Error logs
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
render: shell
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution to the bug.
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- 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
- label: All requested information has been provided properly.
required: true

View File

@ -1,44 +0,0 @@
name: ⭐ Feature request
description: Create a detailed request for a new feature.
title: 'feat: '
labels: ['Feature request']
body:
- type: markdown
attributes:
value: |
# ReVanced Patches feature request
Please check for existing feature requests
[here](https://github.com/ReVanced/revanced-patches/labels/Feature%20request)
before creating a new one.
- type: textarea
attributes:
label: Feature description
description: |
- Describe your feature in detail
- Add images, videos, links, examples, references, etc. if possible
- Add the target application name in case you request a new patch
- type: textarea
attributes:
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
- What makes this feature important?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you don't follow the checklist below.
options:
- label: This request is not a duplicate of an existing issue.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

View File

@ -0,0 +1,106 @@
name: ⭐ Feature request
description: Create a detailed request for a new feature.
title: 'feat: '
labels: ['Feature request']
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patches/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# ReVanced Patches feature request
Before creating a new feature request, please keep the following in mind:
- **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).
- **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:
label: Feature description
description: |
- Describe your feature in detail
- Add images, videos, links, examples, references, etc. if possible
- Add the target application name in case you request a new patch
- type: textarea
attributes:
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
- What makes this feature important?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- 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
- label: All requested information has been provided properly.
required: true

2
.github/config.yml vendored
View File

@ -1,2 +1,2 @@
firstPRMergeComment: > firstPRMergeComment: >
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role. Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.

22
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: npm
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: gradle
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly

View File

@ -0,0 +1,31 @@
name: Build pull request
on:
workflow_dispatch:
pull_request:
branches:
- dev
jobs:
release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build --no-daemon

28
.github/workflows/open_pull_request.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Open a PR to main
on:
push:
branches:
- dev
workflow_dispatch:
env:
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
jobs:
pull-request:
name: Open pull request
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: main
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: |
This pull request will ${{ env.MESSAGE }}.
pr_draft: true

60
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,60 @@
name: Release
on:
workflow_dispatch:
push:
branches:
- main
- dev
jobs:
release:
name: Release
permissions:
contents: write
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# To update `README.md` and `patches.json`, the command `./gradlew generatePatchesFiles clean` should be used instead of the command `./gradlew build clean`
run: ./gradlew generatePatchesFiles clean
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm exec semantic-release

View File

@ -0,0 +1,18 @@
name: Update Gradle wrapper
on:
schedule:
- cron: "0 0 1 * *"
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v1
with:
target-branch: dev

5
.gitignore vendored
View File

@ -122,7 +122,8 @@ gradle-app.setting
# Dependency directories # Dependency directories
node_modules/ node_modules/
# gradle properties, due to Github token # Gradle properties, due to Github token
./gradle.properties ./gradle.properties
.DS_Store # One package is called the same as the Gradle build folder
!**/src/**/build/

3
.idea/misc.xml generated
View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration"> <component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" /> <file type="web" url="file://$PROJECT_DIR$" />
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="azul-17" project-jdk-type="JavaSDK" /> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK" />
</project> </project>

View File

@ -24,8 +24,9 @@
"README.md", "README.md",
"CHANGELOG.md", "CHANGELOG.md",
"gradle.properties", "gradle.properties",
"patches.json" "patches.json",
] ],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
} }
], ],
[ [
@ -33,11 +34,11 @@
{ {
"assets": [ "assets": [
{ {
"path": "build/libs/revanced-patches*" "path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
}, },
{ {
"path": "patches.json" "path": "patches.json"
} },
], ],
successComment: false successComment: false
} }

View File

@ -26,7 +26,6 @@ Example:
} }
], ],
"use":true, "use":true,
"requiresIntegrations":false,
"options": [] "options": []
}, },
{ {
@ -39,7 +38,6 @@ Example:
} }
], ],
"use":true, "use":true,
"requiresIntegrations":false,
"options": [] "options": []
}, },
{ {
@ -52,7 +50,6 @@ Example:
} }
], ],
"use":true, "use":true,
"requiresIntegrations":true,
"options": [] "options": []
} }
] ]

View File

@ -18,9 +18,9 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 18.29.38 ~ 19.16.39 | | `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 18.29.38 ~ 19.16.39 |
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.16.39 | | `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.16.39 |
| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.16.39 | | `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.16.39 |
| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in options.json. | 18.29.38 ~ 19.16.39 | | `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 18.29.38 ~ 19.16.39 |
| `Custom branding name for YouTube` | Renames the YouTube app to the name specified in options.json. | 18.29.38 ~ 19.16.39 | | `Custom branding name for YouTube` | Renames the YouTube app to the name specified in patch options. | 18.29.38 ~ 19.16.39 |
| `Custom double tap length` | Adds Double-tap to seek values that are specified in options.json. | 18.29.38 ~ 19.16.39 | | `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 18.29.38 ~ 19.16.39 |
| `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 18.29.38 ~ 19.16.39 | | `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 18.29.38 ~ 19.16.39 |
| `Description components` | Adds options to hide and disable description components. | 18.29.38 ~ 19.16.39 | | `Description components` | Adds options to hide and disable description components. | 18.29.38 ~ 19.16.39 |
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 18.29.38 ~ 19.16.39 | | `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 18.29.38 ~ 19.16.39 |
@ -67,7 +67,7 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 18.29.38 ~ 19.16.39 | | `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 18.29.38 ~ 19.16.39 |
| `Spoof streaming data` | Adds options to spoof the streaming data to allow video playback. | 18.29.38 ~ 19.16.39 | | `Spoof streaming data` | Adds options to spoof the streaming data to allow video playback. | 18.29.38 ~ 19.16.39 |
| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.16.39 | | `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.16.39 |
| `Theme` | Changes the app's theme to the values specified in options.json. | 18.29.38 ~ 19.16.39 | | `Theme` | Changes the app's theme to the values specified in patch options. | 18.29.38 ~ 19.16.39 |
| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header. | 18.29.38 ~ 19.16.39 | | `Toolbar components` | Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header. | 18.29.38 ~ 19.16.39 |
| `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.16.39 | | `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.16.39 |
| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.16.39 | | `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.16.39 |
@ -86,8 +86,8 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 7.16.53 | | `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 7.16.53 |
| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 6.20.51 ~ 7.16.53 | | `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 6.20.51 ~ 7.16.53 |
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 7.16.53 | | `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 7.16.53 |
| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in options.json. | 6.20.51 ~ 7.16.53 | | `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 7.16.53 |
| `Custom branding name for YouTube Music` | Renames the YouTube Music app to the name specified in options.json. | 6.20.51 ~ 7.16.53 | | `Custom branding name for YouTube Music` | Renames the YouTube Music app to the name specified in patch options. | 6.20.51 ~ 7.16.53 |
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 7.16.53 | | `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 7.16.53 |
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 7.16.53 | | `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 7.16.53 |
| `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 7.16.53 | | `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 7.16.53 |
@ -124,8 +124,8 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version | | 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:| |:--------:|:--------------:|:-----------------:|
| `Change package name` | Changes the package name for Reddit to the name specified in options.json. | 2023.12.0 ~ 2024.17.0 | | `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2023.12.0 ~ 2024.17.0 |
| `Custom branding name for Reddit` | Renames the Reddit app to the name specified in options.json. | 2023.12.0 ~ 2024.17.0 | | `Custom branding name for Reddit` | Renames the Reddit app to the name specified in patch options. | 2023.12.0 ~ 2024.17.0 |
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2023.12.0 ~ 2024.17.0 | | `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2023.12.0 ~ 2024.17.0 |
| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2023.12.0 ~ 2024.17.0 | | `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2023.12.0 ~ 2024.17.0 |
| `Hide ads` | Adds options to hide ads. | 2023.12.0 ~ 2024.17.0 | | `Hide ads` | Adds options to hide ads. | 2023.12.0 ~ 2024.17.0 |
@ -166,7 +166,6 @@ Example:
} }
], ],
"use":true, "use":true,
"requiresIntegrations":false,
"options": [] "options": []
}, },
{ {
@ -185,7 +184,6 @@ Example:
} }
], ],
"use":true, "use":true,
"requiresIntegrations":false,
"options": [] "options": []
}, },
{ {
@ -201,7 +199,6 @@ Example:
} }
], ],
"use":true, "use":true,
"requiresIntegrations":true,
"options": [] "options": []
} }
] ]

View File

@ -1,149 +0,0 @@
import org.gradle.kotlin.dsl.support.listFilesOrdered
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin)
`maven-publish`
signing
}
group = "app.revanced"
repositories {
mavenCentral()
mavenLocal()
google()
maven {
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
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)
// 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/generator")
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<JavaExec>("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/inotia00/revanced-patches")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("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"])
}

View File

@ -0,0 +1,30 @@
extension {
name = "extensions/shared.rve"
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 24
}
buildTypes {
release {
isMinifyEnabled = true
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.preference)
implementation(libs.lang3)
compileOnly(project(":extensions:shared:stub"))
}

9
extensions/shared/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,9 @@
-dontobfuscate
-dontoptimize
-keepattributes *
-keep class app.revanced.** {
*;
}
-keep class com.google.** {
*;
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
</manifest>

View File

@ -0,0 +1,66 @@
package app.revanced.extension.music.patches.account;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.view.View;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import app.revanced.extension.music.settings.Settings;
@SuppressWarnings("unused")
public class AccountPatch {
private static String[] accountMenuBlockList;
static {
accountMenuBlockList = Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS.get().split("\\n");
// Some settings should not be hidden.
if (isSDKAbove(24)) {
accountMenuBlockList = Arrays.stream(accountMenuBlockList)
.filter(item -> !Objects.equals(item, str("settings")))
.toArray(String[]::new);
} else {
List<String> tmp = new ArrayList<>(Arrays.asList(accountMenuBlockList));
tmp.remove(str("settings")); // "Settings" should appear only once in the account menu
accountMenuBlockList = tmp.toArray(new String[0]);
}
}
public static void hideAccountMenu(CharSequence charSequence, View view) {
if (!Settings.HIDE_ACCOUNT_MENU.get())
return;
if (charSequence == null) {
if (Settings.HIDE_ACCOUNT_MENU_EMPTY_COMPONENT.get())
view.setVisibility(View.GONE);
return;
}
for (String filter : accountMenuBlockList) {
if (!filter.isEmpty() && charSequence.toString().equals(filter))
view.setVisibility(View.GONE);
}
}
public static boolean hideHandle(boolean original) {
return Settings.HIDE_HANDLE.get() || original;
}
public static void hideHandle(TextView textView, int visibility) {
final int finalVisibility = Settings.HIDE_HANDLE.get()
? View.GONE
: visibility;
textView.setVisibility(finalVisibility);
}
public static int hideTermsContainer() {
return Settings.HIDE_TERMS_CONTAINER.get() ? View.GONE : View.VISIBLE;
}
}

View File

@ -0,0 +1,89 @@
package app.revanced.extension.music.patches.actionbar;
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import android.view.View;
import androidx.annotation.NonNull;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.utils.VideoUtils;
@SuppressWarnings("unused")
public class ActionBarPatch {
@NonNull
private static String buttonType = "";
public static boolean hideActionBarLabel() {
return Settings.HIDE_ACTION_BUTTON_LABEL.get();
}
public static boolean hideActionButton() {
for (ActionButton actionButton : ActionButton.values())
if (actionButton.enabled && actionButton.name.equals(buttonType))
return true;
return false;
}
public static void hideLikeDislikeButton(View view) {
final boolean enabled = Settings.HIDE_ACTION_BUTTON_LIKE_DISLIKE.get();
hideViewUnderCondition(
enabled,
view
);
hideViewBy0dpUnderCondition(
enabled,
view
);
}
public static void inAppDownloadButtonOnClick(View view) {
if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) {
return;
}
if (buttonType.equals(ActionButton.DOWNLOAD.name))
view.setOnClickListener(imageView -> VideoUtils.launchExternalDownloader());
}
public static void setButtonType(@NonNull Object obj) {
final String buttonType = obj.toString();
for (ActionButton actionButton : ActionButton.values())
if (buttonType.contains(actionButton.identifier))
setButtonType(actionButton.name);
}
public static void setButtonType(@NonNull String newButtonType) {
buttonType = newButtonType;
}
public static void setButtonTypeDownload(int type) {
if (type != 0)
return;
setButtonType(ActionButton.DOWNLOAD.name);
}
private enum ActionButton {
ADD_TO_PLAYLIST("ACTION_BUTTON_ADD_TO_PLAYLIST", "69487224", Settings.HIDE_ACTION_BUTTON_ADD_TO_PLAYLIST.get()),
COMMENT_DISABLED("ACTION_BUTTON_COMMENT", "76623563", Settings.HIDE_ACTION_BUTTON_COMMENT.get()),
COMMENT_ENABLED("ACTION_BUTTON_COMMENT", "138681778", Settings.HIDE_ACTION_BUTTON_COMMENT.get()),
DOWNLOAD("ACTION_BUTTON_DOWNLOAD", "73080600", Settings.HIDE_ACTION_BUTTON_DOWNLOAD.get()),
RADIO("ACTION_BUTTON_RADIO", "48687757", Settings.HIDE_ACTION_BUTTON_RADIO.get()),
SHARE("ACTION_BUTTON_SHARE", "90650344", Settings.HIDE_ACTION_BUTTON_SHARE.get());
private final String name;
private final String identifier;
private final boolean enabled;
ActionButton(String name, String identifier, boolean enabled) {
this.name = name;
this.identifier = identifier;
this.enabled = enabled;
}
}
}

View File

@ -0,0 +1,15 @@
package app.revanced.extension.music.patches.ads;
import app.revanced.extension.music.settings.Settings;
@SuppressWarnings("unused")
public class MusicAdsPatch {
public static boolean hideMusicAds() {
return !Settings.HIDE_MUSIC_ADS.get();
}
public static boolean hideMusicAds(boolean original) {
return !Settings.HIDE_MUSIC_ADS.get() && original;
}
}

View File

@ -0,0 +1,40 @@
package app.revanced.extension.music.patches.ads;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class PremiumPromotionPatch {
public static void hidePremiumPromotion(View view) {
if (!Settings.HIDE_PREMIUM_PROMOTION.get())
return;
view.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
try {
if (!(view instanceof ViewGroup viewGroup)) {
return;
}
if (!(viewGroup.getChildAt(0) instanceof ViewGroup mealBarLayoutRoot)) {
return;
}
if (!(mealBarLayoutRoot.getChildAt(0) instanceof LinearLayout linearLayout)) {
return;
}
if (!(linearLayout.getChildAt(0) instanceof ImageView imageView)) {
return;
}
if (imageView.getVisibility() == View.VISIBLE) {
view.setVisibility(View.GONE);
}
} catch (Exception ex) {
Logger.printException(() -> "hideGetPremium failure", ex);
}
});
}
}

View File

@ -0,0 +1,39 @@
package app.revanced.extension.music.patches.ads;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class PremiumRenewalPatch {
public static void hidePremiumRenewal(LinearLayout buttonContainerView) {
if (!Settings.HIDE_PREMIUM_RENEWAL.get())
return;
buttonContainerView.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
try {
Utils.runOnMainThreadDelayed(() -> {
if (!(buttonContainerView.getChildAt(0) instanceof ViewGroup closeButtonParentView))
return;
if (!(closeButtonParentView.getChildAt(0) instanceof TextView closeButtonView))
return;
if (closeButtonView.getText().toString().equals(str("dialog_got_it_text")))
Utils.clickView(closeButtonView);
else
Utils.hideViewByLayoutParams((View) buttonContainerView.getParent());
}, 0
);
} catch (Exception ex) {
Logger.printException(() -> "hidePremiumRenewal failure", ex);
}
});
}
}

View File

@ -0,0 +1,31 @@
package app.revanced.extension.music.patches.components;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
@SuppressWarnings("unused")
public final class AdsFilter extends Filter {
public AdsFilter() {
final StringFilterGroup alertBannerPromo = new StringFilterGroup(
Settings.HIDE_PROMOTION_ALERT_BANNER,
"alert_banner_promo.eml"
);
final StringFilterGroup paidPromotionLabel = new StringFilterGroup(
Settings.HIDE_PAID_PROMOTION_LABEL,
"music_paid_content_overlay.eml"
);
addIdentifierCallbacks(alertBannerPromo, paidPromotionLabel);
final StringFilterGroup statementBanner = new StringFilterGroup(
Settings.HIDE_GENERAL_ADS,
"statement_banner"
);
addPathCallbacks(statementBanner);
}
}

View File

@ -0,0 +1,164 @@
package app.revanced.extension.music.patches.components;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.music.settings.Settings;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.utils.ByteTrieSearch;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
/**
* Allows custom filtering using a path and optionally a proto buffer string.
*/
@SuppressWarnings("unused")
public final class CustomFilter extends Filter {
private static void showInvalidSyntaxToast(@NonNull 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 a proto buffer string pattern.
*/
public static final String SYNTAX_BUFFER_SYMBOL = "$";
/**
* @return the parsed objects
*/
@NonNull
@SuppressWarnings("ConstantConditions")
static Collection<CustomFilterGroup> parseCustomFilterGroups() {
String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get();
if (rawCustomFilterText.isBlank()) {
return Collections.emptyList();
}
// Map key is the path including optional special characters (^ and/or $)
Map<String, CustomFilterGroup> result = new HashMap<>();
Pattern pattern = Pattern.compile(
"(" // map key group
+ "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with
+ "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path
+ "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol
+ ")" // end map key group
+ "(.*)"); // optional buffer string
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 boolean hasBufferSymbol = !matcher.group(4).isEmpty();
final String bufferString = matcher.group(5);
if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) {
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 (hasBufferSymbol) {
group.addBufferString(bufferString);
}
}
return result.values();
}
final boolean startsWith;
ByteTrieSearch bufferSearch;
CustomFilterGroup(boolean startsWith, @NonNull String path) {
super(Settings.CUSTOM_FILTER, path);
this.startsWith = startsWith;
}
void addBufferString(@NonNull String bufferString) {
if (bufferSearch == null) {
bufferSearch = new ByteTrieSearch();
}
bufferSearch.addPattern(bufferString.getBytes());
}
@NonNull
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("CustomFilterGroup{");
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<CustomFilterGroup> 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 path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// All callbacks are custom filter groups.
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
if (custom.startsWith && contentIndex != 0) {
return false;
}
if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
return false;
}
return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}

View File

@ -0,0 +1,39 @@
package app.revanced.extension.music.patches.components;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
@SuppressWarnings("unused")
public final class LayoutComponentsFilter extends Filter {
public LayoutComponentsFilter() {
final StringFilterGroup buttonShelf = new StringFilterGroup(
Settings.HIDE_BUTTON_SHELF,
"entry_point_button_shelf.eml"
);
final StringFilterGroup carouselShelf = new StringFilterGroup(
Settings.HIDE_CAROUSEL_SHELF,
"music_grid_item_carousel.eml"
);
final StringFilterGroup playlistCardShelf = new StringFilterGroup(
Settings.HIDE_PLAYLIST_CARD_SHELF,
"music_container_card_shelf.eml"
);
final StringFilterGroup sampleShelf = new StringFilterGroup(
Settings.HIDE_SAMPLE_SHELF,
"immersive_card_shelf.eml"
);
addIdentifierCallbacks(
buttonShelf,
carouselShelf,
playlistCardShelf,
sampleShelf
);
}
}

View File

@ -0,0 +1,25 @@
package app.revanced.extension.music.patches.components;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
@SuppressWarnings("unused")
public final class PlayerComponentsFilter extends Filter {
public PlayerComponentsFilter() {
addIdentifierCallbacks(
new StringFilterGroup(
Settings.HIDE_COMMENT_CHANNEL_GUIDELINES,
"channel_guidelines_entry_banner.eml",
"community_guidelines.eml"
)
);
addPathCallbacks(
new StringFilterGroup(
Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS,
"|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|"
)
);
}
}

View File

@ -0,0 +1,19 @@
package app.revanced.extension.music.patches.components;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
@SuppressWarnings("unused")
public final class PlayerFlyoutMenuFilter extends Filter {
public PlayerFlyoutMenuFilter() {
addIdentifierCallbacks(
new StringFilterGroup(
Settings.HIDE_FLYOUT_MENU_3_COLUMN_COMPONENT,
"music_highlight_menu_item_carousel.eml",
"tile_button_carousel.eml"
)
);
}
}

View File

@ -0,0 +1,33 @@
package app.revanced.extension.music.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.music.patches.misc.ShareSheetPatch;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup;
/**
* Abuse LithoFilter for {@link ShareSheetPatch}.
*/
public final class ShareSheetMenuFilter extends Filter {
// Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread.
public static volatile boolean isShareSheetMenuVisible;
public ShareSheetMenuFilter() {
addIdentifierCallbacks(
new StringFilterGroup(
Settings.CHANGE_SHARE_SHEET,
"share_sheet_container.eml"
)
);
}
@Override
public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
isShareSheetMenuVisible = true;
return false;
}
}

View File

@ -0,0 +1,175 @@
package app.revanced.extension.music.patches.flyout;
import static app.revanced.extension.shared.utils.ResourceUtils.getIdentifier;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.clickView;
import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoType;
import app.revanced.extension.music.utils.VideoUtils;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils.ResourceType;
@SuppressWarnings("unused")
public class FlyoutPatch {
public static int enableCompactDialog(int original) {
if (!Settings.ENABLE_COMPACT_DIALOG.get())
return original;
return Math.max(original, 600);
}
public static boolean enableTrimSilence(boolean original) {
if (!Settings.ENABLE_TRIM_SILENCE.get())
return original;
return VideoType.getCurrent().isPodCast() || original;
}
public static boolean enableTrimSilenceSwitch(boolean original) {
if (!Settings.ENABLE_TRIM_SILENCE.get())
return original;
return VideoType.getCurrent().isPodCast() && original;
}
public static boolean hideComponents(@Nullable Enum<?> flyoutMenuEnum) {
if (flyoutMenuEnum == null)
return false;
final String flyoutMenuName = flyoutMenuEnum.name();
Logger.printDebug(() -> "flyoutMenu: " + flyoutMenuName);
for (FlyoutPanelComponent component : FlyoutPanelComponent.values())
if (component.name.equals(flyoutMenuName) && component.enabled)
return true;
return false;
}
public static void hideLikeDislikeContainer(View view) {
if (!Settings.HIDE_FLYOUT_MENU_LIKE_DISLIKE.get())
return;
if (view.getParent() instanceof ViewGroup viewGroup) {
viewGroup.removeView(view);
}
}
private static volatile boolean lastMenuWasDismissQueue = false;
private static WeakReference<View> touchOutSideViewRef = new WeakReference<>(null);
public static void setTouchOutSideView(View touchOutSideView) {
touchOutSideViewRef = new WeakReference<>(touchOutSideView);
}
public static void replaceComponents(@Nullable Enum<?> flyoutPanelEnum, @NonNull TextView textView, @NonNull ImageView imageView) {
if (flyoutPanelEnum == null)
return;
final String enumString = flyoutPanelEnum.name();
final boolean isDismissQue = enumString.equals("DISMISS_QUEUE");
final boolean isReport = enumString.equals("FLAG");
if (isDismissQue) {
replaceDismissQueue(textView, imageView);
} else if (isReport) {
replaceReport(textView, imageView, lastMenuWasDismissQueue);
}
lastMenuWasDismissQueue = isDismissQue;
}
private static void replaceDismissQueue(@NonNull TextView textView, @NonNull ImageView imageView) {
if (!Settings.REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get())
return;
if (!(textView.getParent() instanceof ViewGroup clickAbleArea))
return;
runOnMainThreadDelayed(() -> {
textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label"));
imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
clickAbleArea.setOnClickListener(viewGroup -> VideoUtils.openInYouTube());
}, 0L
);
}
private static final ColorFilter cf = new PorterDuffColorFilter(Color.parseColor("#ffffffff"), PorterDuff.Mode.SRC_ATOP);
private static void replaceReport(@NonNull TextView textView, @NonNull ImageView imageView, boolean wasDismissQueue) {
if (!Settings.REPLACE_FLYOUT_MENU_REPORT.get())
return;
if (Settings.REPLACE_FLYOUT_MENU_REPORT_ONLY_PLAYER.get() && !wasDismissQueue)
return;
if (!(textView.getParent() instanceof ViewGroup clickAbleArea))
return;
runOnMainThreadDelayed(() -> {
textView.setText(str("playback_rate_title"));
imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
imageView.setColorFilter(cf);
clickAbleArea.setOnClickListener(view -> {
clickView(touchOutSideViewRef.get());
VideoUtils.showPlaybackSpeedFlyoutMenu();
});
}, 0L
);
}
private enum FlyoutPanelComponent {
SAVE_EPISODE_FOR_LATER("BOOKMARK_BORDER", Settings.HIDE_FLYOUT_MENU_SAVE_EPISODE_FOR_LATER.get()),
SHUFFLE_PLAY("SHUFFLE", Settings.HIDE_FLYOUT_MENU_SHUFFLE_PLAY.get()),
RADIO("MIX", Settings.HIDE_FLYOUT_MENU_START_RADIO.get()),
SUBSCRIBE("SUBSCRIBE", Settings.HIDE_FLYOUT_MENU_SUBSCRIBE.get()),
EDIT_PLAYLIST("EDIT", Settings.HIDE_FLYOUT_MENU_EDIT_PLAYLIST.get()),
DELETE_PLAYLIST("DELETE", Settings.HIDE_FLYOUT_MENU_DELETE_PLAYLIST.get()),
PLAY_NEXT("QUEUE_PLAY_NEXT", Settings.HIDE_FLYOUT_MENU_PLAY_NEXT.get()),
ADD_TO_QUEUE("QUEUE_MUSIC", Settings.HIDE_FLYOUT_MENU_ADD_TO_QUEUE.get()),
SAVE_TO_LIBRARY("LIBRARY_ADD", Settings.HIDE_FLYOUT_MENU_SAVE_TO_LIBRARY.get()),
REMOVE_FROM_LIBRARY("LIBRARY_REMOVE", Settings.HIDE_FLYOUT_MENU_REMOVE_FROM_LIBRARY.get()),
REMOVE_FROM_PLAYLIST("REMOVE_FROM_PLAYLIST", Settings.HIDE_FLYOUT_MENU_REMOVE_FROM_PLAYLIST.get()),
DOWNLOAD("OFFLINE_DOWNLOAD", Settings.HIDE_FLYOUT_MENU_DOWNLOAD.get()),
SAVE_TO_PLAYLIST("ADD_TO_PLAYLIST", Settings.HIDE_FLYOUT_MENU_SAVE_TO_PLAYLIST.get()),
GO_TO_EPISODE("INFO", Settings.HIDE_FLYOUT_MENU_GO_TO_EPISODE.get()),
GO_TO_PODCAST("BROADCAST", Settings.HIDE_FLYOUT_MENU_GO_TO_PODCAST.get()),
GO_TO_ALBUM("ALBUM", Settings.HIDE_FLYOUT_MENU_GO_TO_ALBUM.get()),
GO_TO_ARTIST("ARTIST", Settings.HIDE_FLYOUT_MENU_GO_TO_ARTIST.get()),
VIEW_SONG_CREDIT("PEOPLE_GROUP", Settings.HIDE_FLYOUT_MENU_VIEW_SONG_CREDIT.get()),
SHARE("SHARE", Settings.HIDE_FLYOUT_MENU_SHARE.get()),
DISMISS_QUEUE("DISMISS_QUEUE", Settings.HIDE_FLYOUT_MENU_DISMISS_QUEUE.get()),
HELP("HELP_OUTLINE", Settings.HIDE_FLYOUT_MENU_HELP.get()),
REPORT("FLAG", Settings.HIDE_FLYOUT_MENU_REPORT.get()),
QUALITY("SETTINGS_MATERIAL", Settings.HIDE_FLYOUT_MENU_QUALITY.get()),
CAPTIONS("CAPTIONS", Settings.HIDE_FLYOUT_MENU_CAPTIONS.get()),
STATS_FOR_NERDS("PLANNER_REVIEW", Settings.HIDE_FLYOUT_MENU_STATS_FOR_NERDS.get()),
SLEEP_TIMER("MOON_Z", Settings.HIDE_FLYOUT_MENU_SLEEP_TIMER.get());
private final boolean enabled;
private final String name;
FlyoutPanelComponent(String name, boolean enabled) {
this.enabled = enabled;
this.name = name;
}
}
}

View File

@ -0,0 +1,181 @@
package app.revanced.extension.music.patches.general;
import static app.revanced.extension.music.utils.ExtendedUtils.isSpoofingToLessThan;
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
import android.app.AlertDialog;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import app.revanced.extension.music.settings.Settings;
/**
* @noinspection ALL
*/
@SuppressWarnings("unused")
public class GeneralPatch {
// region [Change start page] patch
public static String changeStartPage(final String browseId) {
if (!browseId.equals("FEmusic_home"))
return browseId;
return Settings.CHANGE_START_PAGE.get();
}
// endregion
// region [Disable dislike redirection] patch
public static boolean disableDislikeRedirection() {
return Settings.DISABLE_DISLIKE_REDIRECTION.get();
}
// endregion
// region [Enable landscape mode] patch
public static boolean enableLandScapeMode(boolean original) {
return Settings.ENABLE_LANDSCAPE_MODE.get() || original;
}
// endregion
// region [Hide layout components] patch
public static int hideCastButton(int original) {
return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original;
}
public static void hideCastButton(View view) {
hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON.get(), view);
}
public static void hideCategoryBar(View view) {
hideViewBy0dpUnderCondition(Settings.HIDE_CATEGORY_BAR.get(), view);
}
public static boolean hideFloatingButton() {
return Settings.HIDE_FLOATING_BUTTON.get();
}
public static boolean hideTapToUpdateButton() {
return Settings.HIDE_TAP_TO_UPDATE_BUTTON.get();
}
public static boolean hideHistoryButton(boolean original) {
return !Settings.HIDE_HISTORY_BUTTON.get() && original;
}
public static void hideNotificationButton(View view) {
if (view.getParent() instanceof ViewGroup viewGroup) {
hideViewBy0dpUnderCondition(Settings.HIDE_NOTIFICATION_BUTTON, viewGroup);
}
}
public static boolean hideSoundSearchButton(boolean original) {
if (!Settings.SETTINGS_INITIALIZED.get()) {
return original;
}
return !Settings.HIDE_SOUND_SEARCH_BUTTON.get();
}
public static void hideVoiceSearchButton(ImageView view, int visibility) {
final int finalVisibility = Settings.HIDE_VOICE_SEARCH_BUTTON.get()
? View.GONE
: visibility;
view.setVisibility(finalVisibility);
}
public static void hideTasteBuilder(View view) {
view.setVisibility(View.GONE);
}
// endregion
// region [Hide overlay filter] patch
public static void disableDimBehind(Window window) {
if (window != null) {
// Disable AlertDialog's background dim.
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
}
}
// endregion
// region [Remove viewer discretion dialog] patch
/**
* Injection point.
* <p>
* The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called.
* Otherwise {@link AlertDialog#getButton(int)} method will always return null.
* https://stackoverflow.com/a/4604145
* <p>
* That's why {@link AlertDialog#show()} is absolutely necessary.
* Instead, use two tricks to hide Alertdialog.
* <p>
* 1. Change the size of AlertDialog to 0.
* 2. Disable AlertDialog's background dim.
* <p>
* This way, AlertDialog will be completely hidden,
* and {@link AlertDialog#getButton(int)} method can be used without issue.
*/
public static void confirmDialog(final AlertDialog dialog) {
if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) {
return;
}
// This method is called after AlertDialog#show(),
// So we need to hide the AlertDialog before pressing the possitive button.
final Window window = dialog.getWindow();
final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
if (window != null && button != null) {
WindowManager.LayoutParams params = window.getAttributes();
params.height = 0;
params.width = 0;
// Change the size of AlertDialog to 0.
window.setAttributes(params);
// Disable AlertDialog's background dim.
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
button.callOnClick();
}
}
// endregion
// region [Restore old style library shelf] patch
public static String restoreOldStyleLibraryShelf(final String browseId) {
final boolean oldStyleLibraryShelfEnabled =
Settings.RESTORE_OLD_STYLE_LIBRARY_SHELF.get() || isSpoofingToLessThan("5.38.00");
return oldStyleLibraryShelfEnabled && browseId.equals("FEmusic_library_landing")
? "FEmusic_liked"
: browseId;
}
// endregion
// region [Spoof app version] patch
public static String getVersionOverride(String version) {
if (!Settings.SPOOF_APP_VERSION.get())
return version;
return Settings.SPOOF_APP_VERSION_TARGET.get();
}
// endregion
}

View File

@ -0,0 +1,41 @@
package app.revanced.extension.music.patches.general;
import androidx.preference.PreferenceScreen;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.patches.BaseSettingsMenuPatch;
@SuppressWarnings("unused")
public final class SettingsMenuPatch extends BaseSettingsMenuPatch {
public static void hideSettingsMenu(PreferenceScreen mPreferenceScreen) {
if (mPreferenceScreen == null) return;
for (SettingsMenuComponent component : SettingsMenuComponent.values())
if (component.enabled)
removePreference(mPreferenceScreen, component.key);
}
public static boolean hideParentToolsMenu(boolean original) {
return !Settings.HIDE_SETTINGS_MENU_PARENT_TOOLS.get() && original;
}
private enum SettingsMenuComponent {
GENERAL("settings_header_general", Settings.HIDE_SETTINGS_MENU_GENERAL.get()),
PLAYBACK("settings_header_playback", Settings.HIDE_SETTINGS_MENU_PLAYBACK.get()),
DATA_SAVING("settings_header_data_saving", Settings.HIDE_SETTINGS_MENU_DATA_SAVING.get()),
DOWNLOADS_AND_STORAGE("settings_header_downloads_and_storage", Settings.HIDE_SETTINGS_MENU_DOWNLOADS_AND_STORAGE.get()),
NOTIFICATIONS("settings_header_notifications", Settings.HIDE_SETTINGS_MENU_NOTIFICATIONS.get()),
PRIVACY_AND_LOCATION("settings_header_privacy_and_location", Settings.HIDE_SETTINGS_MENU_PRIVACY_AND_LOCATION.get()),
RECOMMENDATIONS("settings_header_recommendations", Settings.HIDE_SETTINGS_MENU_RECOMMENDATIONS.get()),
PAID_MEMBERSHIPS("settings_header_paid_memberships", Settings.HIDE_SETTINGS_MENU_PAID_MEMBERSHIPS.get()),
ABOUT("settings_header_about_youtube_music", Settings.HIDE_SETTINGS_MENU_ABOUT.get());
private final String key;
private final boolean enabled;
SettingsMenuComponent(String key, boolean enabled) {
this.key = key;
this.enabled = enabled;
}
}
}

View File

@ -0,0 +1,11 @@
package app.revanced.extension.music.patches.misc;
import app.revanced.extension.music.settings.Settings;
@SuppressWarnings("unused")
public class CairoSplashAnimationPatch {
public static boolean disableCairoSplashAnimation(boolean original) {
return !Settings.DISABLE_CAIRO_SPLASH_ANIMATION.get() && original;
}
}

View File

@ -0,0 +1,11 @@
package app.revanced.extension.music.patches.misc;
import app.revanced.extension.music.settings.Settings;
@SuppressWarnings("unused")
public class OpusCodecPatch {
public static boolean enableOpusCodec() {
return Settings.ENABLE_OPUS_CODEC.get();
}
}

View File

@ -0,0 +1,41 @@
package app.revanced.extension.music.patches.misc;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import app.revanced.extension.music.patches.components.ShareSheetMenuFilter;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class ShareSheetPatch {
/**
* Injection point.
*/
public static void onShareSheetMenuCreate(final RecyclerView recyclerView) {
if (!Settings.CHANGE_SHARE_SHEET.get())
return;
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try {
if (!ShareSheetMenuFilter.isShareSheetMenuVisible)
return;
if (!(recyclerView.getChildAt(0) instanceof ViewGroup shareContainer)) {
return;
}
if (!(shareContainer.getChildAt(shareContainer.getChildCount() - 1) instanceof ViewGroup shareWithOtherAppsView)) {
return;
}
ShareSheetMenuFilter.isShareSheetMenuVisible = false;
recyclerView.setVisibility(View.GONE);
Utils.clickView(shareWithOtherAppsView);
} catch (Exception ex) {
Logger.printException(() -> "onShareSheetMenuCreate failure", ex);
}
});
}
}

View File

@ -0,0 +1,56 @@
package app.revanced.extension.music.patches.navigation;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.ResourceUtils;
@SuppressWarnings("unused")
public class NavigationPatch {
private static final int colorGrey12 =
ResourceUtils.getColor("revanced_color_grey_12");
public static Enum<?> lastPivotTab;
public static int enableBlackNavigationBar() {
return Settings.ENABLE_BLACK_NAVIGATION_BAR.get()
? Color.BLACK
: colorGrey12;
}
public static void hideNavigationLabel(TextView textview) {
hideViewUnderCondition(Settings.HIDE_NAVIGATION_LABEL.get(), textview);
}
public static void hideNavigationButton(@NonNull View view) {
if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) {
hideViewUnderCondition(true, (View) view.getParent());
return;
}
for (NavigationButton button : NavigationButton.values())
if (lastPivotTab.name().equals(button.name))
hideViewUnderCondition(button.enabled, view);
}
private enum NavigationButton {
HOME("TAB_HOME", Settings.HIDE_NAVIGATION_HOME_BUTTON.get()),
SAMPLES("TAB_SAMPLES", Settings.HIDE_NAVIGATION_SAMPLES_BUTTON.get()),
EXPLORE("TAB_EXPLORE", Settings.HIDE_NAVIGATION_EXPLORE_BUTTON.get()),
LIBRARY("LIBRARY_MUSIC", Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get()),
UPGRADE("TAB_MUSIC_PREMIUM", Settings.HIDE_NAVIGATION_UPGRADE_BUTTON.get());
private final boolean enabled;
private final String name;
NavigationButton(String name, boolean enabled) {
this.enabled = enabled;
this.name = name;
}
}
}

View File

@ -0,0 +1,198 @@
package app.revanced.extension.music.patches.player;
import static app.revanced.extension.shared.utils.Utils.hideViewByRemovingFromParentUnderCondition;
import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.annotation.SuppressLint;
import android.graphics.Color;
import android.view.View;
import java.util.Arrays;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoType;
import app.revanced.extension.music.utils.VideoUtils;
@SuppressWarnings({"unused"})
public class PlayerPatch {
private static final int MUSIC_VIDEO_GREY_BACKGROUND_COLOR = -12566464;
private static final int MUSIC_VIDEO_ORIGINAL_BACKGROUND_COLOR = -16579837;
@SuppressLint("StaticFieldLeak")
public static View previousButton;
@SuppressLint("StaticFieldLeak")
public static View nextButton;
public static boolean disableMiniPlayerGesture() {
return Settings.DISABLE_MINI_PLAYER_GESTURE.get();
}
public static boolean disablePlayerGesture() {
return Settings.DISABLE_PLAYER_GESTURE.get();
}
public static boolean enableColorMatchPlayer() {
return Settings.ENABLE_COLOR_MATCH_PLAYER.get();
}
public static int enableBlackPlayerBackground(int originalColor) {
return Settings.ENABLE_BLACK_PLAYER_BACKGROUND.get()
&& originalColor != MUSIC_VIDEO_GREY_BACKGROUND_COLOR
? Color.BLACK
: originalColor;
}
public static boolean enableForceMinimizedPlayer(boolean original) {
return Settings.ENABLE_FORCE_MINIMIZED_PLAYER.get() || original;
}
public static boolean enableMiniPlayerNextButton(boolean original) {
return !Settings.ENABLE_MINI_PLAYER_NEXT_BUTTON.get() && original;
}
public static View[] getViewArray(View[] oldViewArray) {
if (previousButton != null) {
if (nextButton != null) {
return getViewArray(getViewArray(oldViewArray, previousButton), nextButton);
} else {
return getViewArray(oldViewArray, previousButton);
}
} else {
return oldViewArray;
}
}
private static View[] getViewArray(View[] oldViewArray, View newView) {
final int oldViewArrayLength = oldViewArray.length;
View[] newViewArray = Arrays.copyOf(oldViewArray, oldViewArrayLength + 1);
newViewArray[oldViewArrayLength] = newView;
return newViewArray;
}
public static void setNextButton(View nextButtonView) {
if (nextButtonView == null)
return;
hideViewUnderCondition(
!Settings.ENABLE_MINI_PLAYER_NEXT_BUTTON.get(),
nextButtonView
);
nextButtonView.setOnClickListener(PlayerPatch::setNextButtonOnClickListener);
}
// rest of the implementation added by patch.
private static void setNextButtonOnClickListener(View view) {
if (Settings.ENABLE_MINI_PLAYER_NEXT_BUTTON.get())
view.getClass();
}
public static void setPreviousButton(View previousButtonView) {
if (previousButtonView == null)
return;
hideViewUnderCondition(
!Settings.ENABLE_MINI_PLAYER_PREVIOUS_BUTTON.get(),
previousButtonView
);
previousButtonView.setOnClickListener(PlayerPatch::setPreviousButtonOnClickListener);
}
// rest of the implementation added by patch.
private static void setPreviousButtonOnClickListener(View view) {
if (Settings.ENABLE_MINI_PLAYER_PREVIOUS_BUTTON.get())
view.getClass();
}
public static boolean enableSwipeToDismissMiniPlayer() {
return Settings.ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER.get();
}
public static boolean enableSwipeToDismissMiniPlayer(boolean original) {
return !Settings.ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER.get() && original;
}
public static Object enableSwipeToDismissMiniPlayer(Object object) {
return Settings.ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER.get() ? null : object;
}
public static int enableZenMode(int originalColor) {
if (Settings.ENABLE_ZEN_MODE.get() && originalColor == MUSIC_VIDEO_ORIGINAL_BACKGROUND_COLOR) {
if (Settings.ENABLE_ZEN_MODE_PODCAST.get() || !VideoType.getCurrent().isPodCast()) {
return MUSIC_VIDEO_GREY_BACKGROUND_COLOR;
}
}
return originalColor;
}
public static void hideAudioVideoSwitchToggle(View view, int originalVisibility) {
if (Settings.HIDE_AUDIO_VIDEO_SWITCH_TOGGLE.get()) {
originalVisibility = View.GONE;
}
view.setVisibility(originalVisibility);
}
public static void hideDoubleTapOverlayFilter(View view) {
hideViewByRemovingFromParentUnderCondition(Settings.HIDE_DOUBLE_TAP_OVERLAY_FILTER, view);
}
public static int hideFullscreenShareButton(int original) {
return Settings.HIDE_FULLSCREEN_SHARE_BUTTON.get() ? 0 : original;
}
public static void setShuffleState(Enum<?> shuffleState) {
if (!Settings.REMEMBER_SHUFFLE_SATE.get())
return;
Settings.ALWAYS_SHUFFLE.save(shuffleState.ordinal() == 1);
}
public static void shuffleTracks() {
if (!Settings.ALWAYS_SHUFFLE.get())
return;
VideoUtils.shuffleTracks();
}
public static boolean rememberRepeatState(boolean original) {
return Settings.REMEMBER_REPEAT_SATE.get() || original;
}
public static boolean rememberShuffleState() {
return Settings.REMEMBER_SHUFFLE_SATE.get();
}
public static boolean restoreOldCommentsPopUpPanels() {
return restoreOldCommentsPopUpPanels(true);
}
public static boolean restoreOldCommentsPopUpPanels(boolean original) {
if (!Settings.SETTINGS_INITIALIZED.get()) {
return original;
}
return !Settings.RESTORE_OLD_COMMENTS_POPUP_PANELS.get() && original;
}
public static boolean restoreOldPlayerBackground(boolean original) {
if (!Settings.SETTINGS_INITIALIZED.get()) {
return original;
}
if (!isSDKAbove(23)) {
// Disable this patch on Android 5.0 / 5.1 to fix a black play button.
// Android 5.x have a different design for play button,
// and if the new background is applied forcibly, the play button turns black.
// 6.20.51 uses the old background from the beginning, so there is no impact.
return original;
}
return !Settings.RESTORE_OLD_PLAYER_BACKGROUND.get();
}
public static boolean restoreOldPlayerLayout(boolean original) {
if (!Settings.SETTINGS_INITIALIZED.get()) {
return original;
}
return !Settings.RESTORE_OLD_PLAYER_LAYOUT.get();
}
}

View File

@ -0,0 +1,22 @@
package app.revanced.extension.music.patches.utils;
@SuppressWarnings("unused")
public class DrawableColorPatch {
private static final int[] DARK_VALUES = {
-14606047 // comments box background
};
public static int getColor(int originalValue) {
if (anyEquals(originalValue, DARK_VALUES))
return -16777215;
return originalValue;
}
private static boolean anyEquals(int value, int... of) {
for (int v : of) if (value == v) return true;
return false;
}
}

View File

@ -0,0 +1,34 @@
package app.revanced.extension.music.patches.utils;
import static app.revanced.extension.music.utils.RestartUtils.showRestartDialog;
import android.app.Activity;
import androidx.annotation.NonNull;
import app.revanced.extension.music.utils.ExtendedUtils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class InitializationPatch {
/**
* The new layout is not loaded normally when the app is first installed.
* (Also reproduced on unPatched YouTube Music)
* <p>
* To fix this, show the reboot dialog when the app is installed for the first time.
*/
public static void onCreate(@NonNull Activity mActivity) {
if (BaseSettings.SETTINGS_INITIALIZED.get())
return;
showRestartDialog(mActivity, "revanced_extended_restart_first_run", 3000);
Utils.runOnMainThreadDelayed(() -> BaseSettings.SETTINGS_INITIALIZED.save(true), 3000);
}
public static void setDeviceInformation(@NonNull Activity mActivity) {
ExtendedUtils.setApplicationLabel();
ExtendedUtils.setVersionName();
}
}

View File

@ -0,0 +1,12 @@
package app.revanced.extension.music.patches.utils;
@SuppressWarnings("unused")
public class PatchStatus {
public static boolean SpoofAppVersionDefaultBoolean() {
return false;
}
public static String SpoofAppVersionDefaultString() {
return "6.11.52";
}
}

View File

@ -0,0 +1,19 @@
package app.revanced.extension.music.patches.utils;
import androidx.annotation.Nullable;
import app.revanced.extension.music.shared.PlayerType;
@SuppressWarnings("unused")
public class PlayerTypeHookPatch {
/**
* Injection point.
*/
public static void setPlayerType(@Nullable Enum<?> musicPlayerType) {
if (musicPlayerType == null)
return;
PlayerType.setFromString(musicPlayerType.name());
}
}

View File

@ -0,0 +1,112 @@
package app.revanced.extension.music.patches.utils;
import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
import android.text.Spanned;
import androidx.annotation.Nullable;
import app.revanced.extension.music.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.shared.utils.Logger;
/**
* Handles all interaction of UI patch components.
* <p>
* Does not handle creating dislike spans or anything to do with {@link ReturnYouTubeDislikeApi}.
*/
@SuppressWarnings("unused")
public class ReturnYouTubeDislikePatch {
/**
* RYD data for the current video on screen.
*/
@Nullable
private static volatile ReturnYouTubeDislike currentVideoData;
public static void onRYDStatusChange(boolean rydEnabled) {
ReturnYouTubeDislikeApi.resetRateLimits();
// Must remove all values to protect against using stale data
// if the user enables RYD while a video is on screen.
clearData();
}
private static void clearData() {
currentVideoData = null;
}
/**
* Injection point
* <p>
* Called when a Shorts dislike Spannable is created
*/
public static Spanned onSpannedCreated(Spanned original) {
try {
if (original == null) {
return null;
}
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
return original; // User enabled RYD while a video was on screen.
}
return videoData.getDislikesSpan(original);
} catch (Exception ex) {
Logger.printException(() -> "onSpannedCreated failure", ex);
}
return original;
}
/**
* Injection point.
*/
public static void newVideoLoaded(@Nullable String videoId) {
try {
if (!Settings.RYD_ENABLED.get()) {
return;
}
if (videoId == null || videoId.isEmpty()) {
return;
}
if (videoIdIsSame(currentVideoData, videoId)) {
return;
}
currentVideoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
} catch (Exception ex) {
Logger.printException(() -> "newVideoLoaded failure", ex);
}
}
private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) {
return (fetch == null && videoId == null)
|| (fetch != null && fetch.getVideoId().equals(videoId));
}
/**
* Injection point.
* <p>
* Called when the user likes or dislikes.
*/
public static void sendVote(int vote) {
try {
if (!Settings.RYD_ENABLED.get()) {
return;
}
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
Logger.printDebug(() -> "Cannot send vote, as current video data is null");
return; // User enabled RYD while a regular video was minimized.
}
for (Vote v : Vote.values()) {
if (v.value == vote) {
videoData.sendVote(v);
return;
}
}
Logger.printException(() -> "Unknown vote type: " + vote);
} catch (Exception ex) {
Logger.printException(() -> "sendVote failure", ex);
}
}
}

View File

@ -0,0 +1,19 @@
package app.revanced.extension.music.patches.utils;
import androidx.annotation.Nullable;
import app.revanced.extension.music.shared.VideoType;
@SuppressWarnings("unused")
public class VideoTypeHookPatch {
/**
* Injection point.
*/
public static void setVideoType(@Nullable Enum<?> musicVideoType) {
if (musicVideoType == null)
return;
VideoType.setFromString(musicVideoType.name());
}
}

View File

@ -0,0 +1,94 @@
package app.revanced.extension.music.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
import java.util.Arrays;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class CustomPlaybackSpeedPatch {
/**
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
*/
private static final float MAXIMUM_PLAYBACK_SPEED = 5;
/**
* Custom playback speeds.
*/
private static float[] customPlaybackSpeeds;
static {
loadCustomSpeeds();
}
/**
* Injection point.
*/
public static float[] getArray(float[] original) {
return userChangedCustomPlaybackSpeed() ? customPlaybackSpeeds : original;
}
/**
* Injection point.
*/
public static int getLength(int original) {
return userChangedCustomPlaybackSpeed() ? customPlaybackSpeeds.length : original;
}
/**
* Injection point.
*/
public static int getSize(int original) {
return userChangedCustomPlaybackSpeed() ? 0 : original;
}
private static void resetCustomSpeeds(@NonNull String toastMessage) {
Utils.showToastLong(toastMessage);
Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
}
public static void loadCustomSpeeds() {
try {
String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+");
Arrays.sort(speedStrings);
if (speedStrings.length == 0) {
throw new IllegalArgumentException();
}
customPlaybackSpeeds = new float[speedStrings.length];
for (int i = 0, length = speedStrings.length; i < length; i++) {
final float speed = Float.parseFloat(speedStrings[i]);
if (speed <= 0 || arrayContains(customPlaybackSpeeds, speed)) {
throw new IllegalArgumentException();
}
if (speed > MAXIMUM_PLAYBACK_SPEED) {
resetCustomSpeeds(str("revanced_custom_playback_speeds_invalid", MAXIMUM_PLAYBACK_SPEED + ""));
loadCustomSpeeds();
return;
}
customPlaybackSpeeds[i] = speed;
}
} catch (Exception ex) {
Logger.printInfo(() -> "parse error", ex);
resetCustomSpeeds(str("revanced_custom_playback_speeds_parse_exception"));
loadCustomSpeeds();
}
}
private static boolean userChangedCustomPlaybackSpeed() {
return !Settings.CUSTOM_PLAYBACK_SPEEDS.isSetToDefault() && customPlaybackSpeeds != null;
}
private static boolean arrayContains(float[] array, float value) {
for (float arrayValue : array) {
if (arrayValue == value) return true;
}
return false;
}
}

View File

@ -0,0 +1,32 @@
package app.revanced.extension.music.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.showToastShort;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class PlaybackSpeedPatch {
public static float getPlaybackSpeed(final float playbackSpeed) {
try {
return Settings.DEFAULT_PLAYBACK_SPEED.get();
} catch (Exception ex) {
Logger.printException(() -> "Failed to getPlaybackSpeed", ex);
}
return playbackSpeed;
}
public static void userSelectedPlaybackSpeed(final float playbackSpeed) {
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get())
return;
Settings.DEFAULT_PLAYBACK_SPEED.save(playbackSpeed);
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get())
return;
showToastShort(str("revanced_remember_playback_speed_toast", playbackSpeed + "x"));
}
}

View File

@ -0,0 +1,68 @@
package app.revanced.extension.music.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class VideoQualityPatch {
private static final int DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY = -2;
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
/**
* Injection point.
*/
public static void newVideoStarted(final String ignoredVideoId) {
final int preferredQuality =
Utils.getNetworkType() == Utils.NetworkType.MOBILE
? mobileQualitySetting.get()
: wifiQualitySetting.get();
if (preferredQuality == DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY)
return;
Utils.runOnMainThreadDelayed(() ->
VideoInformation.overrideVideoQuality(
VideoInformation.getAvailableVideoQuality(preferredQuality)
),
500
);
}
/**
* Injection point.
*/
public static void userSelectedVideoQuality() {
Utils.runOnMainThreadDelayed(() ->
userSelectedVideoQuality(VideoInformation.getVideoQuality()),
300
);
}
private static void userSelectedVideoQuality(final int defaultQuality) {
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get())
return;
if (defaultQuality == DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY)
return;
final Utils.NetworkType networkType = Utils.getNetworkType();
switch (networkType) {
case NONE -> {
Utils.showToastShort(str("revanced_remember_video_quality_none"));
return;
}
case MOBILE -> mobileQualitySetting.save(defaultQuality);
default -> wifiQualitySetting.save(defaultQuality);
}
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
return;
Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p"));
}
}

View File

@ -0,0 +1,564 @@
package app.revanced.extension.music.returnyoutubedislike;
import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.graphics.drawable.shapes.RectShape;
import android.icu.text.CompactDecimalFormat;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.returnyoutubedislike.requests.RYDVoteData;
import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
/**
* Because Litho creates spans using multiple threads, this entire class supports multithreading as well.
*/
public class ReturnYouTubeDislike {
/**
* Maximum amount of time to block the UI from updates while waiting for network call to complete.
* <p>
* Must be less than 5 seconds, as per:
* <a href="https://developer.android.com/topic/performance/vitals/anr"/>
*/
private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000;
/**
* How long to retain successful RYD fetches.
*/
private static final long CACHE_TIMEOUT_SUCCESS_MILLISECONDS = 7 * 60 * 1000; // 7 Minutes
/**
* How long to retain unsuccessful RYD fetches,
* and also the minimum time before retrying again.
*/
private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 3 * 60 * 1000; // 3 Minutes
/**
* Unique placeholder character, used to detect if a segmented span already has dislikes added to it.
* Can be any almost any non-visible character.
*/
private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye'
private static final int SEPARATOR_COLOR = 872415231;
/**
* Cached lookup of all video ids.
*/
@GuardedBy("itself")
private static final Map<String, ReturnYouTubeDislike> fetchCache = new HashMap<>();
/**
* Used to send votes, one by one, in the same order the user created them.
*/
private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor();
/**
* For formatting dislikes as number.
*/
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
private static CompactDecimalFormat dislikeCountFormatter;
/**
* For formatting dislikes as percentage.
*/
@GuardedBy("ReturnYouTubeDislike.class")
private static NumberFormat dislikePercentageFormatter;
public static final Rect leftSeparatorBounds;
private static final Rect middleSeparatorBounds;
static {
DisplayMetrics dp = Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics();
leftSeparatorBounds = new Rect(0, 0,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp),
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, dp));
final int middleSeparatorSize =
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get();
}
private final String videoId;
/**
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes.
* Absolutely cannot be holding any lock during calls to {@link Future#get()}.
*/
private final Future<RYDVoteData> future;
/**
* Time this instance and the fetch future was created.
*/
private final long timeFetched;
/**
* Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing.
*/
@Nullable
@GuardedBy("this")
private Vote userVote;
/**
* Original dislike span, before modifications.
*/
@Nullable
@GuardedBy("this")
private Spanned originalDislikeSpan;
/**
* Replacement like/dislike span that includes formatted dislikes.
* Used to prevent recreating the same span multiple times.
*/
@Nullable
@GuardedBy("this")
private SpannableString replacementLikeDislikeSpan;
@NonNull
private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable,
@NonNull RYDVoteData voteData) {
CharSequence oldLikes = oldSpannable;
// YouTube creators can hide the like count on a video,
// and the like count appears as a device language specific string that says 'Like'.
// Check if the string contains any numbers.
if (!Utils.containsNumber(oldLikes)) {
if (Settings.RYD_ESTIMATED_LIKE.get()) {
// Likes are hidden by video creator
//
// RYD does not directly provide like data, but can use an estimated likes
// using the same scale factor RYD applied to the raw dislikes.
//
// example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
Logger.printDebug(() -> "Using estimated likes");
oldLikes = formatDislikeCount(voteData.getLikeCount());
} else {
// Change the "Likes" string to show that likes and dislikes are hidden.
String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
}
}
SpannableStringBuilder builder = new SpannableStringBuilder("\u2009\u2009");
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
if (!compactLayout) {
String leftSeparatorString = "\u200E "; // u200E = left to right character
Spannable leftSeparatorSpan = new SpannableString(leftSeparatorString);
ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape());
shapeDrawable.getPaint().setColor(SEPARATOR_COLOR);
shapeDrawable.setBounds(leftSeparatorBounds);
leftSeparatorSpan.setSpan(new VerticallyCenteredImageSpan(shapeDrawable), 1, 2,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE); // drawable cannot overwrite RTL or LTR character
builder.append(leftSeparatorSpan);
}
// likes
builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikes));
// middle separator
String middleSeparatorString = compactLayout
? "\u200E " + MIDDLE_SEPARATOR_CHARACTER + " "
: "\u200E \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character
final int shapeInsertionIndex = middleSeparatorString.length() / 2;
Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString);
ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape());
shapeDrawable.getPaint().setColor(SEPARATOR_COLOR);
shapeDrawable.setBounds(middleSeparatorBounds);
// Use original text width if using Rolling Number,
// to ensure the replacement styled span has the same width as the measured String,
// otherwise layout can be broken (especially on devices with small system font sizes).
middleSeparatorSpan.setSpan(
new VerticallyCenteredImageSpan(shapeDrawable),
shapeInsertionIndex, shapeInsertionIndex + 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
builder.append(middleSeparatorSpan);
// dislikes
builder.append(newSpannableWithDislikes(oldSpannable, voteData));
return new SpannableString(builder);
}
/**
* @return If the text is likely for a previously created likes/dislikes segmented span.
*/
public static boolean isPreviouslyCreatedSegmentedSpan(@NonNull String text) {
return text.indexOf(MIDDLE_SEPARATOR_CHARACTER) >= 0;
}
private static boolean spansHaveEqualTextAndColor(@NonNull Spanned one, @NonNull Spanned two) {
// Cannot use equals on the span, because many of the inner styling spans do not implement equals.
// Instead, compare the underlying text and the text color to handle when dark mode is changed.
// Cannot compare the status of device dark mode, as Litho components are updated just before dark mode status changes.
if (!one.toString().equals(two.toString())) {
return false;
}
ForegroundColorSpan[] oneColors = one.getSpans(0, one.length(), ForegroundColorSpan.class);
ForegroundColorSpan[] twoColors = two.getSpans(0, two.length(), ForegroundColorSpan.class);
final int oneLength = oneColors.length;
if (oneLength != twoColors.length) {
return false;
}
for (int i = 0; i < oneLength; i++) {
if (oneColors[i].getForegroundColor() != twoColors[i].getForegroundColor()) {
return false;
}
}
return true;
}
private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
return newSpanUsingStylingOfAnotherSpan(sourceStyling,
Settings.RYD_DISLIKE_PERCENTAGE.get()
? formatDislikePercentage(voteData.getDislikePercentage())
: formatDislikeCount(voteData.getDislikeCount()));
}
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
SpannableString destination = new SpannableString(newSpanText);
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
for (Object span : spans) {
destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span));
}
return destination;
}
private static String formatDislikeCount(long dislikeCount) {
if (isSDKAbove(24)) {
if (dislikeCountFormatter == null) {
// Note: Java number formatters will use the locale specific number characters.
// such as Arabic which formats "1.234" into "۱,۲۳٤"
// But YouTube disregards locale specific number characters
// and instead shows english number characters everywhere.
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0);
Logger.printDebug(() -> "Locale: " + locale);
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
}
return dislikeCountFormatter.format(dislikeCount);
} else {
return String.valueOf(dislikeCount);
}
}
private static String formatDislikePercentage(float dislikePercentage) {
if (isSDKAbove(24)) {
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
if (dislikePercentageFormatter == null) {
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0);
Logger.printDebug(() -> "Locale: " + locale);
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
}
if (dislikePercentage >= 0.01) { // at least 1%
dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
} else {
dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
}
return dislikePercentageFormatter.format(dislikePercentage);
}
} else {
return String.valueOf((int) (dislikePercentage * 100));
}
}
@NonNull
public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) {
Objects.requireNonNull(videoId);
synchronized (fetchCache) {
// Remove any expired entries.
final long now = System.currentTimeMillis();
if (isSDKAbove(24)) {
fetchCache.values().removeIf(value -> {
final boolean expired = value.isExpired(now);
if (expired)
Logger.printDebug(() -> "Removing expired fetch: " + value.videoId);
return expired;
});
} else {
final Iterator<Map.Entry<String, ReturnYouTubeDislike>> itr = fetchCache.entrySet().iterator();
while (itr.hasNext()) {
final Map.Entry<String, ReturnYouTubeDislike> entry = itr.next();
if (entry.getValue().isExpired(now)) {
Logger.printDebug(() -> "Removing expired fetch: " + entry.getValue().videoId);
itr.remove();
}
}
}
ReturnYouTubeDislike fetch = fetchCache.get(videoId);
if (fetch == null) {
fetch = new ReturnYouTubeDislike(videoId);
fetchCache.put(videoId, fetch);
}
return fetch;
}
}
/**
* Should be called if the user changes dislikes appearance settings.
*/
public static void clearAllUICaches() {
synchronized (fetchCache) {
for (ReturnYouTubeDislike fetch : fetchCache.values()) {
fetch.clearUICache();
}
}
}
private ReturnYouTubeDislike(@NonNull String videoId) {
this.videoId = Objects.requireNonNull(videoId);
this.timeFetched = System.currentTimeMillis();
this.future = Utils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
}
private boolean isExpired(long now) {
final long timeSinceCreation = now - timeFetched;
if (timeSinceCreation < CACHE_TIMEOUT_FAILURE_MILLISECONDS) {
return false; // Not expired, even if the API call failed.
}
if (timeSinceCreation > CACHE_TIMEOUT_SUCCESS_MILLISECONDS) {
return true; // Always expired.
}
// Only expired if the fetch failed (API null response).
return (!fetchCompleted() || getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH) == null);
}
@Nullable
public RYDVoteData getFetchData(long maxTimeToWait) {
try {
return future.get(maxTimeToWait, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Logger.printDebug(() -> "Waited but future was not complete after: " + maxTimeToWait + "ms");
} catch (ExecutionException | InterruptedException ex) {
Logger.printException(() -> "Future failure ", ex); // will never happen
}
return null;
}
/**
* @return if the RYD fetch call has completed.
*/
public boolean fetchCompleted() {
return future.isDone();
}
private synchronized void clearUICache() {
if (replacementLikeDislikeSpan != null) {
Logger.printDebug(() -> "Clearing replacement span for: " + videoId);
}
replacementLikeDislikeSpan = null;
}
/**
* Must call off main thread, as this will make a network call if user is not yet registered.
*
* @return ReturnYouTubeDislike user ID. If user registration has never happened
* and the network call fails, this returns NULL.
*/
@Nullable
private static String getUserId() {
Utils.verifyOffMainThread();
String userId = Settings.RYD_USER_ID.get();
if (!userId.isEmpty()) {
return userId;
}
userId = ReturnYouTubeDislikeApi.registerAsNewUser();
if (userId != null) {
Settings.RYD_USER_ID.save(userId);
}
return userId;
}
@NonNull
public String getVideoId() {
return videoId;
}
/**
* @return the replacement span containing dislikes, or the original span if RYD is not available.
*/
@NonNull
public synchronized Spanned getDislikesSpan(@NonNull Spanned original) {
return waitForFetchAndUpdateReplacementSpan(original);
}
@NonNull
private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original) {
try {
RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
if (votingData == null) {
Logger.printDebug(() -> "Cannot add dislike to UI (RYD data not available)");
return original;
}
synchronized (this) {
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) {
if (spansHaveEqualTextAndColor(original, replacementLikeDislikeSpan)) {
Logger.printDebug(() -> "Ignoring previously created dislikes span of data: " + videoId);
return original;
}
if (spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
return replacementLikeDislikeSpan;
}
}
if (isPreviouslyCreatedSegmentedSpan(original.toString())) {
// need to recreate using original, as original has prior outdated dislike values
if (originalDislikeSpan == null) {
// Should never happen.
Logger.printDebug(() -> "Cannot add dislikes - original span is null. videoId: " + videoId);
return original;
}
original = originalDislikeSpan;
}
// No replacement span exist, create it now.
if (userVote != null) {
votingData.updateUsingVote(userVote);
}
originalDislikeSpan = original;
replacementLikeDislikeSpan = createDislikeSpan(original, votingData);
Logger.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '"
+ replacementLikeDislikeSpan + "'" + " using video: " + videoId);
return replacementLikeDislikeSpan;
}
} catch (Exception e) {
Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen
}
return original;
}
public void sendVote(@NonNull Vote vote) {
Utils.verifyOnMainThread();
Objects.requireNonNull(vote);
try {
setUserVote(vote);
voteSerialExecutor.execute(() -> {
try { // Must wrap in try/catch to properly log exceptions.
ReturnYouTubeDislikeApi.sendVote(getUserId(), videoId, vote);
} catch (Exception ex) {
Logger.printException(() -> "Failed to send vote", ex);
}
});
} catch (Exception ex) {
Logger.printException(() -> "Error trying to send vote", ex);
}
}
/**
* Sets the current user vote value, and does not send the vote to the RYD API.
* <p>
* Only used to set value if thumbs up/down is already selected on video load.
*/
public void setUserVote(@NonNull Vote vote) {
Objects.requireNonNull(vote);
try {
Logger.printDebug(() -> "setUserVote: " + vote);
synchronized (this) {
userVote = vote;
clearUICache();
}
if (future.isDone()) {
// Update the fetched vote data.
RYDVoteData voteData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
if (voteData == null) {
// RYD fetch failed.
Logger.printDebug(() -> "Cannot update UI (vote data not available)");
return;
}
voteData.updateUsingVote(vote);
} // Else, vote will be applied after fetch completes.
} catch (Exception ex) {
Logger.printException(() -> "setUserVote failure", ex);
}
}
}
/**
* Vertically centers a Spanned Drawable.
*/
class VerticallyCenteredImageSpan extends ImageSpan {
public VerticallyCenteredImageSpan(Drawable drawable) {
super(drawable);
}
@Override
public int getSize(@NonNull Paint paint, @NonNull CharSequence text,
int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) {
Drawable drawable = getDrawable();
Rect bounds = drawable.getBounds();
if (fontMetrics != null) {
Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt();
final int fontHeight = paintMetrics.descent - paintMetrics.ascent;
final int drawHeight = bounds.bottom - bounds.top;
final int halfDrawHeight = drawHeight / 2;
final int yCenter = paintMetrics.ascent + fontHeight / 2;
fontMetrics.ascent = yCenter - halfDrawHeight;
fontMetrics.top = fontMetrics.ascent;
fontMetrics.bottom = yCenter + halfDrawHeight;
fontMetrics.descent = fontMetrics.bottom;
}
return bounds.right;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, @NonNull Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt();
final int fontHeight = paintMetrics.descent - paintMetrics.ascent;
final int yCenter = y + paintMetrics.descent - fontHeight / 2;
final Rect drawBounds = drawable.getBounds();
final int translateY = yCenter - (drawBounds.bottom - drawBounds.top) / 2;
canvas.translate(x, translateY);
drawable.draw(canvas);
canvas.restore();
}
}

View File

@ -0,0 +1,80 @@
package app.revanced.extension.music.settings;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
import app.revanced.extension.music.settings.preference.ReVancedPreferenceFragment;
import app.revanced.extension.shared.utils.Logger;
/**
* @noinspection ALL
*/
public class ActivityHook {
private static WeakReference<Activity> activityRef = new WeakReference<>(null);
public static Activity getActivity() {
return activityRef.get();
}
/**
* Injection point.
*
* @param object object is usually Activity, but sometimes object cannot be cast to Activity.
* Check whether object can be cast as Activity for a safe hook.
*/
public static void setActivity(@NonNull Object object) {
if (object instanceof Activity mActivity) {
activityRef = new WeakReference<>(mActivity);
}
}
/**
* Injection point.
*
* @param baseActivity Activity containing intent data.
* It should be finished immediately after obtaining the dataString.
* @return Whether or not dataString is included.
*/
public static boolean initialize(@NonNull Activity baseActivity) {
try {
final Intent baseActivityIntent = baseActivity.getIntent();
if (baseActivityIntent == null)
return false;
// If we do not finish the activity immediately, the YT Music logo will remain on the screen.
baseActivity.finish();
String dataString = baseActivityIntent.getDataString();
if (dataString == null || dataString.isEmpty())
return false;
// Checks whether dataString contains settings that use Intent.
if (!Settings.includeWithIntent(dataString))
return false;
// Save intent data in settings activity.
Activity mActivity = activityRef.get();
Intent intent = mActivity.getIntent();
intent.setData(Uri.parse(dataString));
mActivity.setIntent(intent);
// Starts a new PreferenceFragment to handle activities freely.
mActivity.getFragmentManager()
.beginTransaction()
.add(new ReVancedPreferenceFragment(), "")
.commit();
return true;
} catch (Exception ex) {
Logger.printException(() -> "initializeSettings failure", ex);
}
return false;
}
}

View File

@ -0,0 +1,251 @@
package app.revanced.extension.music.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
import androidx.annotation.NonNull;
import app.revanced.extension.music.patches.utils.PatchStatus;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.settings.LongSetting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class Settings extends BaseSettings {
// PreferenceScreen: Account
public static final BooleanSetting HIDE_ACCOUNT_MENU = new BooleanSetting("revanced_hide_account_menu", FALSE);
public static final StringSetting HIDE_ACCOUNT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_account_menu_filter_strings", "");
public static final BooleanSetting HIDE_ACCOUNT_MENU_EMPTY_COMPONENT = new BooleanSetting("revanced_hide_account_menu_empty_component", FALSE);
public static final BooleanSetting HIDE_HANDLE = new BooleanSetting("revanced_hide_handle", TRUE, true);
public static final BooleanSetting HIDE_TERMS_CONTAINER = new BooleanSetting("revanced_hide_terms_container", FALSE);
// PreferenceScreen: Action Bar
public static final BooleanSetting HIDE_ACTION_BUTTON_LIKE_DISLIKE = new BooleanSetting("revanced_hide_action_button_like_dislike", FALSE, true);
public static final BooleanSetting HIDE_ACTION_BUTTON_COMMENT = new BooleanSetting("revanced_hide_action_button_comment", FALSE, true);
public static final BooleanSetting HIDE_ACTION_BUTTON_ADD_TO_PLAYLIST = new BooleanSetting("revanced_hide_action_button_add_to_playlist", FALSE, true);
public static final BooleanSetting HIDE_ACTION_BUTTON_DOWNLOAD = new BooleanSetting("revanced_hide_action_button_download", FALSE, true);
public static final BooleanSetting HIDE_ACTION_BUTTON_SHARE = new BooleanSetting("revanced_hide_action_button_share", FALSE, true);
public static final BooleanSetting HIDE_ACTION_BUTTON_RADIO = new BooleanSetting("revanced_hide_action_button_radio", FALSE, true);
public static final BooleanSetting HIDE_ACTION_BUTTON_LABEL = new BooleanSetting("revanced_hide_action_button_label", FALSE, true);
public static final BooleanSetting EXTERNAL_DOWNLOADER_ACTION_BUTTON = new BooleanSetting("revanced_external_downloader_action", FALSE, true);
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME = new StringSetting("revanced_external_downloader_package_name", "com.deniscerri.ytdl");
// PreferenceScreen: Ads
public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE, true);
public static final BooleanSetting HIDE_MUSIC_ADS = new BooleanSetting("revanced_hide_music_ads", TRUE, true);
public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE, true);
public static final BooleanSetting HIDE_PREMIUM_PROMOTION = new BooleanSetting("revanced_hide_premium_promotion", TRUE, true);
public static final BooleanSetting HIDE_PREMIUM_RENEWAL = new BooleanSetting("revanced_hide_premium_renewal", TRUE, true);
// PreferenceScreen: Flyout menu
public static final BooleanSetting ENABLE_COMPACT_DIALOG = new BooleanSetting("revanced_enable_compact_dialog", TRUE);
public static final BooleanSetting ENABLE_TRIM_SILENCE = new BooleanSetting("revanced_enable_trim_silence", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_LIKE_DISLIKE = new BooleanSetting("revanced_hide_flyout_menu_like_dislike", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_3_COLUMN_COMPONENT = new BooleanSetting("revanced_hide_flyout_menu_3_column_component", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_ADD_TO_QUEUE = new BooleanSetting("revanced_hide_flyout_menu_add_to_queue", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_CAPTIONS = new BooleanSetting("revanced_hide_flyout_menu_captions", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_DELETE_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_delete_playlist", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_DISMISS_QUEUE = new BooleanSetting("revanced_hide_flyout_menu_dismiss_queue", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_DOWNLOAD = new BooleanSetting("revanced_hide_flyout_menu_download", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_EDIT_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_edit_playlist", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_ALBUM = new BooleanSetting("revanced_hide_flyout_menu_go_to_album", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_ARTIST = new BooleanSetting("revanced_hide_flyout_menu_go_to_artist", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_EPISODE = new BooleanSetting("revanced_hide_flyout_menu_go_to_episode", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_PODCAST = new BooleanSetting("revanced_hide_flyout_menu_go_to_podcast", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_HELP = new BooleanSetting("revanced_hide_flyout_menu_help", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_PLAY_NEXT = new BooleanSetting("revanced_hide_flyout_menu_play_next", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_QUALITY = new BooleanSetting("revanced_hide_flyout_menu_quality", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_REMOVE_FROM_LIBRARY = new BooleanSetting("revanced_hide_flyout_menu_remove_from_library", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_REMOVE_FROM_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_remove_from_playlist", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_hide_flyout_menu_report", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_SAVE_EPISODE_FOR_LATER = new BooleanSetting("revanced_hide_flyout_menu_save_episode_for_later", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_SAVE_TO_LIBRARY = new BooleanSetting("revanced_hide_flyout_menu_save_to_library", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_SAVE_TO_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_save_to_playlist", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_SHARE = new BooleanSetting("revanced_hide_flyout_menu_share", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_SHUFFLE_PLAY = new BooleanSetting("revanced_hide_flyout_menu_shuffle_play", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_SLEEP_TIMER = new BooleanSetting("revanced_hide_flyout_menu_sleep_timer", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_START_RADIO = new BooleanSetting("revanced_hide_flyout_menu_start_radio", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_STATS_FOR_NERDS = new BooleanSetting("revanced_hide_flyout_menu_stats_for_nerds", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_SUBSCRIBE = new BooleanSetting("revanced_hide_flyout_menu_subscribe", FALSE, true);
public static final BooleanSetting HIDE_FLYOUT_MENU_VIEW_SONG_CREDIT = new BooleanSetting("revanced_hide_flyout_menu_view_song_credit", FALSE, true);
public static final BooleanSetting REPLACE_FLYOUT_MENU_DISMISS_QUEUE = new BooleanSetting("revanced_replace_flyout_menu_dismiss_queue", FALSE, true);
public static final BooleanSetting REPLACE_FLYOUT_MENU_DISMISS_QUEUE_CONTINUE_WATCH = new BooleanSetting("revanced_replace_flyout_menu_dismiss_queue_continue_watch", TRUE);
public static final BooleanSetting REPLACE_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_replace_flyout_menu_report", TRUE, true);
public static final BooleanSetting REPLACE_FLYOUT_MENU_REPORT_ONLY_PLAYER = new BooleanSetting("revanced_replace_flyout_menu_report_only_player", TRUE, true);
// PreferenceScreen: General
public static final StringSetting CHANGE_START_PAGE = new StringSetting("revanced_change_start_page", "FEmusic_home", true);
public static final BooleanSetting DISABLE_DISLIKE_REDIRECTION = new BooleanSetting("revanced_disable_dislike_redirection", FALSE);
public static final BooleanSetting ENABLE_LANDSCAPE_MODE = new BooleanSetting("revanced_enable_landscape_mode", FALSE, true);
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);
public static final BooleanSetting HIDE_BUTTON_SHELF = new BooleanSetting("revanced_hide_button_shelf", FALSE, true);
public static final BooleanSetting HIDE_CAROUSEL_SHELF = new BooleanSetting("revanced_hide_carousel_shelf", FALSE, true);
public static final BooleanSetting HIDE_PLAYLIST_CARD_SHELF = new BooleanSetting("revanced_hide_playlist_card_shelf", FALSE, true);
public static final BooleanSetting HIDE_SAMPLE_SHELF = new BooleanSetting("revanced_hide_samples_shelf", FALSE, true);
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE);
public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_hide_category_bar", FALSE, true);
public static final BooleanSetting HIDE_FLOATING_BUTTON = new BooleanSetting("revanced_hide_floating_button", FALSE, true);
public static final BooleanSetting HIDE_TAP_TO_UPDATE_BUTTON = new BooleanSetting("revanced_hide_tap_to_update_button", FALSE, true);
public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_hide_history_button", FALSE);
public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_notification_button", FALSE, true);
public static final BooleanSetting HIDE_SOUND_SEARCH_BUTTON = new BooleanSetting("revanced_hide_sound_search_button", FALSE, true);
public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true);
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE);
public static final BooleanSetting RESTORE_OLD_STYLE_LIBRARY_SHELF = new BooleanSetting("revanced_restore_old_style_library_shelf", FALSE, true);
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version",
PatchStatus.SpoofAppVersionDefaultBoolean(), true);
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target",
PatchStatus.SpoofAppVersionDefaultString(), true);
// PreferenceScreen: Navigation bar
public static final BooleanSetting ENABLE_BLACK_NAVIGATION_BAR = new BooleanSetting("revanced_enable_black_navigation_bar", TRUE);
public static final BooleanSetting HIDE_NAVIGATION_HOME_BUTTON = new BooleanSetting("revanced_hide_navigation_home_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_SAMPLES_BUTTON = new BooleanSetting("revanced_hide_navigation_samples_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_EXPLORE_BUTTON = new BooleanSetting("revanced_hide_navigation_explore_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_LIBRARY_BUTTON = new BooleanSetting("revanced_hide_navigation_library_button", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_UPGRADE_BUTTON = new BooleanSetting("revanced_hide_navigation_upgrade_button", TRUE, true);
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true);
// PreferenceScreen: Player
public static final BooleanSetting DISABLE_MINI_PLAYER_GESTURE = new BooleanSetting("revanced_disable_mini_player_gesture", FALSE, true);
public static final BooleanSetting DISABLE_PLAYER_GESTURE = new BooleanSetting("revanced_disable_player_gesture", FALSE, true);
public static final BooleanSetting ENABLE_BLACK_PLAYER_BACKGROUND = new BooleanSetting("revanced_enable_black_player_background", FALSE, true);
public static final BooleanSetting ENABLE_COLOR_MATCH_PLAYER = new BooleanSetting("revanced_enable_color_match_player", TRUE);
public static final BooleanSetting ENABLE_FORCE_MINIMIZED_PLAYER = new BooleanSetting("revanced_enable_force_minimized_player", TRUE);
public static final BooleanSetting ENABLE_MINI_PLAYER_NEXT_BUTTON = new BooleanSetting("revanced_enable_mini_player_next_button", TRUE, true);
public static final BooleanSetting ENABLE_MINI_PLAYER_PREVIOUS_BUTTON = new BooleanSetting("revanced_enable_mini_player_previous_button", TRUE, true);
public static final BooleanSetting ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER = new BooleanSetting("revanced_enable_swipe_to_dismiss_mini_player", TRUE, true);
public static final BooleanSetting ENABLE_ZEN_MODE = new BooleanSetting("revanced_enable_zen_mode", FALSE);
public static final BooleanSetting ENABLE_ZEN_MODE_PODCAST = new BooleanSetting("revanced_enable_zen_mode_podcast", FALSE);
public static final BooleanSetting HIDE_AUDIO_VIDEO_SWITCH_TOGGLE = new BooleanSetting("revanced_hide_audio_video_switch_toggle", FALSE, true);
public static final BooleanSetting HIDE_COMMENT_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_comment_channel_guidelines", TRUE);
public static final BooleanSetting HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comment_timestamp_and_emoji_buttons", FALSE);
public static final BooleanSetting HIDE_DOUBLE_TAP_OVERLAY_FILTER = new BooleanSetting("revanced_hide_double_tap_overlay_filter", FALSE, true);
public static final BooleanSetting HIDE_FULLSCREEN_SHARE_BUTTON = new BooleanSetting("revanced_hide_fullscreen_share_button", FALSE, true);
public static final BooleanSetting REMEMBER_REPEAT_SATE = new BooleanSetting("revanced_remember_repeat_state", TRUE);
public static final BooleanSetting REMEMBER_SHUFFLE_SATE = new BooleanSetting("revanced_remember_shuffle_state", TRUE);
public static final BooleanSetting ALWAYS_SHUFFLE = new BooleanSetting("revanced_always_shuffle", FALSE);
public static final BooleanSetting RESTORE_OLD_COMMENTS_POPUP_PANELS = new BooleanSetting("revanced_restore_old_comments_popup_panels", FALSE, true);
public static final BooleanSetting RESTORE_OLD_PLAYER_BACKGROUND = new BooleanSetting("revanced_restore_old_player_background", FALSE, true);
public static final BooleanSetting RESTORE_OLD_PLAYER_LAYOUT = new BooleanSetting("revanced_restore_old_player_layout", FALSE, true);
// PreferenceScreen: Settings menu
public static final BooleanSetting HIDE_SETTINGS_MENU_PARENT_TOOLS = new BooleanSetting("revanced_hide_settings_menu_parent_tools", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_GENERAL = new BooleanSetting("revanced_hide_settings_menu_general", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_PLAYBACK = new BooleanSetting("revanced_hide_settings_menu_playback", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_DATA_SAVING = new BooleanSetting("revanced_hide_settings_menu_data_saving", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_DOWNLOADS_AND_STORAGE = new BooleanSetting("revanced_hide_settings_menu_downloads_and_storage", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_NOTIFICATIONS = new BooleanSetting("revanced_hide_settings_menu_notification", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_PRIVACY_AND_LOCATION = new BooleanSetting("revanced_hide_settings_menu_privacy_and_location", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_RECOMMENDATIONS = new BooleanSetting("revanced_hide_settings_menu_recommendations", FALSE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_PAID_MEMBERSHIPS = new BooleanSetting("revanced_hide_settings_menu_paid_memberships", TRUE, true);
public static final BooleanSetting HIDE_SETTINGS_MENU_ABOUT = new BooleanSetting("revanced_hide_settings_menu_about", FALSE, true);
// PreferenceScreen: Video
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.5\n0.8\n1.0\n1.2\n1.5\n1.8\n2.0", true);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE);
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", 1.0f);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
// PreferenceScreen: Miscellaneous
public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true);
public static final BooleanSetting DISABLE_CAIRO_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_cairo_splash_animation", FALSE, true);
public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true);
public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false);
// PreferenceScreen: Return YouTube Dislike
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE);
public static final StringSetting RYD_USER_ID = new StringSetting("revanced_ryd_user_id", "", false, false);
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("revanced_ryd_dislike_percentage", FALSE);
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("revanced_ryd_compact_layout", FALSE);
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", FALSE, true);
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", FALSE);
// PreferenceScreen: Return YouTube Username
public static final BooleanSetting RETURN_YOUTUBE_USERNAME_ABOUT = new BooleanSetting("revanced_return_youtube_username_youtube_data_api_v3_about", FALSE, false);
// PreferenceScreen: SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", FALSE);
public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE);
public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app");
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id", "");
public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE);
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
// SB settings not exported
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
public static final String OPEN_DEFAULT_APP_SETTINGS = "revanced_default_app_settings";
/**
* If a setting path has this prefix, then remove it.
*/
public static final String OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX = "sb_segments_";
/**
* Array of settings using intent
*/
private static final String[] intentSettingArray = new String[]{
BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN.key,
CHANGE_START_PAGE.key,
CUSTOM_FILTER_STRINGS.key,
CUSTOM_PLAYBACK_SPEEDS.key,
EXTERNAL_DOWNLOADER_PACKAGE_NAME.key,
HIDE_ACCOUNT_MENU_FILTER_STRINGS.key,
SB_API_URL.key,
SETTINGS_IMPORT_EXPORT.key,
SPOOF_APP_VERSION_TARGET.key,
RETURN_YOUTUBE_USERNAME_ABOUT.key,
RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.key,
RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.key,
OPEN_DEFAULT_APP_SETTINGS,
OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX
};
/**
* @return whether dataString contains settings that use Intent
*/
public static boolean includeWithIntent(@NonNull String dataString) {
return Utils.containsAny(dataString, intentSettingArray);
}
}

View File

@ -0,0 +1,132 @@
package app.revanced.extension.music.settings.preference;
import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.net.Uri;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import java.util.Arrays;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.utils.ExtendedUtils;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils;
/**
* @noinspection all
*/
public class ExternalDownloaderPreference {
private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME;
private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_label");
private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_package_name");
private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_website");
private static EditText mEditText;
private static String packageName;
private static int mClickedDialogEntryIndex;
private static final TextWatcher textWatcher = new TextWatcher() {
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
public void afterTextChanged(Editable s) {
packageName = s.toString();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
}
};
public static void showDialog(Activity mActivity) {
packageName = settings.get().toString();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
AlertDialog.Builder builder = getDialogBuilder(mActivity);
TableLayout table = new TableLayout(mActivity);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(15, 0, 15, 0);
TableRow row = new TableRow(mActivity);
mEditText = new EditText(mActivity);
mEditText.setText(packageName);
mEditText.addTextChangedListener(textWatcher);
mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9);
mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(mEditText);
table.addView(row);
builder.setView(table);
builder.setTitle(str("revanced_external_downloader_dialog_title"));
builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> {
mClickedDialogEntryIndex = which;
mEditText.setText(mEntryValues[which].toString());
});
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
final String packageName = mEditText.getText().toString().trim();
settings.save(packageName);
checkPackageIsValid(mActivity, packageName);
dialog.dismiss();
});
builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault());
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private static boolean checkPackageIsValid(Activity mActivity, String packageName) {
String appName = "";
String website = "";
if (mClickedDialogEntryIndex >= 0) {
appName = mEntries[mClickedDialogEntryIndex].toString();
website = mWebsiteEntries[mClickedDialogEntryIndex].toString();
return showToastOrOpenWebsites(mActivity, appName, packageName, website);
} else {
return showToastOrOpenWebsites(mActivity, appName, packageName, website);
}
}
private static boolean showToastOrOpenWebsites(Activity mActivity, String appName, String packageName, String website) {
if (ExtendedUtils.isPackageEnabled(packageName))
return true;
if (website.isEmpty()) {
Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName));
return false;
}
getDialogBuilder(mActivity)
.setTitle(str("revanced_external_downloader_not_installed_dialog_title"))
.setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName))
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website));
mActivity.startActivity(i);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
return false;
}
public static boolean checkPackageIsEnabled() {
final Activity mActivity = Utils.getActivity();
packageName = settings.get().toString();
mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName);
return checkPackageIsValid(mActivity, packageName);
}
}

View File

@ -0,0 +1,351 @@
package app.revanced.extension.music.settings.preference;
import static app.revanced.extension.music.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN;
import static app.revanced.extension.music.settings.Settings.CHANGE_START_PAGE;
import static app.revanced.extension.music.settings.Settings.CUSTOM_FILTER_STRINGS;
import static app.revanced.extension.music.settings.Settings.CUSTOM_PLAYBACK_SPEEDS;
import static app.revanced.extension.music.settings.Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME;
import static app.revanced.extension.music.settings.Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS;
import static app.revanced.extension.music.settings.Settings.OPEN_DEFAULT_APP_SETTINGS;
import static app.revanced.extension.music.settings.Settings.OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX;
import static app.revanced.extension.music.settings.Settings.RETURN_YOUTUBE_USERNAME_ABOUT;
import static app.revanced.extension.music.settings.Settings.SB_API_URL;
import static app.revanced.extension.music.settings.Settings.SETTINGS_IMPORT_EXPORT;
import static app.revanced.extension.music.settings.Settings.SPOOF_APP_VERSION_TARGET;
import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams;
import static app.revanced.extension.music.utils.RestartUtils.showRestartDialog;
import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT;
import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY;
import static app.revanced.extension.shared.settings.Setting.getSettingFromPath;
import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.shared.utils.Utils.showToastShort;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.icu.text.SimpleDateFormat;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.text.InputType;
import android.util.TypedValue;
import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.TextInputLayout;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Objects;
import app.revanced.extension.music.patches.utils.ReturnYouTubeDislikePatch;
import app.revanced.extension.music.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.music.settings.ActivityHook;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.utils.ExtendedUtils;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.settings.preference.YouTubeDataAPIDialogBuilder;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("all")
public class ReVancedPreferenceFragment extends PreferenceFragment {
private static final String IMPORT_EXPORT_SETTINGS_ENTRY_KEY = "revanced_extended_settings_import_export_entries";
private static final int READ_REQUEST_CODE = 42;
private static final int WRITE_REQUEST_CODE = 43;
private static String existingSettings;
public ReVancedPreferenceFragment() {
}
/**
* Injection point.
*/
public static void onPreferenceChanged(@Nullable String key, boolean newValue) {
if (key == null || key.isEmpty())
return;
if (key.equals(Settings.RESTORE_OLD_PLAYER_LAYOUT.key) && newValue) {
Settings.RESTORE_OLD_PLAYER_BACKGROUND.save(newValue);
} else if (key.equals(Settings.RYD_ENABLED.key)) {
ReturnYouTubeDislikePatch.onRYDStatusChange(newValue);
} else if (key.equals(Settings.RYD_DISLIKE_PERCENTAGE.key) || key.equals(Settings.RYD_COMPACT_LAYOUT.key)) {
ReturnYouTubeDislike.clearAllUICaches();
}
for (Setting<?> setting : Setting.allLoadedSettings()) {
if (key.equals(setting.key)) {
((BooleanSetting) setting).save(newValue);
if (setting.rebootApp) {
showRebootDialog();
}
break;
}
}
}
public static void showRebootDialog() {
final Activity activity = ActivityHook.getActivity();
if (activity == null)
return;
showRestartDialog(activity);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
final Activity baseActivity = this.getActivity();
final Activity mActivity = ActivityHook.getActivity();
final Intent savedInstanceStateIntent = baseActivity.getIntent();
if (savedInstanceStateIntent == null)
return;
final String dataString = savedInstanceStateIntent.getDataString();
if (dataString == null || dataString.isEmpty())
return;
if (dataString.startsWith(OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX)) {
SponsorBlockCategoryPreference.showDialog(baseActivity, dataString.replaceAll(OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX, ""));
return;
} else if (dataString.equals(OPEN_DEFAULT_APP_SETTINGS)) {
openDefaultAppSetting();
return;
}
final Setting<?> settings = getSettingFromPath(dataString);
if (settings instanceof StringSetting stringSetting) {
if (settings.equals(CHANGE_START_PAGE)) {
ResettableListPreference.showDialog(mActivity, stringSetting, 2);
} else if (settings.equals(BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN)
|| settings.equals(CUSTOM_FILTER_STRINGS)
|| settings.equals(CUSTOM_PLAYBACK_SPEEDS)
|| settings.equals(HIDE_ACCOUNT_MENU_FILTER_STRINGS)
|| settings.equals(RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY)) {
ResettableEditTextPreference.showDialog(mActivity, stringSetting);
} else if (settings.equals(EXTERNAL_DOWNLOADER_PACKAGE_NAME)) {
ExternalDownloaderPreference.showDialog(mActivity);
} else if (settings.equals(SB_API_URL)) {
SponsorBlockApiUrlPreference.showDialog(mActivity);
} else if (settings.equals(SPOOF_APP_VERSION_TARGET)) {
ResettableListPreference.showDialog(mActivity, stringSetting, 0);
} else {
Logger.printDebug(() -> "Failed to find the right value: " + dataString);
}
} else if (settings instanceof BooleanSetting) {
if (settings.equals(SETTINGS_IMPORT_EXPORT)) {
importExportListDialogBuilder();
} else if (settings.equals(RETURN_YOUTUBE_USERNAME_ABOUT)) {
YouTubeDataAPIDialogBuilder.showDialog(mActivity);
} else {
Logger.printDebug(() -> "Failed to find the right value: " + dataString);
}
} else if (settings instanceof EnumSetting<?> enumSetting) {
if (settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT)) {
ResettableListPreference.showDialog(mActivity, enumSetting, 0);
}
}
} catch (Exception ex) {
Logger.printException(() -> "onCreate failure", ex);
}
}
@Override
public void onDestroy() {
super.onDestroy();
}
private void openDefaultAppSetting() {
try {
Context context = getActivity();
final Uri uri = Uri.parse("package:" + context.getPackageName());
final Intent intent = isSDKAbove(31)
? new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri)
: new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri);
context.startActivity(intent);
} catch (Exception exception) {
Logger.printException(() -> "openDefaultAppSetting failed");
}
}
/**
* Build a ListDialog for Import / Export settings
* When importing/exporting as file, {@link #onActivityResult} is used, so declare it here.
*/
private void importExportListDialogBuilder() {
try {
final Activity activity = getActivity();
final String[] mEntries = getStringArray(IMPORT_EXPORT_SETTINGS_ENTRY_KEY);
getDialogBuilder(activity)
.setTitle(str("revanced_extended_settings_import_export_title"))
.setItems(mEntries, (dialog, index) -> {
switch (index) {
case 0 -> exportActivity();
case 1 -> importActivity();
case 2 -> importExportEditTextDialogBuilder();
}
})
.setNegativeButton(android.R.string.cancel, null)
.show();
} catch (Exception ex) {
Logger.printException(() -> "importExportListDialogBuilder failure", ex);
}
}
/**
* Build a EditTextDialog for Import / Export settings
*/
private void importExportEditTextDialogBuilder() {
try {
final Activity activity = getActivity();
final EditText textView = new EditText(activity);
existingSettings = Setting.exportToJson(null);
textView.setText(existingSettings);
textView.setInputType(textView.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
textView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap.
TextInputLayout textInputLayout = new TextInputLayout(activity);
textInputLayout.setLayoutParams(getLayoutParams());
textInputLayout.addView(textView);
FrameLayout container = new FrameLayout(activity);
container.addView(textInputLayout);
getDialogBuilder(activity)
.setTitle(str("revanced_extended_settings_import_export_title"))
.setView(container)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("revanced_extended_settings_import_copy"), (dialog, which) -> Utils.setClipboard(textView.getText().toString(), str("revanced_share_copy_settings_success")))
.setPositiveButton(str("revanced_extended_settings_import"), (dialog, which) -> importSettings(textView.getText().toString()))
.show();
} catch (Exception ex) {
Logger.printException(() -> "importExportEditTextDialogBuilder failure", ex);
}
}
/**
* Invoke the SAF(Storage Access Framework) to export settings
*/
private void exportActivity() {
@SuppressLint("SimpleDateFormat")
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
var appName = ExtendedUtils.getApplicationLabel();
var versionName = ExtendedUtils.getVersionName();
var formatDate = dateFormat.format(new Date(System.currentTimeMillis()));
var fileName = String.format("%s_v%s_%s.txt", appName, versionName, formatDate);
var intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TITLE, fileName);
startActivityForResult(intent, WRITE_REQUEST_CODE);
}
/**
* Invoke the SAF(Storage Access Framework) to import settings
*/
private void importActivity() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(isSDKAbove(29) ? "text/plain" : "*/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
exportText(data.getData());
} else if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) {
importText(data.getData());
}
}
private void exportText(Uri uri) {
try {
final Context context = this.getContext();
@SuppressLint("Recycle")
FileWriter jsonFileWriter =
new FileWriter(
Objects.requireNonNull(context.getApplicationContext()
.getContentResolver()
.openFileDescriptor(uri, "w"))
.getFileDescriptor()
);
PrintWriter printWriter = new PrintWriter(jsonFileWriter);
printWriter.write(Setting.exportToJson(null));
printWriter.close();
jsonFileWriter.close();
showToastShort(str("revanced_extended_settings_export_success"));
} catch (IOException e) {
showToastShort(str("revanced_extended_settings_export_failed"));
}
}
private void importText(Uri uri) {
final Context context = this.getContext();
StringBuilder sb = new StringBuilder();
String line;
try {
@SuppressLint("Recycle")
FileReader fileReader =
new FileReader(
Objects.requireNonNull(context.getApplicationContext()
.getContentResolver()
.openFileDescriptor(uri, "r"))
.getFileDescriptor()
);
BufferedReader bufferedReader = new BufferedReader(fileReader);
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
bufferedReader.close();
fileReader.close();
final boolean restartNeeded = Setting.importFromJSON(sb.toString(), false);
if (restartNeeded) {
ReVancedPreferenceFragment.showRebootDialog();
}
} catch (IOException e) {
showToastShort(str("revanced_extended_settings_import_failed"));
throw new RuntimeException(e);
}
}
private void importSettings(String replacementSettings) {
try {
existingSettings = Setting.exportToJson(null);
if (replacementSettings.equals(existingSettings)) {
return;
}
final boolean restartNeeded = Setting.importFromJSON(replacementSettings, false);
if (restartNeeded) {
ReVancedPreferenceFragment.showRebootDialog();
}
} catch (Exception ex) {
Logger.printException(() -> "importSettings failure", ex);
}
}
}

View File

@ -0,0 +1,50 @@
package app.revanced.extension.music.settings.preference;
import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.Activity;
import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import com.google.android.material.textfield.TextInputLayout;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
public class ResettableEditTextPreference {
public static void showDialog(Activity mActivity, @NonNull Setting<String> setting) {
try {
final EditText textView = new EditText(mActivity);
textView.setText(setting.get());
TextInputLayout textInputLayout = new TextInputLayout(mActivity);
textInputLayout.setLayoutParams(getLayoutParams());
textInputLayout.addView(textView);
FrameLayout container = new FrameLayout(mActivity);
container.addView(textInputLayout);
getDialogBuilder(mActivity)
.setTitle(str(setting.key + "_title"))
.setView(container)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> {
setting.resetToDefault();
ReVancedPreferenceFragment.showRebootDialog();
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
setting.save(textView.getText().toString().trim());
ReVancedPreferenceFragment.showRebootDialog();
})
.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
}

View File

@ -0,0 +1,81 @@
package app.revanced.extension.music.settings.preference;
import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.Activity;
import androidx.annotation.NonNull;
import java.util.Arrays;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger;
public class ResettableListPreference {
private static int mClickedDialogEntryIndex;
public static void showDialog(Activity mActivity, @NonNull Setting<String> setting, int defaultIndex) {
try {
final String settingsKey = setting.key;
final String entryKey = settingsKey + "_entries";
final String entryValueKey = settingsKey + "_entry_values";
final String[] mEntries = getStringArray(entryKey);
final String[] mEntryValues = getStringArray(entryValueKey);
final int findIndex = Arrays.binarySearch(mEntryValues, setting.get());
mClickedDialogEntryIndex = findIndex >= 0 ? findIndex : defaultIndex;
getDialogBuilder(mActivity)
.setTitle(str(settingsKey + "_title"))
.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex,
(dialog, id) -> mClickedDialogEntryIndex = id)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> {
setting.resetToDefault();
ReVancedPreferenceFragment.showRebootDialog();
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
setting.save(mEntryValues[mClickedDialogEntryIndex]);
ReVancedPreferenceFragment.showRebootDialog();
})
.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
public static void showDialog(Activity mActivity, @NonNull EnumSetting<?> setting, int defaultIndex) {
try {
final String settingsKey = setting.key;
final String entryKey = settingsKey + "_entries";
final String entryValueKey = settingsKey + "_entry_values";
final String[] mEntries = getStringArray(entryKey);
final String[] mEntryValues = getStringArray(entryValueKey);
final int findIndex = Arrays.binarySearch(mEntryValues, setting.get().toString());
mClickedDialogEntryIndex = findIndex >= 0 ? findIndex : defaultIndex;
getDialogBuilder(mActivity)
.setTitle(str(settingsKey + "_title"))
.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex,
(dialog, id) -> mClickedDialogEntryIndex = id)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> {
setting.resetToDefault();
ReVancedPreferenceFragment.showRebootDialog();
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
setting.saveValueFromString(mEntryValues[mClickedDialogEntryIndex]);
ReVancedPreferenceFragment.showRebootDialog();
})
.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
}

View File

@ -0,0 +1,70 @@
package app.revanced.extension.music.settings.preference;
import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.Activity;
import android.util.Patterns;
import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import com.google.android.material.textfield.TextInputLayout;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
public class SponsorBlockApiUrlPreference {
public static void showDialog(Activity mActivity) {
try {
final StringSetting apiUrl = Settings.SB_API_URL;
final EditText textView = new EditText(mActivity);
textView.setText(apiUrl.get());
TextInputLayout textInputLayout = new TextInputLayout(mActivity);
textInputLayout.setLayoutParams(getLayoutParams());
textInputLayout.addView(textView);
FrameLayout container = new FrameLayout(mActivity);
container.addView(textInputLayout);
getDialogBuilder(mActivity)
.setTitle(str("revanced_sb_api_url"))
.setView(container)
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> {
apiUrl.resetToDefault();
Utils.showToastShort(str("revanced_sb_api_url_reset"));
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String serverAddress = textView.getText().toString().trim();
if (!isValidSBServerAddress(serverAddress)) {
Utils.showToastShort(str("revanced_sb_api_url_invalid"));
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
apiUrl.save(serverAddress);
Utils.showToastShort(str("revanced_sb_api_url_changed"));
}
})
.show();
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
}
public static boolean isValidSBServerAddress(@NonNull String serverAddress) {
if (!Patterns.WEB_URL.matcher(serverAddress).matches()) {
return false;
}
// Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/"
// Could use Patterns.compile, but this is simpler
final int lastDotIndex = serverAddress.lastIndexOf('.');
return lastDotIndex == -1 || !serverAddress.substring(lastDotIndex).contains("/");
}
}

View File

@ -0,0 +1,124 @@
package app.revanced.extension.music.settings.preference;
import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.Activity;
import android.app.AlertDialog;
import android.graphics.Color;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import java.util.Arrays;
import java.util.Objects;
import app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.music.sponsorblock.objects.SegmentCategory;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
public class SponsorBlockCategoryPreference {
private static final String[] CategoryBehaviourEntries = {str("revanced_sb_skip_automatically"), str("revanced_sb_skip_ignore")};
private static final CategoryBehaviour[] CategoryBehaviourEntryValues = {CategoryBehaviour.SKIP_AUTOMATICALLY, CategoryBehaviour.IGNORE};
private static int mClickedDialogEntryIndex;
public static void showDialog(Activity baseActivity, String categoryString) {
try {
SegmentCategory category = Objects.requireNonNull(SegmentCategory.byCategoryKey(categoryString));
final AlertDialog.Builder builder = getDialogBuilder(baseActivity);
TableLayout table = new TableLayout(baseActivity);
table.setOrientation(LinearLayout.HORIZONTAL);
table.setPadding(70, 0, 150, 0);
TableRow row = new TableRow(baseActivity);
TextView colorTextLabel = new TextView(baseActivity);
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
row.addView(colorTextLabel);
TextView colorDotView = new TextView(baseActivity);
colorDotView.setText(category.getCategoryColorDot());
colorDotView.setPadding(30, 0, 30, 0);
row.addView(colorDotView);
final EditText mEditText = new EditText(baseActivity);
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
mEditText.setText(category.colorString());
mEditText.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) {
}
@Override
public void afterTextChanged(Editable s) {
try {
String colorString = s.toString();
if (!colorString.startsWith("#")) {
s.insert(0, "#"); // recursively calls back into this method
return;
}
if (colorString.length() > 7) {
s.delete(7, colorString.length());
return;
}
final int color = Color.parseColor(colorString);
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
} catch (IllegalArgumentException ex) {
// ignore
}
}
});
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
row.addView(mEditText);
table.addView(row);
builder.setView(table);
builder.setTitle(category.title.toString());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
category.behaviour = CategoryBehaviourEntryValues[mClickedDialogEntryIndex];
category.setBehaviour(category.behaviour);
SegmentCategory.updateEnabledCategories();
String colorString = mEditText.getText().toString();
try {
if (!colorString.equals(category.colorString())) {
category.setColor(colorString);
Utils.showToastShort(str("revanced_sb_color_changed"));
}
} catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_sb_color_invalid"));
}
});
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
try {
category.resetColor();
Utils.showToastShort(str("revanced_sb_color_reset"));
} catch (Exception ex) {
Logger.printException(() -> "setNeutralButton failure", ex);
}
});
builder.setNegativeButton(android.R.string.cancel, null);
final int index = Arrays.asList(CategoryBehaviourEntryValues).indexOf(category.behaviour);
mClickedDialogEntryIndex = Math.max(index, 0);
builder.setSingleChoiceItems(CategoryBehaviourEntries, mClickedDialogEntryIndex,
(dialog, id) -> mClickedDialogEntryIndex = id);
builder.show();
} catch (Exception ex) {
Logger.printException(() -> "dialogBuilder failure", ex);
}
}
}

View File

@ -0,0 +1,54 @@
package app.revanced.extension.music.shared
import app.revanced.extension.shared.utils.Event
import app.revanced.extension.shared.utils.Logger
/**
* WatchWhile player type
*/
enum class PlayerType {
DISMISSED,
MINIMIZED,
MAXIMIZED_NOW_PLAYING,
MAXIMIZED_PLAYER_ADDITIONAL_VIEW,
FULLSCREEN,
SLIDING_VERTICALLY,
QUEUE_EXPANDING,
SLIDING_HORIZONTALLY;
companion object {
private val nameToPlayerType = values().associateBy { it.name }
@JvmStatic
fun setFromString(enumName: String) {
val newType = nameToPlayerType[enumName]
if (newType == null) {
Logger.printException { "Unknown PlayerType encountered: $enumName" }
} else if (current != newType) {
Logger.printDebug { "PlayerType changed to: $newType" }
current = newType
}
}
/**
* The current player type.
*/
@JvmStatic
var current
get() = currentPlayerType
private set(value) {
currentPlayerType = value
onChange(currentPlayerType)
}
@Volatile // value is read/write from different threads
private var currentPlayerType = MINIMIZED
/**
* player type change listener
*/
@JvmStatic
val onChange = Event<PlayerType>()
}
}

View File

@ -0,0 +1,319 @@
package app.revanced.extension.music.shared;
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
import static app.revanced.extension.shared.utils.Utils.getFormattedTimeStamp;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
/**
* Hooking class for the current playing video.
*/
@SuppressWarnings("unused")
public final class VideoInformation {
private static final float DEFAULT_YOUTUBE_MUSIC_PLAYBACK_SPEED = 1.0f;
private static final int DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY = -2;
private static final String DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING = getString("quality_auto");
@NonNull
private static String videoId = "";
private static long videoLength = 0;
private static long videoTime = -1;
/**
* The current playback speed
*/
private static float playbackSpeed = DEFAULT_YOUTUBE_MUSIC_PLAYBACK_SPEED;
/**
* The current video quality
*/
private static int videoQuality = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY;
/**
* The current video quality string
*/
private static String videoQualityString = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING;
/**
* The available qualities of the current video in human readable form: [1080, 720, 480]
*/
@Nullable
private static List<Integer> videoQualities;
/**
* Injection point.
*/
public static void initialize() {
videoTime = -1;
videoLength = 0;
Logger.printDebug(() -> "Initialized Player");
}
/**
* Injection point.
*/
public static void initializeMdx() {
Logger.printDebug(() -> "Initialized Mdx Player");
}
/**
* Id of the current video playing. Includes Shorts and YouTube Stories.
*
* @return The id of the video. Empty string if not set yet.
*/
@NonNull
public static String getVideoId() {
return videoId;
}
/**
* Injection point.
*
* @param newlyLoadedVideoId id of the current video
*/
public static void setVideoId(@NonNull String newlyLoadedVideoId) {
if (Objects.equals(newlyLoadedVideoId, videoId)) {
return;
}
Logger.printDebug(() -> "New video id: " + newlyLoadedVideoId);
videoId = newlyLoadedVideoId;
}
/**
* Seek on the current video.
* Does not function for playback of Shorts.
* <p>
* Caution: If called from a videoTimeHook() callback,
* this will cause a recursive call into the same videoTimeHook() callback.
*
* @param seekTime The millisecond to seek the video to.
* @return if the seek was successful
*/
public static boolean seekTo(final long seekTime) {
Utils.verifyOnMainThread();
try {
final long videoLength = getVideoLength();
final long videoTime = getVideoTime();
final long adjustedSeekTime = getAdjustedSeekTime(seekTime, videoLength);
if (videoTime <= 0 || videoLength <= 0) {
Logger.printDebug(() -> "Skipping seekTo as the video is not initialized");
return false;
}
Logger.printDebug(() -> "Seeking to: " + getFormattedTimeStamp(adjustedSeekTime));
// Try regular playback controller first, and it will not succeed if casting.
if (overrideVideoTime(adjustedSeekTime)) return true;
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
// Else the video is loading or changing videos, or video is casting to a different device.
// Try calling the seekTo method of the MDX player director (called when casting).
// The difference has to be a different second mark in order to avoid infinite skip loops
// as the Lounge API only supports seconds.
if (adjustedSeekTime / 1000 == videoTime / 1000) {
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
return false;
}
return overrideMDXVideoTime(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek", ex);
return false;
}
}
// Prevent issues such as play/pause button or autoplay not working.
private static long getAdjustedSeekTime(final long seekTime, final long videoLength) {
// If the user skips to a section that is 500 ms before the video length,
// it will get stuck in a loop.
if (videoLength - seekTime > 500) {
return seekTime;
} else {
// Otherwise, just skips to a time longer than the video length.
// Paradoxically, if user skips to a section much longer than the video length, does not get stuck in a loop.
return Integer.MAX_VALUE;
}
}
/**
* @return The current playback speed.
*/
public static float getPlaybackSpeed() {
return playbackSpeed;
}
/**
* Injection point.
*
* @param newlyLoadedPlaybackSpeed The current playback speed.
*/
public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) {
playbackSpeed = newlyLoadedPlaybackSpeed;
}
/**
* @return The current video quality.
*/
public static int getVideoQuality() {
return videoQuality;
}
/**
* @return The current video quality string.
*/
public static String getVideoQualityString() {
return videoQualityString;
}
/**
* Injection point.
*
* @param newlyLoadedQuality The current video quality string.
*/
public static void setVideoQuality(String newlyLoadedQuality) {
if (newlyLoadedQuality == null) {
return;
}
try {
String splitVideoQuality;
if (newlyLoadedQuality.contains("p")) {
splitVideoQuality = newlyLoadedQuality.split("p")[0];
videoQuality = Integer.parseInt(splitVideoQuality);
videoQualityString = splitVideoQuality + "p";
} else if (newlyLoadedQuality.contains("s")) {
splitVideoQuality = newlyLoadedQuality.split("s")[0];
videoQuality = Integer.parseInt(splitVideoQuality);
videoQualityString = splitVideoQuality + "s";
} else {
videoQuality = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY;
videoQualityString = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING;
}
} catch (NumberFormatException ignored) {
}
}
/**
* @return available video quality.
*/
public static int getAvailableVideoQuality(int preferredQuality) {
if (videoQualities != null) {
int qualityToUse = videoQualities.get(0); // first element is automatic mode
for (Integer quality : videoQualities) {
if (quality <= preferredQuality && qualityToUse < quality) {
qualityToUse = quality;
}
}
preferredQuality = qualityToUse;
}
return preferredQuality;
}
/**
* Injection point.
*
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
*/
public static void setVideoQualityList(Object[] qualities) {
try {
if (videoQualities == null || videoQualities.size() != qualities.length) {
videoQualities = new ArrayList<>(qualities.length);
for (Object streamQuality : qualities) {
for (Field field : streamQuality.getClass().getFields()) {
if (field.getType().isAssignableFrom(Integer.TYPE)
&& field.getName().length() <= 2) {
videoQualities.add(field.getInt(streamQuality));
}
}
}
Logger.printDebug(() -> "videoQualities: " + videoQualities);
}
} catch (Exception ex) {
Logger.printException(() -> "Failed to set quality list", ex);
}
}
/**
* Length of the current video playing. Includes Shorts.
*
* @return The length of the video in milliseconds.
* If the video is not yet loaded, or if the video is playing in the background with no video visible,
* then this returns zero.
*/
public static long getVideoLength() {
return videoLength;
}
/**
* Injection point.
*
* @param length The length of the video in milliseconds.
*/
public static void setVideoLength(final long length) {
if (videoLength != length) {
videoLength = length;
}
}
/**
* Playback time of the current video playing. Includes Shorts.
* <p>
* Value will lag behind the actual playback time by a variable amount based on the playback speed.
* <p>
* If playback speed is 2.0x, this value may be up to 2000ms behind the actual playback time.
* If playback speed is 1.0x, this value may be up to 1000ms behind the actual playback time.
* If playback speed is 0.5x, this value may be up to 500ms behind the actual playback time.
* Etc.
*
* @return The time of the video in milliseconds. -1 if not set yet.
*/
public static long getVideoTime() {
return videoTime;
}
/**
* Injection point.
* Called on the main thread every 1000ms.
*
* @param currentPlaybackTime The current playback time of the video in milliseconds.
*/
public static void setVideoTime(final long currentPlaybackTime) {
videoTime = currentPlaybackTime;
}
/**
* Overrides the current quality.
* Rest of the implementation added by patch.
*/
public static void overrideVideoQuality(int qualityOverride) {
Logger.printDebug(() -> "Overriding video quality to: " + qualityOverride);
}
/**
* Overrides the current video time by seeking.
* Rest of the implementation added by patch.
*/
public static boolean overrideVideoTime(final long seekTime) {
// These instructions are ignored by patch.
Logger.printDebug(() -> "Seeking to " + seekTime);
return false;
}
/**
* Overrides the current video time by seeking. (MDX player)
* Rest of the implementation added by patch.
*/
public static boolean overrideMDXVideoTime(final long seekTime) {
// These instructions are ignored by patch.
Logger.printDebug(() -> "Seeking to " + seekTime);
return false;
}
}

View File

@ -0,0 +1,63 @@
package app.revanced.extension.music.shared
import app.revanced.extension.shared.utils.Event
import app.revanced.extension.shared.utils.Logger
/**
* Music video type
*/
enum class VideoType {
MUSIC_VIDEO_TYPE_UNKNOWN,
MUSIC_VIDEO_TYPE_ATV,
MUSIC_VIDEO_TYPE_OMV,
MUSIC_VIDEO_TYPE_UGC,
MUSIC_VIDEO_TYPE_SHOULDER,
MUSIC_VIDEO_TYPE_OFFICIAL_SOURCE_MUSIC,
MUSIC_VIDEO_TYPE_PRIVATELY_OWNED_TRACK,
MUSIC_VIDEO_TYPE_LIVE_STREAM,
MUSIC_VIDEO_TYPE_PODCAST_EPISODE;
companion object {
private val nameToVideoType = values().associateBy { it.name }
@JvmStatic
fun setFromString(enumName: String) {
val newType = nameToVideoType[enumName]
if (newType == null) {
Logger.printException { "Unknown VideoType encountered: $enumName" }
} else if (current != newType) {
Logger.printDebug { "VideoType changed to: $newType" }
current = newType
}
}
/**
* The current video type.
*/
@JvmStatic
var current
get() = currentVideoType
private set(value) {
currentVideoType = value
onChange(currentVideoType)
}
@Volatile // value is read/write from different threads
private var currentVideoType = MUSIC_VIDEO_TYPE_UNKNOWN
/**
* player type change listener
*/
@JvmStatic
val onChange = Event<VideoType>()
}
fun isMusicVideo(): Boolean {
return this == MUSIC_VIDEO_TYPE_OMV
}
fun isPodCast(): Boolean {
return this == MUSIC_VIDEO_TYPE_PODCAST_EPISODE
}
}

View File

@ -0,0 +1,472 @@
package app.revanced.extension.music.sponsorblock;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.graphics.Canvas;
import android.graphics.Rect;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Objects;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.music.sponsorblock.objects.SponsorSegment;
import app.revanced.extension.music.sponsorblock.requests.SBRequester;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
/**
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
* <p>
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
*/
@SuppressWarnings("unused")
public class SegmentPlaybackController {
@Nullable
private static String currentVideoId;
@Nullable
private static SponsorSegment[] segments;
/**
* Currently playing (non-highlight) segment that user can manually skip.
*/
@Nullable
private static SponsorSegment segmentCurrentlyPlaying;
/**
* Currently playing manual skip segment that is scheduled to hide.
* This will always be NULL or equal to {@link #segmentCurrentlyPlaying}.
*/
@Nullable
private static SponsorSegment scheduledHideSegment;
/**
* Upcoming segment that is scheduled to either autoskip or show the manual skip button.
*/
@Nullable
private static SponsorSegment scheduledUpcomingSegment;
/**
* System time (in milliseconds) of when to hide the skip button of {@link #segmentCurrentlyPlaying}.
* Value is zero if playback is not inside a segment ({@link #segmentCurrentlyPlaying} is null),
*/
private static long skipSegmentButtonEndTime;
private static int sponsorBarAbsoluteLeft;
private static int sponsorAbsoluteBarRight;
private static int sponsorBarThickness = 7;
private static SponsorSegment lastSegmentSkipped;
private static long lastSegmentSkippedTime;
private static int toastNumberOfSegmentsSkipped;
@Nullable
private static SponsorSegment toastSegmentSkipped;
private static void setSegments(@NonNull SponsorSegment[] videoSegments) {
Arrays.sort(videoSegments);
segments = videoSegments;
}
/**
* Clears all downloaded data.
*/
private static void clearData() {
SponsorBlockSettings.initialize();
currentVideoId = null;
segments = null;
segmentCurrentlyPlaying = null;
scheduledUpcomingSegment = null;
scheduledHideSegment = null;
skipSegmentButtonEndTime = 0;
toastSegmentSkipped = null;
toastNumberOfSegmentsSkipped = 0;
}
/**
* Injection point.
*/
public static void setVideoId(@NonNull String videoId) {
try {
if (Objects.equals(currentVideoId, videoId)) {
return;
}
clearData();
if (!Settings.SB_ENABLED.get()) {
return;
}
if (Utils.isNetworkNotConnected()) {
Logger.printDebug(() -> "Network not connected, ignoring video");
return;
}
currentVideoId = videoId;
Logger.printDebug(() -> "setCurrentVideoId: " + videoId);
Utils.runOnBackgroundThread(() -> {
try {
executeDownloadSegments(videoId);
} catch (Exception e) {
Logger.printException(() -> "Failed to download segments", e);
}
});
} catch (Exception ex) {
Logger.printException(() -> "setCurrentVideoId failure", ex);
}
}
/**
* Must be called off main thread
*/
static void executeDownloadSegments(@NonNull String videoId) {
Objects.requireNonNull(videoId);
try {
SponsorSegment[] segments = SBRequester.getSegments(videoId);
Utils.runOnMainThread(() -> {
if (!videoId.equals(currentVideoId)) {
// user changed videos before get segments network call could complete
Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId);
return;
}
setSegments(segments);
// check for any skips now, instead of waiting for the next update to setVideoTime()
setVideoTime(VideoInformation.getVideoTime());
});
} catch (Exception ex) {
Logger.printException(() -> "executeDownloadSegments failure", ex);
}
}
/**
* Injection point.
* Updates SponsorBlock every 1000ms.
* When changing videos, this is first called with value 0 and then the video is changed.
*/
public static void setVideoTime(long millis) {
try {
if (!Settings.SB_ENABLED.get() || segments == null || segments.length == 0) {
return;
}
Logger.printDebug(() -> "setVideoTime: " + millis);
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
// Amount of time to look ahead for the next segment,
// and the threshold to determine if a scheduled show/hide is at the correct video time when it's run.
//
// This value must be greater than largest time between calls to this method (1000ms),
// and must be adjusted for the video speed.
//
// To debug the stale skip logic, set this to a very large value (5000 or more)
// then try manually seeking just before playback reaches a segment skip.
final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 1200);
final long startTimerLookAheadThreshold = millis + speedAdjustedTimeThreshold;
SponsorSegment foundSegmentCurrentlyPlaying = null;
SponsorSegment foundUpcomingSegment = null;
for (final SponsorSegment segment : segments) {
if (segment.category.behaviour == CategoryBehaviour.IGNORE) {
continue;
}
if (segment.end <= millis) {
continue; // past this segment
}
if (segment.start <= millis) {
// we are in the segment!
if (segment.shouldAutoSkip()) {
skipSegment(segment);
return; // must return, as skipping causes a recursive call back into this method
}
// first found segment, or it's an embedded segment and fully inside the outer segment
if (foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) {
// If the found segment is not currently displayed, then do not show if the segment is nearly over.
// This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time.
// Also prevents showing the skip button if user seeks into the last 800ms of the segment.
final long minMillisOfSegmentRemainingThreshold = 800;
if (segmentCurrentlyPlaying == segment
|| !segment.endIsNear(millis, minMillisOfSegmentRemainingThreshold)) {
foundSegmentCurrentlyPlaying = segment;
} else {
Logger.printDebug(() -> "Ignoring segment that ends very soon: " + segment);
}
}
// Keep iterating and looking. There may be an upcoming autoskip,
// or there may be another smaller segment nested inside this segment
continue;
}
// segment is upcoming
if (startTimerLookAheadThreshold < segment.start) {
break; // segment is not close enough to schedule, and no segments after this are of interest
}
if (segment.shouldAutoSkip()) { // upcoming autoskip
foundUpcomingSegment = segment;
break; // must stop here
}
// upcoming manual skip
// do not schedule upcoming segment, if it is not fully contained inside the current segment
if ((foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment))
// use the most inner upcoming segment
&& (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) {
// Only schedule, if the segment start time is not near the end time of the current segment.
// This check is needed to prevent scheduled hide and show from clashing with each other.
// Instead the upcoming segment will be handled when the current segment scheduled hide calls back into this method.
final long minTimeBetweenStartEndOfSegments = 1000;
if (foundSegmentCurrentlyPlaying == null
|| !foundSegmentCurrentlyPlaying.endIsNear(segment.start, minTimeBetweenStartEndOfSegments)) {
foundUpcomingSegment = segment;
} else {
Logger.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment);
}
}
}
if (segmentCurrentlyPlaying != foundSegmentCurrentlyPlaying) {
setSegmentCurrentlyPlaying(foundSegmentCurrentlyPlaying);
} else if (foundSegmentCurrentlyPlaying != null
&& skipSegmentButtonEndTime != 0 && skipSegmentButtonEndTime <= System.currentTimeMillis()) {
Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying);
skipSegmentButtonEndTime = 0;
}
// schedule a hide, only if the segment end is near
final SponsorSegment segmentToHide =
(foundSegmentCurrentlyPlaying != null && foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold))
? foundSegmentCurrentlyPlaying
: null;
if (scheduledHideSegment != segmentToHide) {
if (segmentToHide == null) {
Logger.printDebug(() -> "Clearing scheduled hide: " + scheduledHideSegment);
scheduledHideSegment = null;
} else {
scheduledHideSegment = segmentToHide;
Logger.printDebug(() -> "Scheduling hide segment: " + segmentToHide + " playbackSpeed: " + playbackSpeed);
final long delayUntilHide = (long) ((segmentToHide.end - millis) / playbackSpeed);
Utils.runOnMainThreadDelayed(() -> {
if (scheduledHideSegment != segmentToHide) {
Logger.printDebug(() -> "Ignoring old scheduled hide segment: " + segmentToHide);
return;
}
scheduledHideSegment = null;
final long videoTime = VideoInformation.getVideoTime();
if (!segmentToHide.endIsNear(videoTime, speedAdjustedTimeThreshold)) {
// current video time is not what's expected. User paused playback
Logger.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide
+ " videoInformation time: " + videoTime);
return;
}
Logger.printDebug(() -> "Running scheduled hide segment: " + segmentToHide);
// Need more than just hide the skip button, as this may have been an embedded segment
// Instead call back into setVideoTime to check everything again.
// Should not use VideoInformation time as it is less accurate,
// but this scheduled handler was scheduled precisely so we can just use the segment end time
setSegmentCurrentlyPlaying(null);
setVideoTime(segmentToHide.end);
}, delayUntilHide);
}
}
if (scheduledUpcomingSegment != foundUpcomingSegment) {
if (foundUpcomingSegment == null) {
Logger.printDebug(() -> "Clearing scheduled segment: " + scheduledUpcomingSegment);
scheduledUpcomingSegment = null;
} else {
scheduledUpcomingSegment = foundUpcomingSegment;
final SponsorSegment segmentToSkip = foundUpcomingSegment;
Logger.printDebug(() -> "Scheduling segment: " + segmentToSkip + " playbackSpeed: " + playbackSpeed);
final long delayUntilSkip = (long) ((segmentToSkip.start - millis) / playbackSpeed);
Utils.runOnMainThreadDelayed(() -> {
if (scheduledUpcomingSegment != segmentToSkip) {
Logger.printDebug(() -> "Ignoring old scheduled segment: " + segmentToSkip);
return;
}
scheduledUpcomingSegment = null;
final long videoTime = VideoInformation.getVideoTime();
if (!segmentToSkip.startIsNear(videoTime, speedAdjustedTimeThreshold)) {
// current video time is not what's expected. User paused playback
Logger.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip
+ " videoInformation time: " + videoTime);
return;
}
if (segmentToSkip.shouldAutoSkip()) {
Logger.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip);
skipSegment(segmentToSkip);
} else {
Logger.printDebug(() -> "Running scheduled show segment: " + segmentToSkip);
setSegmentCurrentlyPlaying(segmentToSkip);
}
}, delayUntilSkip);
}
}
} catch (Exception e) {
Logger.printException(() -> "setVideoTime failure", e);
}
}
private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) {
if (segment == null) {
if (segmentCurrentlyPlaying != null)
Logger.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying);
segmentCurrentlyPlaying = null;
skipSegmentButtonEndTime = 0;
return;
}
segmentCurrentlyPlaying = segment;
skipSegmentButtonEndTime = 0;
Logger.printDebug(() -> "Showing segment: " + segment);
}
private static void skipSegment(@NonNull SponsorSegment segmentToSkip) {
try {
// If trying to seek to end of the video, YouTube can seek just before of the actual end.
// (especially if the video does not end on a whole second boundary).
// This causes additional segment skip attempts, even though it cannot seek any closer to the desired time.
// Check for and ignore repeated skip attempts of the same segment over a small time period.
final long now = System.currentTimeMillis();
final long minimumMillisecondsBetweenSkippingSameSegment = 500;
if ((lastSegmentSkipped == segmentToSkip) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) {
Logger.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segmentToSkip);
return;
}
Logger.printDebug(() -> "Skipping segment: " + segmentToSkip);
lastSegmentSkipped = segmentToSkip;
lastSegmentSkippedTime = now;
setSegmentCurrentlyPlaying(null);
scheduledHideSegment = null;
scheduledUpcomingSegment = null;
// If the seek is successful, then the seek causes a recursive call back into this class.
final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end);
if (!seekSuccessful) {
// can happen when switching videos and is normal
Logger.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip);
return;
}
// check for any smaller embedded segments, and count those as autoskipped
final boolean showSkipToast = Settings.SB_TOAST_ON_SKIP.get();
for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) {
if (segmentToSkip.end < otherSegment.start) {
break; // no other segments can be contained
}
if (otherSegment == segmentToSkip ||
segmentToSkip.containsSegment(otherSegment)) {
otherSegment.didAutoSkipped = true;
// Do not show a toast if the user is scrubbing thru a paused video.
// Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date.
// So instead, only hide toasts because all other skip logic done while paused causes no harm.
if (showSkipToast) {
showSkippedSegmentToast(otherSegment);
}
}
}
} catch (Exception ex) {
Logger.printException(() -> "skipSegment failure", ex);
}
}
private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) {
Utils.verifyOnMainThread();
toastNumberOfSegmentsSkipped++;
if (toastNumberOfSegmentsSkipped > 1) {
return; // toast already scheduled
}
toastSegmentSkipped = segment;
final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments
Utils.runOnMainThreadDelayed(() -> {
try {
if (toastSegmentSkipped == null) { // video was changed just after skipping segment
Logger.printDebug(() -> "Ignoring old scheduled show toast");
return;
}
Utils.showToastShort(toastNumberOfSegmentsSkipped == 1
? toastSegmentSkipped.getSkippedToastText()
: str("revanced_sb_skipped_multiple_segments"));
} catch (Exception ex) {
Logger.printException(() -> "showSkippedSegmentToast failure", ex);
} finally {
toastNumberOfSegmentsSkipped = 0;
toastSegmentSkipped = null;
}
}, delayToToastMilliseconds);
}
/**
* Injection point
*/
public static void setSponsorBarRect(final Object self, final String fieldName) {
try {
Field field = self.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Rect rect = (Rect) Objects.requireNonNull(field.get(self));
setSponsorBarAbsoluteLeft(rect);
setSponsorBarAbsoluteRight(rect);
} catch (Exception ex) {
Logger.printException(() -> "setSponsorBarRect failure", ex);
}
}
private static void setSponsorBarAbsoluteLeft(Rect rect) {
final int left = rect.left;
if (sponsorBarAbsoluteLeft != left) {
Logger.printDebug(() -> "setSponsorBarAbsoluteLeft: " + left);
sponsorBarAbsoluteLeft = left;
}
}
private static void setSponsorBarAbsoluteRight(Rect rect) {
final int right = rect.right;
if (sponsorAbsoluteBarRight != right) {
Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right);
sponsorAbsoluteBarRight = right;
}
}
/**
* Injection point
*/
public static void setSponsorBarThickness(int thickness) {
if (sponsorBarThickness != thickness) {
sponsorBarThickness = (int) Math.round(thickness * 1.2);
Logger.printDebug(() -> "setSponsorBarThickness: " + sponsorBarThickness);
}
}
/**
* Injection point.
*/
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
try {
if (segments == null) return;
final long videoLength = VideoInformation.getVideoLength();
if (videoLength <= 0) return;
final int thicknessDiv2 = sponsorBarThickness / 2; // rounds down
final float top = posY - (sponsorBarThickness - thicknessDiv2);
final float bottom = posY + thicknessDiv2;
final float videoMillisecondsToPixels = (1f / videoLength) * (sponsorAbsoluteBarRight - sponsorBarAbsoluteLeft);
final float leftPadding = sponsorBarAbsoluteLeft;
for (SponsorSegment segment : segments) {
final float left = leftPadding + segment.start * videoMillisecondsToPixels;
final float right = leftPadding + segment.end * videoMillisecondsToPixels;
canvas.drawRect(left, top, right, bottom, segment.category.paint);
}
} catch (Exception ex) {
Logger.printException(() -> "drawSponsorTimeBars failure", ex);
}
}
}

View File

@ -0,0 +1,52 @@
package app.revanced.extension.music.sponsorblock;
import androidx.annotation.NonNull;
import java.util.UUID;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.sponsorblock.objects.SegmentCategory;
import app.revanced.extension.shared.settings.Setting;
public class SponsorBlockSettings {
private static boolean initialized;
/**
* @return if the user has ever voted, created a segment, or imported existing SB settings.
*/
public static boolean userHasSBPrivateId() {
return !Settings.SB_PRIVATE_USER_ID.get().isEmpty();
}
/**
* Use this only if a user id is required (creating segments, voting).
*/
@NonNull
public static String getSBPrivateUserID() {
String uuid = Settings.SB_PRIVATE_USER_ID.get();
if (uuid.isEmpty()) {
uuid = (UUID.randomUUID().toString() +
UUID.randomUUID().toString() +
UUID.randomUUID().toString())
.replace("-", "");
Settings.SB_PRIVATE_USER_ID.save(uuid);
}
return uuid;
}
public static void initialize() {
if (initialized) {
return;
}
initialized = true;
SegmentCategory.updateEnabledCategories();
}
/**
* Updates internal data based on {@link Setting} values.
*/
public static void updateFromImportedSettings() {
SegmentCategory.loadAllCategoriesFromSettings();
}
}

View File

@ -0,0 +1,49 @@
package app.revanced.extension.music.sponsorblock.objects;
import static app.revanced.extension.shared.utils.StringRef.sf;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.utils.StringRef;
public enum CategoryBehaviour {
SKIP_AUTOMATICALLY("skip", 2, true, sf("revanced_sb_skip_automatically")),
// ignored categories are not exported to json, and ignore is the default behavior when importing
IGNORE("ignore", -1, false, sf("revanced_sb_skip_ignore"));
/**
* ReVanced specific value.
*/
@NonNull
public final String reVancedKeyValue;
/**
* Desktop specific value.
*/
public final int desktopKeyValue;
/**
* If the segment should skip automatically
*/
public final boolean skipAutomatically;
@NonNull
public final StringRef description;
CategoryBehaviour(String reVancedKeyValue, int desktopKeyValue, boolean skipAutomatically, StringRef description) {
this.reVancedKeyValue = Objects.requireNonNull(reVancedKeyValue);
this.desktopKeyValue = desktopKeyValue;
this.skipAutomatically = skipAutomatically;
this.description = Objects.requireNonNull(description);
}
@Nullable
public static CategoryBehaviour byReVancedKeyValue(@NonNull String keyValue) {
for (CategoryBehaviour behaviour : values()) {
if (behaviour.reVancedKeyValue.equals(keyValue)) {
return behaviour;
}
}
return null;
}
}

View File

@ -0,0 +1,293 @@
package app.revanced.extension.music.sponsorblock.objects;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_FILLER;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_FILLER_COLOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTERACTION;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTERACTION_COLOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTRO;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTRO_COLOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_OUTRO;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_OUTRO_COLOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_PREVIEW;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_PREVIEW_COLOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SELF_PROMO;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SPONSOR;
import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SPONSOR_COLOR;
import static app.revanced.extension.shared.utils.StringRef.sf;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils;
public enum SegmentCategory {
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_segments_sponsor_sum"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR),
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_segments_selfpromo_sum"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR),
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_segments_interaction_sum"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR),
INTRO("intro", sf("revanced_sb_segments_intro"), sf("revanced_sb_segments_intro_sum"),
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR),
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_segments_outro_sum"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR),
PREVIEW("preview", sf("revanced_sb_segments_preview"), sf("revanced_sb_segments_preview_sum"),
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR),
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_segments_filler_sum"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR),
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_segments_nomusic_sum"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR);
private static final SegmentCategory[] categoriesWithoutUnsubmitted = new SegmentCategory[]{
SPONSOR,
SELF_PROMO,
INTERACTION,
INTRO,
OUTRO,
PREVIEW,
FILLER,
MUSIC_OFFTOPIC,
};
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length);
/**
* Categories currently enabled, formatted for an API call
*/
public static String sponsorBlockAPIFetchCategories = "[]";
static {
for (SegmentCategory value : categoriesWithoutUnsubmitted)
mValuesMap.put(value.keyValue, value);
}
@NonNull
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted;
}
@Nullable
public static SegmentCategory byCategoryKey(@NonNull String key) {
return mValuesMap.get(key);
}
/**
* Must be called if behavior of any category is changed
*/
public static void updateEnabledCategories() {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "updateEnabledCategories");
SegmentCategory[] categories = categoriesWithoutUnsubmitted();
List<String> enabledCategories = new ArrayList<>(categories.length);
for (SegmentCategory category : categories) {
if (category.behaviour != CategoryBehaviour.IGNORE) {
enabledCategories.add(category.keyValue);
}
}
//"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]";
if (enabledCategories.isEmpty())
sponsorBlockAPIFetchCategories = "[]";
else
sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
}
public static void loadAllCategoriesFromSettings() {
for (SegmentCategory category : values()) {
category.loadFromSettings();
}
updateEnabledCategories();
}
@NonNull
public final String keyValue;
@NonNull
private final StringSetting behaviorSetting;
@NonNull
private final StringSetting colorSetting;
@NonNull
public final StringRef title;
@NonNull
public final StringRef description;
/**
* Skip button text, if the skip occurs in the first quarter of the video
*/
@NonNull
public final StringRef skipButtonTextBeginning;
/**
* Skip button text, if the skip occurs in the middle half of the video
*/
@NonNull
public final StringRef skipButtonTextMiddle;
/**
* Skip button text, if the skip occurs in the last quarter of the video
*/
@NonNull
public final StringRef skipButtonTextEnd;
/**
* Skipped segment toast, if the skip occurred in the first quarter of the video
*/
@NonNull
public final StringRef skippedToastBeginning;
/**
* Skipped segment toast, if the skip occurred in the middle half of the video
*/
@NonNull
public final StringRef skippedToastMiddle;
/**
* Skipped segment toast, if the skip occurred in the last quarter of the video
*/
@NonNull
public final StringRef skippedToastEnd;
@NonNull
public final Paint paint;
/**
* Value must be changed using {@link #setColor(String)}.
*/
public int color;
/**
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
* Caller must also {@link #updateEnabledCategories()}.
*/
@NonNull
public CategoryBehaviour behaviour = CategoryBehaviour.SKIP_AUTOMATICALLY;
SegmentCategory(String keyValue, StringRef title, StringRef description,
StringRef skipButtonText,
StringRef skippedToastText,
StringSetting behavior, StringSetting color) {
this(keyValue, title, description,
skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText,
behavior, color);
}
SegmentCategory(String keyValue, StringRef title, StringRef description,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
StringSetting behavior, StringSetting color) {
this.keyValue = Objects.requireNonNull(keyValue);
this.title = Objects.requireNonNull(title);
this.description = Objects.requireNonNull(description);
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
this.skipButtonTextMiddle = Objects.requireNonNull(skipButtonTextMiddle);
this.skipButtonTextEnd = Objects.requireNonNull(skipButtonTextEnd);
this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning);
this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle);
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviorSetting = Objects.requireNonNull(behavior);
this.colorSetting = Objects.requireNonNull(color);
this.paint = new Paint();
loadFromSettings();
}
private void loadFromSettings() {
String behaviorString = behaviorSetting.get();
CategoryBehaviour savedBehavior = CategoryBehaviour.byReVancedKeyValue(behaviorString);
if (savedBehavior == null) {
Logger.printException(() -> "Invalid behavior: " + behaviorString);
behaviorSetting.resetToDefault();
loadFromSettings();
return;
}
this.behaviour = savedBehavior;
String colorString = colorSetting.get();
try {
setColor(colorString);
} catch (Exception ex) {
Logger.printException(() -> "Invalid color: " + colorString, ex);
colorSetting.resetToDefault();
loadFromSettings();
}
}
public void setBehaviour(@NonNull CategoryBehaviour behaviour) {
this.behaviour = Objects.requireNonNull(behaviour);
this.behaviorSetting.save(behaviour.reVancedKeyValue);
}
/**
* @return HTML color format string
*/
@NonNull
public String colorString() {
return String.format("#%06X", color);
}
public void setColor(@NonNull String colorString) throws IllegalArgumentException {
final int color = Color.parseColor(colorString) & 0xFFFFFF;
this.color = color;
paint.setColor(color);
paint.setAlpha(255);
colorSetting.save(colorString); // Save after parsing.
}
public void resetColor() {
setColor(colorSetting.defaultValue);
}
@NonNull
private static String getCategoryColorDotHTML(int color) {
color &= 0xFFFFFF;
return String.format("<font color=\"#%06X\">⬤</font>", color);
}
/**
* @noinspection deprecation
*/
@NonNull
public static Spanned getCategoryColorDot(int color) {
return Html.fromHtml(getCategoryColorDotHTML(color));
}
@NonNull
public Spanned getCategoryColorDot() {
return getCategoryColorDot(color);
}
/**
* @param segmentStartTime video time the segment category started
* @param videoLength length of the video
* @return 'skipped segment' toast message
*/
@NonNull
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) {
return skippedToastBeginning; // video is still loading. Assume it's the beginning
}
final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) {
return skippedToastBeginning;
} else if (position < 0.75f) {
return skippedToastMiddle;
}
return skippedToastEnd;
}
}

View File

@ -0,0 +1,102 @@
package app.revanced.extension.music.sponsorblock.objects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.music.shared.VideoInformation;
public class SponsorSegment implements Comparable<SponsorSegment> {
@NonNull
public final SegmentCategory category;
/**
* NULL if segment is unsubmitted
*/
@Nullable
public final String UUID;
public final long start;
public final long end;
public final boolean isLocked;
public boolean didAutoSkipped = false;
public SponsorSegment(@NonNull SegmentCategory category, @Nullable String UUID, long start, long end, boolean isLocked) {
this.category = category;
this.UUID = UUID;
this.start = start;
this.end = end;
this.isLocked = isLocked;
}
public boolean shouldAutoSkip() {
return category.behaviour.skipAutomatically;
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
*/
public boolean startIsNear(long videoTime, long nearThreshold) {
return Math.abs(start - videoTime) <= nearThreshold;
}
/**
* @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number
*/
public boolean endIsNear(long videoTime, long nearThreshold) {
return Math.abs(end - videoTime) <= nearThreshold;
}
/**
* @return if the segment is completely contained inside this segment
*/
public boolean containsSegment(SponsorSegment other) {
return start <= other.start && other.end <= end;
}
/**
* @return the length of this segment, in milliseconds. Always a positive number.
*/
public long length() {
return end - start;
}
/**
* @return 'skipped segment' toast message
*/
@NonNull
public String getSkippedToastText() {
return category.getSkippedToastText(start, VideoInformation.getVideoLength()).toString();
}
@Override
public int compareTo(SponsorSegment o) {
// If both segments start at the same time, then sort with the longer segment first.
// This keeps the seekbar drawing correct since it draws the segments using the sorted order.
return start == o.start ? Long.compare(o.length(), length()) : Long.compare(start, o.start);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SponsorSegment other)) return false;
return Objects.equals(UUID, other.UUID)
&& category == other.category
&& start == other.start
&& end == other.end;
}
@Override
public int hashCode() {
return Objects.hashCode(UUID);
}
@NonNull
@Override
public String toString() {
return "SponsorSegment{"
+ "category=" + category
+ ", start=" + start
+ ", end=" + end
+ '}';
}
}

View File

@ -0,0 +1,145 @@
package app.revanced.extension.music.sponsorblock.requests;
import static app.revanced.extension.shared.utils.StringRef.str;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.sponsorblock.SponsorBlockSettings;
import app.revanced.extension.music.sponsorblock.objects.SegmentCategory;
import app.revanced.extension.music.sponsorblock.objects.SponsorSegment;
import app.revanced.extension.shared.requests.Requester;
import app.revanced.extension.shared.requests.Route;
import app.revanced.extension.shared.sponsorblock.requests.SBRoutes;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
public class SBRequester {
/**
* TCP timeout
*/
private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 7000;
/**
* HTTP response timeout
*/
private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 10000;
/**
* Response code of a successful API call
*/
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
private SBRequester() {
}
private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) {
if (Settings.SB_TOAST_ON_CONNECTION_ERROR.get()) {
Utils.showToastShort(toastMessage);
}
if (ex != null) {
Logger.printInfo(() -> toastMessage, ex);
}
}
@NonNull
public static SponsorSegment[] getSegments(@NonNull String videoId) {
Utils.verifyOffMainThread();
List<SponsorSegment> segments = new ArrayList<>();
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SegmentCategory.sponsorBlockAPIFetchCategories);
final int responseCode = connection.getResponseCode();
if (responseCode == HTTP_STATUS_CODE_SUCCESS) {
JSONArray responseArray = Requester.parseJSONArray(connection);
final long minSegmentDuration = 0;
for (int i = 0, length = responseArray.length(); i < length; i++) {
JSONObject obj = (JSONObject) responseArray.get(i);
JSONArray segment = obj.getJSONArray("segment");
final long start = (long) (segment.getDouble(0) * 1000);
final long end = (long) (segment.getDouble(1) * 1000);
String uuid = obj.getString("UUID");
final boolean locked = obj.getInt("locked") == 1;
String categoryKey = obj.getString("category");
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
if (category == null) {
Logger.printException(() -> "Received unknown category: " + categoryKey); // should never happen
} else if ((end - start) >= minSegmentDuration) {
segments.add(new SponsorSegment(category, uuid, start, end, locked));
}
}
Logger.printDebug(() -> {
StringBuilder builder = new StringBuilder("Downloaded segments:");
for (SponsorSegment segment : segments) {
builder.append('\n').append(segment);
}
return builder.toString();
});
runVipCheckInBackgroundIfNeeded();
} else if (responseCode == 404) {
// no segments are found. a normal response
Logger.printDebug(() -> "No segments found for video: " + videoId);
} else {
handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_status", responseCode), null);
connection.disconnect(); // something went wrong, might as well disconnect
}
} catch (SocketTimeoutException ex) {
handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_timeout"), ex);
} catch (IOException ex) {
handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_generic"), ex);
} catch (Exception ex) {
// Should never happen
Logger.printException(() -> "getSegments failure", ex);
}
return segments.toArray(new SponsorSegment[0]);
}
public static void runVipCheckInBackgroundIfNeeded() {
if (!SponsorBlockSettings.userHasSBPrivateId()) {
return; // User cannot be a VIP. User has never voted, created any segments, or has imported a SB user id.
}
long now = System.currentTimeMillis();
if (now < (Settings.SB_LAST_VIP_CHECK.get() + TimeUnit.DAYS.toMillis(3))) {
return;
}
Utils.runOnBackgroundThread(() -> {
try {
JSONObject json = getJSONObject(SponsorBlockSettings.getSBPrivateUserID());
boolean vip = json.getBoolean("vip");
Settings.SB_USER_IS_VIP.save(vip);
Settings.SB_LAST_VIP_CHECK.save(now);
} catch (IOException ex) {
Logger.printInfo(() -> "Failed to check VIP (network error)", ex); // info, so no error toast is shown
} catch (Exception ex) {
Logger.printException(() -> "Failed to check VIP", ex); // should never happen
}
});
}
// helpers
private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException {
HttpURLConnection connection = Requester.getConnectionFromRoute(Settings.SB_API_URL.get(), route, params);
connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS);
connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS);
return connection;
}
private static JSONObject getJSONObject(String... params) throws IOException, JSONException {
return Requester.parseJSONObject(getConnectionFromRoute(SBRoutes.IS_USER_VIP, params));
}
}

View File

@ -0,0 +1,46 @@
package app.revanced.extension.music.utils;
import android.app.AlertDialog;
import android.content.Context;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.utils.PackageUtils;
public class ExtendedUtils extends PackageUtils {
public static boolean isSpoofingToLessThan(@NonNull String versionName) {
if (!Settings.SPOOF_APP_VERSION.get())
return false;
return isVersionToLessThan(Settings.SPOOF_APP_VERSION_TARGET.get(), versionName);
}
private static int dpToPx(float dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
@SuppressWarnings("deprecation")
public static AlertDialog.Builder getDialogBuilder(@NonNull Context context) {
return new AlertDialog.Builder(context, isSDKAbove(22)
? android.R.style.Theme_DeviceDefault_Dialog_Alert
: AlertDialog.THEME_DEVICE_DEFAULT_DARK
);
}
public static FrameLayout.LayoutParams getLayoutParams() {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
int left_margin = dpToPx(20);
int top_margin = dpToPx(10);
int right_margin = dpToPx(20);
int bottom_margin = dpToPx(4);
params.setMargins(left_margin, top_margin, right_margin, bottom_margin);
return params;
}
}

View File

@ -0,0 +1,36 @@
package app.revanced.extension.music.utils;
import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.NonNull;
import java.util.Objects;
public class RestartUtils {
public static void restartApp(@NonNull Activity activity) {
final Intent intent = Objects.requireNonNull(activity.getPackageManager().getLaunchIntentForPackage(activity.getPackageName()));
final Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
activity.finishAffinity();
activity.startActivity(mainIntent);
Runtime.getRuntime().exit(0);
}
public static void showRestartDialog(@NonNull Activity activity) {
showRestartDialog(activity, "revanced_extended_restart_message", 0);
}
public static void showRestartDialog(@NonNull Activity activity, @NonNull String message, long delay) {
getDialogBuilder(activity)
.setMessage(str(message))
.setPositiveButton(android.R.string.ok, (dialog, id) -> runOnMainThreadDelayed(() -> restartApp(activity), delay))
.setNegativeButton(android.R.string.cancel, null)
.show();
}
}

View File

@ -0,0 +1,87 @@
package app.revanced.extension.music.utils;
import static app.revanced.extension.music.settings.preference.ExternalDownloaderPreference.checkPackageIsEnabled;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.annotation.SuppressLint;
import android.content.Context;
import android.media.AudioManager;
import android.util.Log;
import androidx.annotation.NonNull;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.IntentUtils;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class VideoUtils extends IntentUtils {
private static final StringSetting externalDownloaderPackageName =
Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME;
public static void launchExternalDownloader() {
launchExternalDownloader(VideoInformation.getVideoId());
}
public static void launchExternalDownloader(@NonNull String videoId) {
try {
String downloaderPackageName = externalDownloaderPackageName.get().trim();
if (downloaderPackageName.isEmpty()) {
externalDownloaderPackageName.resetToDefault();
downloaderPackageName = externalDownloaderPackageName.defaultValue;
}
if (!checkPackageIsEnabled()) {
return;
}
final String content = String.format("https://music.youtube.com/watch?v=%s", videoId);
launchExternalDownloader(content, downloaderPackageName);
} catch (Exception ex) {
Logger.printException(() -> "launchExternalDownloader failure", ex);
}
}
@SuppressLint("IntentReset")
public static void openInYouTube() {
final String videoId = VideoInformation.getVideoId();
if (videoId.isEmpty()) {
showToastShort(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_warning"));
return;
}
if (context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE) instanceof AudioManager audioManager) {
audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
}
String url = String.format("vnd.youtube://%s", videoId);
if (Settings.REPLACE_FLYOUT_MENU_DISMISS_QUEUE_CONTINUE_WATCH.get()) {
long seconds = VideoInformation.getVideoTime() / 1000;
url += String.format("?t=%s", seconds);
}
launchView(url);
}
public static void openInYouTubeMusic(@NonNull String songId) {
final String url = String.format("vnd.youtube.music://%s", songId);
launchView(url, context.getPackageName());
}
/**
* Rest of the implementation added by patch.
*/
public static void shuffleTracks() {
Log.d("Extended: VideoUtils", "Tracks are shuffled");
}
/**
* Rest of the implementation added by patch.
*/
public static void showPlaybackSpeedFlyoutMenu() {
Logger.printDebug(() -> "Playback speed flyout menu opened");
}
}

View File

@ -0,0 +1,42 @@
package app.revanced.extension.reddit.patches;
import com.reddit.domain.model.ILink;
import java.util.ArrayList;
import java.util.List;
import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused")
public final class GeneralAdsPatch {
private static List<?> filterChildren(final Iterable<?> links) {
final List<Object> filteredList = new ArrayList<>();
for (Object item : links) {
if (item instanceof ILink iLink && iLink.getPromoted()) continue;
filteredList.add(item);
}
return filteredList;
}
public static boolean hideCommentAds() {
return Settings.HIDE_COMMENT_ADS.get();
}
public static List<?> hideOldPostAds(List<?> list) {
if (!Settings.HIDE_OLD_POST_ADS.get())
return list;
return filterChildren(list);
}
public static void hideNewPostAds(ArrayList<Object> arrayList, Object object) {
if (Settings.HIDE_NEW_POST_ADS.get())
return;
arrayList.add(object);
}
}

View File

@ -0,0 +1,53 @@
package app.revanced.extension.reddit.patches;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import app.revanced.extension.reddit.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public final class NavigationButtonsPatch {
public static List<?> hideNavigationButtons(List<?> list) {
try {
for (NavigationButton button : NavigationButton.values()) {
if (button.enabled && list.size() > button.index) {
list.remove(button.index);
}
}
} catch (Exception exception) {
Logger.printException(() -> "Failed to remove button list", exception);
}
return list;
}
public static void hideNavigationButtons(ViewGroup viewGroup) {
try {
if (viewGroup == null) return;
for (NavigationButton button : NavigationButton.values()) {
if (button.enabled && viewGroup.getChildCount() > button.index) {
View view = viewGroup.getChildAt(button.index);
if (view != null) view.setVisibility(View.GONE);
}
}
} catch (Exception exception) {
Logger.printException(() -> "Failed to remove button view", exception);
}
}
private enum NavigationButton {
CHAT(Settings.HIDE_CHAT_BUTTON.get(), 3),
CREATE(Settings.HIDE_CREATE_BUTTON.get(), 2),
DISCOVER(Settings.HIDE_DISCOVER_BUTTON.get(), 1);
private final boolean enabled;
private final int index;
NavigationButton(final boolean enabled, final int index) {
this.enabled = enabled;
this.index = index;
}
}
}

View File

@ -0,0 +1,30 @@
package app.revanced.extension.reddit.patches;
import android.net.Uri;
import app.revanced.extension.reddit.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public final class OpenLinksDirectlyPatch {
/**
* Parses the given Reddit redirect uri by extracting the redirect query.
*
* @param uri The Reddit redirect uri.
* @return The redirect query.
*/
public static Uri parseRedirectUri(Uri uri) {
try {
if (Settings.OPEN_LINKS_DIRECTLY.get()) {
final String parsedUri = uri.getQueryParameter("url");
if (parsedUri != null && !parsedUri.isEmpty())
return Uri.parse(parsedUri);
}
} catch (Exception e) {
Logger.printException(() -> "Can not parse URL: " + uri, e);
}
return uri;
}
}

View File

@ -0,0 +1,33 @@
package app.revanced.extension.reddit.patches;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import app.revanced.extension.reddit.settings.Settings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class OpenLinksExternallyPatch {
/**
* Override 'CustomTabsIntent', in order to open links in the default browser.
* Instead of doing CustomTabsActivity,
*
* @param activity The activity, to start an Intent.
* @param uri The URL to be opened in the default browser.
*/
public static boolean openLinksExternally(Activity activity, Uri uri) {
try {
if (activity != null && uri != null && Settings.OPEN_LINKS_EXTERNALLY.get()) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);
activity.startActivity(intent);
return true;
}
} catch (Exception e) {
Logger.printException(() -> "Can not open URL: " + uri, e);
}
return false;
}
}

View File

@ -0,0 +1,14 @@
package app.revanced.extension.reddit.patches;
import java.util.Collections;
import java.util.List;
import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused")
public final class RecentlyVisitedShelfPatch {
public static List<?> hideRecentlyVisitedShelf(List<?> list) {
return Settings.HIDE_RECENTLY_VISITED_SHELF.get() ? Collections.emptyList() : list;
}
}

View File

@ -0,0 +1,12 @@
package app.revanced.extension.reddit.patches;
import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused")
public final class RecommendedCommunitiesPatch {
public static boolean hideRecommendedCommunitiesShelf() {
return Settings.HIDE_RECOMMENDED_COMMUNITIES_SHELF.get();
}
}

View File

@ -0,0 +1,41 @@
package app.revanced.extension.reddit.patches;
import static app.revanced.extension.shared.utils.StringRef.str;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import app.revanced.extension.reddit.settings.Settings;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class RemoveSubRedditDialogPatch {
public static void confirmDialog(@NonNull TextView textView) {
if (!Settings.REMOVE_NSFW_DIALOG.get())
return;
if (!textView.getText().toString().equals(str("nsfw_continue_non_anonymously")))
return;
clickViewDelayed(textView);
}
public static void dismissDialog(View cancelButtonView) {
if (!Settings.REMOVE_NOTIFICATION_DIALOG.get())
return;
clickViewDelayed(cancelButtonView);
}
private static void clickViewDelayed(View view) {
Utils.runOnMainThreadDelayed(() -> {
if (view != null) {
view.setSoundEffectsEnabled(false);
view.performClick();
}
}, 0);
}
}

View File

@ -0,0 +1,12 @@
package app.revanced.extension.reddit.patches;
import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused")
public final class SanitizeUrlQueryPatch {
public static boolean stripQueryParameters() {
return Settings.SANITIZE_URL_QUERY.get();
}
}

View File

@ -0,0 +1,11 @@
package app.revanced.extension.reddit.patches;
import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused")
public class ScreenshotPopupPatch {
public static boolean disableScreenshotPopup() {
return Settings.DISABLE_SCREENSHOT_POPUP.get();
}
}

View File

@ -0,0 +1,16 @@
package app.revanced.extension.reddit.patches;
import android.view.View;
import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused")
public class ToolBarButtonPatch {
public static void hideToolBarButton(View view) {
if (!Settings.HIDE_TOOLBAR_BUTTON.get())
return;
view.setVisibility(View.GONE);
}
}

View File

@ -0,0 +1,35 @@
package app.revanced.extension.reddit.settings;
import android.app.Activity;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import app.revanced.extension.reddit.settings.preference.ReVancedPreferenceFragment;
/**
* @noinspection ALL
*/
public class ActivityHook {
public static void initialize(Activity activity) {
SettingsStatus.load();
final int fragmentId = View.generateViewId();
final FrameLayout fragment = new FrameLayout(activity);
fragment.setLayoutParams(new FrameLayout.LayoutParams(-1, -1));
fragment.setId(fragmentId);
final LinearLayout linearLayout = new LinearLayout(activity);
linearLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setFitsSystemWindows(true);
linearLayout.setTransitionGroup(true);
linearLayout.addView(fragment);
activity.setContentView(linearLayout);
activity.getFragmentManager()
.beginTransaction()
.replace(fragmentId, new ReVancedPreferenceFragment())
.commit();
}
}

View File

@ -0,0 +1,30 @@
package app.revanced.extension.reddit.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
public class Settings extends BaseSettings {
// Ads
public static final BooleanSetting HIDE_COMMENT_ADS = new BooleanSetting("revanced_hide_comment_ads", TRUE, true);
public static final BooleanSetting HIDE_OLD_POST_ADS = new BooleanSetting("revanced_hide_old_post_ads", TRUE, true);
public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true);
// Layout
public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE);
public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true);
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true);
public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true);
public static final BooleanSetting HIDE_RECENTLY_VISITED_SHELF = new BooleanSetting("revanced_hide_recently_visited_shelf", FALSE);
public static final BooleanSetting HIDE_RECOMMENDED_COMMUNITIES_SHELF = new BooleanSetting("revanced_hide_recommended_communities_shelf", FALSE, true);
public static final BooleanSetting HIDE_TOOLBAR_BUTTON = new BooleanSetting("revanced_hide_toolbar_button", FALSE, true);
public static final BooleanSetting REMOVE_NSFW_DIALOG = new BooleanSetting("revanced_remove_nsfw_dialog", FALSE, true);
public static final BooleanSetting REMOVE_NOTIFICATION_DIALOG = new BooleanSetting("revanced_remove_notification_dialog", FALSE, true);
// Miscellaneous
public static final BooleanSetting OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_open_links_directly", TRUE);
public static final BooleanSetting OPEN_LINKS_EXTERNALLY = new BooleanSetting("revanced_open_links_externally", TRUE);
public static final BooleanSetting SANITIZE_URL_QUERY = new BooleanSetting("revanced_sanitize_url_query", TRUE);
}

View File

@ -0,0 +1,78 @@
package app.revanced.extension.reddit.settings;
@SuppressWarnings("unused")
public class SettingsStatus {
public static boolean generalAdsEnabled = false;
public static boolean navigationButtonsEnabled = false;
public static boolean openLinksDirectlyEnabled = false;
public static boolean openLinksExternallyEnabled = false;
public static boolean recentlyVisitedShelfEnabled = false;
public static boolean recommendedCommunitiesShelfEnabled = false;
public static boolean sanitizeUrlQueryEnabled = false;
public static boolean screenshotPopupEnabled = false;
public static boolean subRedditDialogEnabled = false;
public static boolean toolBarButtonEnabled = false;
public static void enableGeneralAds() {
generalAdsEnabled = true;
}
public static void enableNavigationButtons() {
navigationButtonsEnabled = true;
}
public static void enableOpenLinksDirectly() {
openLinksDirectlyEnabled = true;
}
public static void enableOpenLinksExternally() {
openLinksExternallyEnabled = true;
}
public static void enableRecentlyVisitedShelf() {
recentlyVisitedShelfEnabled = true;
}
public static void enableRecommendedCommunitiesShelf() {
recommendedCommunitiesShelfEnabled = true;
}
public static void enableSubRedditDialog() {
subRedditDialogEnabled = true;
}
public static void enableSanitizeUrlQuery() {
sanitizeUrlQueryEnabled = true;
}
public static void enableScreenshotPopup() {
screenshotPopupEnabled = true;
}
public static void enableToolBarButton() {
toolBarButtonEnabled = true;
}
public static boolean adsCategoryEnabled() {
return generalAdsEnabled;
}
public static boolean layoutCategoryEnabled() {
return navigationButtonsEnabled ||
recentlyVisitedShelfEnabled ||
screenshotPopupEnabled ||
subRedditDialogEnabled ||
toolBarButtonEnabled;
}
public static boolean miscellaneousCategoryEnabled() {
return openLinksDirectlyEnabled ||
openLinksExternallyEnabled ||
sanitizeUrlQueryEnabled;
}
public static void load() {
}
}

View File

@ -0,0 +1,46 @@
package app.revanced.extension.reddit.settings.preference;
import android.content.Context;
import android.preference.Preference;
import android.preference.PreferenceScreen;
import androidx.annotation.NonNull;
import org.jetbrains.annotations.NotNull;
import app.revanced.extension.reddit.settings.preference.categories.AdsPreferenceCategory;
import app.revanced.extension.reddit.settings.preference.categories.LayoutPreferenceCategory;
import app.revanced.extension.reddit.settings.preference.categories.MiscellaneousPreferenceCategory;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
/**
* Preference fragment for ReVanced settings
*/
@SuppressWarnings("deprecation")
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
@Override
protected void syncSettingWithPreference(@NonNull @NotNull Preference pref,
@NonNull @NotNull Setting<?> setting,
boolean applySettingToPreference) {
super.syncSettingWithPreference(pref, setting, applySettingToPreference);
}
@Override
protected void initialize() {
final Context context = getContext();
// Currently no resources can be compiled for Reddit (fails with aapt error).
// So all Reddit Strings are hard coded in integrations.
restartDialogMessage = "Refresh and restart";
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
// Custom categories reference app specific Settings class.
new AdsPreferenceCategory(context, preferenceScreen);
new LayoutPreferenceCategory(context, preferenceScreen);
new MiscellaneousPreferenceCategory(context, preferenceScreen);
}
}

View File

@ -0,0 +1,17 @@
package app.revanced.extension.reddit.settings.preference;
import android.content.Context;
import android.preference.SwitchPreference;
import app.revanced.extension.shared.settings.BooleanSetting;
@SuppressWarnings("deprecation")
public class TogglePreference extends SwitchPreference {
public TogglePreference(Context context, String title, String summary, BooleanSetting setting) {
super(context);
this.setTitle(title);
this.setSummary(summary);
this.setKey(setting.key);
this.setChecked(setting.get());
}
}

View File

@ -0,0 +1,43 @@
package app.revanced.extension.reddit.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.reddit.settings.Settings;
import app.revanced.extension.reddit.settings.SettingsStatus;
import app.revanced.extension.reddit.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class AdsPreferenceCategory extends ConditionalPreferenceCategory {
public AdsPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Ads");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.adsCategoryEnabled();
}
@Override
public void addPreferences(Context context) {
addPreference(new TogglePreference(
context,
"Hide comment ads",
"Hides ads in the comments section.",
Settings.HIDE_COMMENT_ADS
));
addPreference(new TogglePreference(
context,
"Hide feed ads",
"Hides ads in the feed (old method).",
Settings.HIDE_OLD_POST_ADS
));
addPreference(new TogglePreference(
context,
"Hide feed ads",
"Hides ads in the feed (new method).",
Settings.HIDE_NEW_POST_ADS
));
}
}

View File

@ -0,0 +1,22 @@
package app.revanced.extension.reddit.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
@SuppressWarnings("deprecation")
public abstract class ConditionalPreferenceCategory extends PreferenceCategory {
public ConditionalPreferenceCategory(Context context, PreferenceScreen screen) {
super(context);
if (getSettingsStatus()) {
screen.addPreference(this);
addPreferences(context);
}
}
public abstract boolean getSettingsStatus();
public abstract void addPreferences(Context context);
}

View File

@ -0,0 +1,91 @@
package app.revanced.extension.reddit.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.reddit.settings.Settings;
import app.revanced.extension.reddit.settings.SettingsStatus;
import app.revanced.extension.reddit.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class LayoutPreferenceCategory extends ConditionalPreferenceCategory {
public LayoutPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Layout");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.layoutCategoryEnabled();
}
@Override
public void addPreferences(Context context) {
if (SettingsStatus.screenshotPopupEnabled) {
addPreference(new TogglePreference(
context,
"Disable screenshot popup",
"Disables the popup that appears when taking a screenshot.",
Settings.DISABLE_SCREENSHOT_POPUP
));
}
if (SettingsStatus.navigationButtonsEnabled) {
addPreference(new TogglePreference(
context,
"Hide Chat button",
"Hides the Chat button in the navigation bar.",
Settings.HIDE_CHAT_BUTTON
));
addPreference(new TogglePreference(
context,
"Hide Create button",
"Hides the Create button in the navigation bar.",
Settings.HIDE_CREATE_BUTTON
));
addPreference(new TogglePreference(
context,
"Hide Discover or Communities button",
"Hides the Discover or Communities button in the navigation bar.",
Settings.HIDE_DISCOVER_BUTTON
));
}
if (SettingsStatus.recentlyVisitedShelfEnabled) {
addPreference(new TogglePreference(
context,
"Hide Recently Visited shelf",
"Hides the Recently Visited shelf in the sidebar.",
Settings.HIDE_RECENTLY_VISITED_SHELF
));
}
if (SettingsStatus.recommendedCommunitiesShelfEnabled) {
addPreference(new TogglePreference(
context,
"Hide recommended communities",
"Hides the recommended communities shelves in subreddits.",
Settings.HIDE_RECOMMENDED_COMMUNITIES_SHELF
));
}
if (SettingsStatus.toolBarButtonEnabled) {
addPreference(new TogglePreference(
context,
"Hide toolbar button",
"Hide toolbar button",
Settings.HIDE_TOOLBAR_BUTTON
));
}
if (SettingsStatus.subRedditDialogEnabled) {
addPreference(new TogglePreference(
context,
"Remove NSFW warning dialog",
"Removes the NSFW warning dialog that appears when visiting a subreddit by accepting it automatically.",
Settings.REMOVE_NSFW_DIALOG
));
addPreference(new TogglePreference(
context,
"Remove notification suggestion dialog",
"Removes the notifications suggestion dialog that appears when visiting a subreddit by dismissing it automatically.",
Settings.REMOVE_NOTIFICATION_DIALOG
));
}
}
}

View File

@ -0,0 +1,49 @@
package app.revanced.extension.reddit.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.reddit.settings.Settings;
import app.revanced.extension.reddit.settings.SettingsStatus;
import app.revanced.extension.reddit.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class MiscellaneousPreferenceCategory extends ConditionalPreferenceCategory {
public MiscellaneousPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Miscellaneous");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.miscellaneousCategoryEnabled();
}
@Override
public void addPreferences(Context context) {
if (SettingsStatus.openLinksDirectlyEnabled) {
addPreference(new TogglePreference(
context,
"Open links directly",
"Skips over redirection URLs in external links.",
Settings.OPEN_LINKS_DIRECTLY
));
}
if (SettingsStatus.openLinksExternallyEnabled) {
addPreference(new TogglePreference(
context,
"Open links externally",
"Opens links in your browser instead of in the in-app-browser.",
Settings.OPEN_LINKS_EXTERNALLY
));
}
if (SettingsStatus.sanitizeUrlQueryEnabled) {
addPreference(new TogglePreference(
context,
"Sanitize sharing links",
"Removes tracking query parameters from URLs when sharing links.",
Settings.SANITIZE_URL_QUERY
));
}
}
}

View File

@ -0,0 +1,18 @@
package app.revanced.extension.shared.patches;
import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("unused")
public final class AutoCaptionsPatch {
private static boolean captionsButtonStatus;
public static boolean disableAutoCaptions() {
return BaseSettings.DISABLE_AUTO_CAPTIONS.get() &&
!captionsButtonStatus;
}
public static void setCaptionsButtonStatus(boolean status) {
captionsButtonStatus = status;
}
}

View File

@ -0,0 +1,16 @@
package app.revanced.extension.shared.patches;
import android.util.Log;
import androidx.preference.PreferenceScreen;
@SuppressWarnings("unused")
public class BaseSettingsMenuPatch {
/**
* Rest of the implementation added by patch.
*/
public static void removePreference(PreferenceScreen mPreferenceScreen, String key) {
Log.d("Extended: SettingsMenuPatch", "key: " + key);
}
}

View File

@ -0,0 +1,34 @@
package app.revanced.extension.shared.patches;
import java.util.regex.Pattern;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public final class BypassImageRegionRestrictionsPatch {
private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BaseSettings.BYPASS_IMAGE_REGION_RESTRICTIONS.get();
private static final String REPLACEMENT_IMAGE_DOMAIN = BaseSettings.BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN.get();
/**
* YouTube static images domain. Includes user and channel avatar images and community post images.
*/
private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN = Pattern.compile("(ap[1-2]|gm[1-4]|gz0|(cp|ci|gp|lh)[3-6]|sp[1-3]|yt[3-4]|(play|ccp)-lh)\\.(ggpht|googleusercontent)\\.com");
public static String overrideImageURL(String originalUrl) {
try {
if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) {
final String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
.matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN);
if (!replacement.equals(originalUrl)) {
Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'");
}
return replacement;
}
} catch (Exception ex) {
Logger.printException(() -> "overrideImageURL failure", ex);
}
return originalUrl;
}
}

View File

@ -0,0 +1,74 @@
package app.revanced.extension.shared.patches;
import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition;
import android.view.View;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class FullscreenAdsPatch {
private static final boolean hideFullscreenAdsEnabled = BaseSettings.HIDE_FULLSCREEN_ADS.get();
private static final ByteArrayFilterGroup exception =
new ByteArrayFilterGroup(
null,
"post_image_lightbox.eml" // Community post image in fullscreen
);
public static boolean disableFullscreenAds(final byte[] bytes, int type) {
if (!hideFullscreenAdsEnabled) {
return false;
}
final DialogType dialogType = DialogType.getDialogType(type);
final String dialogName = dialogType.name();
// The dialog type of a fullscreen dialog is always {@code DialogType.FULLSCREEN}
if (dialogType != DialogType.FULLSCREEN) {
Logger.printDebug(() -> "Ignoring dialogType " + dialogName);
return false;
}
// Image in community post in fullscreen is not filtered
final boolean isException = bytes != null &&
exception.check(bytes).isFiltered();
if (isException) {
Logger.printDebug(() -> "Ignoring exception");
} else {
Logger.printDebug(() -> "Blocked fullscreen ads");
}
return !isException;
}
public static void hideFullscreenAds(View view) {
hideViewBy0dpUnderCondition(
hideFullscreenAdsEnabled,
view
);
}
private enum DialogType {
NULL(0),
ALERT(1),
FULLSCREEN(2),
LAYOUT_FULLSCREEN(3);
private final int type;
DialogType(int type) {
this.type = type;
}
private static DialogType getDialogType(int type) {
for (DialogType val : values())
if (type == val.type) return val;
return DialogType.NULL;
}
}
}

View File

@ -0,0 +1,233 @@
package app.revanced.extension.shared.patches;
import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.PowerManager;
import android.provider.Settings;
import org.apache.commons.lang3.StringUtils;
import java.net.MalformedURLException;
import java.net.URL;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class GmsCoreSupport {
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
private static final String GMS_CORE_PACKAGE_NAME
= getGmsCoreVendorGroupId() + ".android.gms";
private static final Uri GMS_CORE_PROVIDER
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
private static final String DONT_KILL_MY_APP_LINK
= "https://dontkillmyapp.com";
private static final String META_SPOOF_PACKAGE_NAME =
GMS_CORE_PACKAGE_NAME + ".SPOOFED_PACKAGE_NAME";
private static void open(Activity mActivity, String queryOrLink) {
Intent intent;
try {
// Check if queryOrLink is a valid URL.
new URL(queryOrLink);
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
} catch (MalformedURLException e) {
intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.putExtra(SearchManager.QUERY, queryOrLink);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mActivity.startActivity(intent);
// Gracefully exit, otherwise the broken app will continue to run.
System.exit(0);
}
private static void showBatteryOptimizationDialog(Activity context,
String dialogMessageRef,
String positiveButtonStringRef,
DialogInterface.OnClickListener onPositiveClickListener) {
// 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(() -> new AlertDialog.Builder(context)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageRef))
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
// Allow using back button to skip the action, just in case the check can never be satisfied.
.setCancelable(true)
.show(), 100);
}
/**
* Injection point.
*/
public static void checkGmsCore(Activity mActivity) {
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 (StringUtils.equalsAny(mActivity.getPackageName(), PACKAGE_NAME_YOUTUBE, PACKAGE_NAME_YOUTUBE_MUSIC)) {
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 be relaunched
// with the appearance of a hung app.
}
// Verify GmsCore is installed.
try {
PackageManager manager = mActivity.getPackageManager();
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printInfo(() -> "GmsCore was not found");
// Cannot show a dialog and must show a toast,
// because on some installations the app crashes before a dialog can be displayed.
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
open(mActivity, getGmsCoreDownload());
return;
}
if (contentProviderClientUnAvailable(mActivity)) {
Logger.printInfo(() -> "GmsCore is not running in the background");
showBatteryOptimizationDialog(mActivity,
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
"gms_core_dialog_open_website_text",
(dialog, id) -> open(mActivity, DONT_KILL_MY_APP_LINK));
return;
}
// Check if GmsCore is whitelisted from battery optimizations.
if (batteryOptimizationsEnabled(mActivity)) {
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
showBatteryOptimizationDialog(mActivity,
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
"gms_core_dialog_continue_text",
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(mActivity));
}
} catch (Exception ex) {
Logger.printException(() -> "checkGmsCore failure", ex);
}
}
/**
* @return If GmsCore is not running in the background.
*/
@SuppressWarnings("deprecation")
private static boolean contentProviderClientUnAvailable(Context context) {
// Check if GmsCore is running in the background.
// Do this check before the battery optimization check.
if (isSDKAbove(24)) {
try (ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
return client == null;
}
} else {
ContentProviderClient client = null;
try {
//noinspection resource
client = context.getContentResolver()
.acquireContentProviderClient(GMS_CORE_PROVIDER);
return client == null;
} finally {
if (client != null) client.release();
}
}
}
@SuppressLint("BatteryLife") // Permission is part of GmsCore
private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity mActivity) {
if (!isSDKAbove(23)) return;
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null));
mActivity.startActivityForResult(intent, 0);
}
/**
* @return If GmsCore is not whitelisted from battery optimizations.
*/
private static boolean batteryOptimizationsEnabled(Context context) {
if (isSDKAbove(23) && context.getSystemService(Context.POWER_SERVICE) instanceof PowerManager powerManager) {
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
}
return false;
}
/**
* Injection point.
*/
public static String spoofPackageName(Context context) {
// Package name of ReVanced.
final String packageName = context.getPackageName();
try {
final PackageManager packageManager = context.getPackageManager();
// Package name of YouTube or YouTube Music.
String originalPackageName;
try {
originalPackageName = packageManager
.getPackageInfo(packageName, PackageManager.GET_META_DATA)
.applicationInfo
.metaData
.getString(META_SPOOF_PACKAGE_NAME);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printDebug(() -> "Failed to parsing metadata");
return packageName;
}
if (StringUtils.isBlank(originalPackageName)) {
Logger.printDebug(() -> "Failed to parsing spoofed package name");
return packageName;
}
try {
packageManager.getPackageInfo(originalPackageName, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printDebug(() -> "Original app '" + originalPackageName + "' was not found");
return packageName;
}
Logger.printDebug(() -> "Package name of '" + packageName + "' spoofed to '" + originalPackageName + "'");
return originalPackageName;
} catch (Exception ex) {
Logger.printException(() -> "spoofPackageName failure", ex);
}
return packageName;
}
private static String getGmsCoreDownload() {
final String vendorGroupId = getGmsCoreVendorGroupId();
return switch (vendorGroupId) {
case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest";
case "com.mgoogle" -> "https://github.com/inotia00/VancedMicroG/releases/latest";
default -> vendorGroupId + ".android.gms";
};
}
// Modified by a patch. Do not touch.
private static String getGmsCoreVendorGroupId() {
return "app.revanced";
}
}

View File

@ -0,0 +1,113 @@
package app.revanced.extension.shared.patches;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import android.text.SpannableString;
import android.text.Spanned;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.returnyoutubeusername.requests.ChannelRequest;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class ReturnYouTubeUsernamePatch {
private static final boolean RETURN_YOUTUBE_USERNAME_ENABLED = BaseSettings.RETURN_YOUTUBE_USERNAME_ENABLED.get();
private static final Boolean RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = BaseSettings.RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.get().userNameFirst;
private static final String YOUTUBE_API_KEY = BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.get();
private static final String AUTHOR_BADGE_PATH = "|author_badge.eml|";
private static volatile String lastFetchedHandle = "";
/**
* Injection point.
*
* @param original The original string before the SpannableString is built.
*/
public static CharSequence preFetchLithoText(@NonNull Object conversionContext,
@NonNull CharSequence original) {
onLithoTextLoaded(conversionContext, original, true);
return original;
}
/**
* Injection point.
*
* @param original The original string after the SpannableString is built.
*/
@NonNull
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
@NonNull CharSequence original) {
return onLithoTextLoaded(conversionContext, original, false);
}
@NonNull
private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
@NonNull CharSequence original,
boolean fetchNeeded) {
try {
if (!RETURN_YOUTUBE_USERNAME_ENABLED) {
return original;
}
if (YOUTUBE_API_KEY.isEmpty()) {
Logger.printDebug(() -> "API key is empty");
return original;
}
// In comments, the path to YouTube Handle(@youtube) always includes [AUTHOR_BADGE_PATH].
if (!conversionContext.toString().contains(AUTHOR_BADGE_PATH)) {
return original;
}
String handle = original.toString();
if (fetchNeeded && !handle.equals(lastFetchedHandle)) {
lastFetchedHandle = handle;
// Get the original username using YouTube Data API v3.
ChannelRequest.fetchRequestIfNeeded(handle, YOUTUBE_API_KEY, RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT);
return original;
}
// If the username is not in the cache, put it in the cache.
ChannelRequest channelRequest = ChannelRequest.getRequestForHandle(handle);
if (channelRequest == null) {
Logger.printDebug(() -> "ChannelRequest is null, handle:" + handle);
return original;
}
final String userName = channelRequest.getStream();
if (userName == null) {
Logger.printDebug(() -> "ChannelRequest Stream is null, handle:" + handle);
return original;
}
final CharSequence copiedSpannableString = copySpannableString(original, userName);
Logger.printDebug(() -> "Replaced: '" + original + "' with: '" + copiedSpannableString + "'");
return copiedSpannableString;
} catch (Exception ex) {
Logger.printException(() -> "onLithoTextLoaded failure", ex);
}
return original;
}
private static CharSequence copySpannableString(CharSequence original, String userName) {
if (original instanceof Spanned spanned) {
SpannableString newString = new SpannableString(userName);
Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
for (Object span : spans) {
int flags = spanned.getSpanFlags(span);
newString.setSpan(span, 0, newString.length(), flags);
}
return newString;
}
return original;
}
public enum DisplayFormat {
USERNAME_ONLY(null),
USERNAME_HANDLE(TRUE),
HANDLE_USERNAME(FALSE);
final Boolean userNameFirst;
DisplayFormat(Boolean userNameFirst) {
this.userNameFirst = userNameFirst;
}
}
}

View File

@ -0,0 +1,50 @@
package app.revanced.extension.shared.patches;
import android.content.Intent;
import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("all")
public final class SanitizeUrlQueryPatch {
/**
* This tracking parameter is mainly used.
*/
private static final String NEW_TRACKING_REGEX = ".si=.+";
/**
* This tracking parameter is outdated.
* Used when patching old versions or enabling spoof app version.
*/
private static final String OLD_TRACKING_REGEX = ".feature=.+";
private static final String URL_PROTOCOL = "http";
/**
* Strip query parameters from a given URL string.
* <p>
* URL example containing tracking parameter:
* https://youtu.be/ZWgr7qP6yhY?si=kKA_-9cygieuFY7R
* https://youtu.be/ZWgr7qP6yhY?feature=shared
* https://youtube.com/watch?v=ZWgr7qP6yhY&si=s_PZAxnJHKX1Mc8C
* https://youtube.com/watch?v=ZWgr7qP6yhY&feature=shared
* https://youtube.com/playlist?list=PLBsP89CPrMeO7uztAu6YxSB10cRMpjgiY&si=N0U8xncY2ZmQoSMp
* https://youtube.com/playlist?list=PLBsP89CPrMeO7uztAu6YxSB10cRMpjgiY&feature=shared
* <p>
* Since we need to support support all these examples,
* We cannot use [URL.getpath()] or [Uri.getQueryParameter()].
*
* @param urlString URL string to strip query parameters from.
* @return URL string without query parameters if possible, otherwise the original string.
*/
public static String stripQueryParameters(final String urlString) {
if (!BaseSettings.SANITIZE_SHARING_LINKS.get())
return urlString;
return urlString.replaceAll(NEW_TRACKING_REGEX, "").replaceAll(OLD_TRACKING_REGEX, "");
}
public static void stripQueryParameters(final Intent intent, final String extraName, final String extraValue) {
intent.putExtra(extraName, extraValue.startsWith(URL_PROTOCOL)
? stripQueryParameters(extraValue)
: extraValue
);
}
}

View File

@ -0,0 +1,98 @@
package app.revanced.extension.shared.patches.components;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.utils.ByteTrieSearch;
import app.revanced.extension.shared.utils.Logger;
/**
* If you have more than 1 filter patterns, then all instances of
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
* which uses a prefix tree to give better performance.
*/
@SuppressWarnings("unused")
public class ByteArrayFilterGroup extends FilterGroup<byte[]> {
private volatile int[][] failurePatterns;
// Modified implementation from https://stackoverflow.com/a/1507813
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
// Finds the first occurrence of the pattern in the byte array using
// KMP matching algorithm.
int patternLength = pattern.length;
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
while (j > 0 && pattern[j] != data[i]) {
j = failure[j - 1];
}
if (pattern[j] == data[i]) {
j++;
}
if (j == patternLength) {
return i - patternLength + 1;
}
}
return -1;
}
private static int[] createFailurePattern(byte[] pattern) {
// Computes the failure function using a boot-strapping process,
// where the pattern is matched against itself.
final int patternLength = pattern.length;
final int[] failure = new int[patternLength];
for (int i = 1, j = 0; i < patternLength; i++) {
while (j > 0 && pattern[j] != pattern[i]) {
j = failure[j - 1];
}
if (pattern[j] == pattern[i]) {
j++;
}
failure[i] = j;
}
return failure;
}
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
super(setting, filters);
}
/**
* Converts the Strings into byte arrays. Used to search for text in binary data.
*/
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
}
private synchronized void buildFailurePatterns() {
if (failurePatterns != null)
return; // Thread race and another thread already initialized the search.
Logger.printDebug(() -> "Building failure array for: " + this);
int[][] failurePatterns = new int[filters.length][];
int i = 0;
for (byte[] pattern : filters) {
failurePatterns[i++] = createFailurePattern(pattern);
}
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
}
@Override
public FilterGroupResult check(final byte[] bytes) {
int matchedLength = 0;
int matchedIndex = -1;
if (isEnabled()) {
int[][] failures = failurePatterns;
if (failures == null) {
buildFailurePatterns(); // Lazy load.
failures = failurePatterns;
}
for (int i = 0, length = filters.length; i < length; i++) {
byte[] filter = filters[i];
matchedIndex = indexOf(bytes, filter, failures[i]);
if (matchedIndex >= 0) {
matchedLength = filter.length;
break;
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}

View File

@ -0,0 +1,14 @@
package app.revanced.extension.shared.patches.components;
import app.revanced.extension.shared.utils.ByteTrieSearch;
/**
* If searching for a single byte pattern, then it is slightly better to use
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
* than a prefix tree to search for only 1 pattern.
*/
public final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
protected ByteTrieSearch createSearchGraph() {
return new ByteTrieSearch();
}
}

View File

@ -0,0 +1,106 @@
package app.revanced.extension.shared.patches.components;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
/**
* Filters litho based components.
* <p>
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
* and {@link #addPathCallbacks(StringFilterGroup...)}.
* <p>
* To filter {@link FilterContentType#PROTOBUFFER}, 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 ByteArrayFilterGroupList} (if searching for more than 1 pattern).
* <p>
* All callbacks must be registered before the constructor completes.
*/
@SuppressWarnings("unused")
public abstract class Filter {
public enum FilterContentType {
IDENTIFIER,
PATH,
ALLVALUE,
PROTOBUFFER
}
/**
* Identifier callbacks. Do not add to this instance,
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
*/
protected final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
/**
* Path callbacks. Do not add to this instance,
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
*/
protected final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
/**
* Path callbacks. Do not add to this instance,
* and instead use {@link #addAllValueCallbacks(StringFilterGroup...)}.
*/
protected final List<StringFilterGroup> allValueCallbacks = 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));
}
/**
* Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
*/
protected final void addAllValueCallbacks(StringFilterGroup... groups) {
allValueCallbacks.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.
* <p>
* If the content is to be filtered, subclasses should always
* call this method (and never return a plain 'true').
* That way the logs will always show when a component was filtered and which filter hide it.
* <p>
* Method is called off the main thread.
*
* @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 path, @Nullable String identifier, String allValue, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (BaseSettings.ENABLE_DEBUG_LOGGING.get()) {
String filterSimpleName = getClass().getSimpleName();
if (contentType == FilterContentType.IDENTIFIER) {
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
} else if (contentType == FilterContentType.PATH) {
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
} else if (contentType == FilterContentType.ALLVALUE) {
Logger.printDebug(() -> filterSimpleName + " Filtered object: " + allValue);
}
}
return true;
}
}

View File

@ -0,0 +1,95 @@
package app.revanced.extension.shared.patches.components;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.settings.BooleanSetting;
@SuppressWarnings("unused")
public abstract class FilterGroup<T> {
public final static class FilterGroupResult {
private BooleanSetting setting;
private int matchedIndex;
private int matchedLength;
// In the future it might be useful to include which pattern matched,
// but for now that is not needed.
FilterGroupResult() {
this(null, -1, 0);
}
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
setValues(setting, matchedIndex, matchedLength);
}
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
this.setting = setting;
this.matchedIndex = matchedIndex;
this.matchedLength = matchedLength;
}
/**
* A null value if the group has no setting,
* or if no match is returned from {@link FilterGroupList#check(Object)}.
*/
public BooleanSetting getSetting() {
return setting;
}
public boolean isFiltered() {
return matchedIndex >= 0;
}
/**
* Matched index of first pattern that matched, or -1 if nothing matched.
*/
public int getMatchedIndex() {
return matchedIndex;
}
/**
* Length of the matched filter pattern.
*/
public int getMatchedLength() {
return matchedLength;
}
}
protected final BooleanSetting setting;
protected final T[] filters;
/**
* Initialize a new filter group.
*
* @param setting The associated setting.
* @param filters The filters.
*/
@SafeVarargs
public FilterGroup(final BooleanSetting setting, final T... filters) {
this.setting = setting;
this.filters = filters;
if (filters.length == 0) {
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
}
}
public boolean isEnabled() {
return setting == null || setting.get();
}
/**
* @return If {@link FilterGroupList} should include this group when searching.
* By default, all filters are included except non enabled settings that require reboot.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean includeInSearch() {
return isEnabled() || !setting.rebootApp;
}
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
}
public abstract FilterGroupResult check(final T stack);
}

View File

@ -0,0 +1,69 @@
package app.revanced.extension.shared.patches.components;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
import app.revanced.extension.shared.utils.TrieSearch;
@SuppressWarnings("unused")
public abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
private final List<T> filterGroups = new ArrayList<>();
private final TrieSearch<V> search = createSearchGraph();
@SafeVarargs
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<T> iterator() {
return filterGroups.iterator();
}
@RequiresApi(24)
@Override
public void forEach(@NonNull Consumer<? super T> action) {
filterGroups.forEach(action);
}
@RequiresApi(24)
@NonNull
@Override
public Spliterator<T> spliterator() {
return filterGroups.spliterator();
}
public FilterGroup.FilterGroupResult check(V stack) {
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
search.matches(stack, result);
return result;
}
protected abstract TrieSearch<V> createSearchGraph();
}

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