mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-04-29 14:14:36 +02:00
refactor: Bump ReVanced Patcher & merge integrations by using ReVanced Patches Gradle plugin
BREAKING CHANGE: ReVanced Patcher >= 21 required
This commit is contained in:
parent
f074c3ecc5
commit
b31865afbe
3
.editorconfig
Normal file
3
.editorconfig
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
ktlint_code_style = intellij_idea
|
||||||
|
ktlint_standard_no-wildcard-imports = disabled
|
49
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
49
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -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
110
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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
|
44
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
44
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -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
|
|
106
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
106
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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
2
.github/config.yml
vendored
@ -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
22
.github/dependabot.yml
vendored
Normal 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
|
31
.github/workflows/build_pull_request.yml
vendored
Normal file
31
.github/workflows/build_pull_request.yml
vendored
Normal 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
28
.github/workflows/open_pull_request.yml
vendored
Normal 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
60
.github/workflows/release.yml
vendored
Normal 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
|
18
.github/workflows/update-gradle-wrapper.yml
vendored
Normal file
18
.github/workflows/update-gradle-wrapper.yml
vendored
Normal 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
5
.gitignore
vendored
@ -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
3
.idea/misc.xml
generated
@ -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>
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
19
README.md
19
README.md
@ -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": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
149
build.gradle.kts
149
build.gradle.kts
@ -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"])
|
|
||||||
}
|
|
30
extensions/shared/build.gradle.kts
Normal file
30
extensions/shared/build.gradle.kts
Normal 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
9
extensions/shared/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-dontobfuscate
|
||||||
|
-dontoptimize
|
||||||
|
-keepattributes *
|
||||||
|
-keep class app.revanced.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.google.** {
|
||||||
|
*;
|
||||||
|
}
|
4
extensions/shared/src/main/AndroidManifest.xml
Normal file
4
extensions/shared/src/main/AndroidManifest.xml
Normal 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>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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|"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>()
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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
Loading…
x
Reference in New Issue
Block a user