Compare commits

..

2 Commits

Author SHA1 Message Date
semantic-release-bot
85a32e833e chore(release): 2.169.1-dev.2 [skip ci]
## [2.169.1-dev.2](https://github.com/revanced/revanced-patches/compare/v2.169.1-dev.1...v2.169.1-dev.2) (2023-04-21)

### Bug Fixes

* **"enable-android-debugging:** don't include by default ([5614651](5614651d34))
2023-04-21 16:50:56 +00:00
oSumAtrIX
5614651d34 fix("enable-android-debugging): don't include by default 2023-04-21 18:49:10 +02:00
1518 changed files with 22420 additions and 117664 deletions

View File

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

9
.gitattributes vendored
View File

@@ -1,9 +0,0 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf

73
.github/ISSUE_TEMPLATE/bug-issue.yml vendored Normal file
View File

@@ -0,0 +1,73 @@
name: 🐞 Bug report
description: Report a very clearly broken issue.
title: 'bug: <title>'
labels: [bug]
body:
- type: markdown
attributes:
value: |
# ReVanced bug report
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-patches/labels/bug).
- type: dropdown
attributes:
label: Type
options:
- Error while patching
- Error at runtime
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Bug description
description: How did you find the bug? Any additional details that might help?
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Add the steps to reproduce this bug including your environment.
placeholder: Step 1. Download some files. Step 2. ...
validations:
required: true
- type: textarea
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. Capture crash logs by running `logcat | grep AndroidRuntime`.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Screenshots or videos
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I filled out all of the requested information in this issue properly.
required: true

View File

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

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 📃 Documentation
url: https://github.com/revanced/revanced-documentation/
about: Don't know how or where to start? Check out our documentation!
- name: 🗨 Discussions
url: https://github.com/revanced/revanced-suggestions/discussions
about: Have something unspecific to ReVanced Patches in mind? Search for or start a new discussion!
about: Got something you think should change or be added? Search for or start a new discussion!

View File

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

View File

@@ -0,0 +1,41 @@
name: ⭐ Patch Request
description: Create a detailed patch request.
title: 'feat(patch): '
labels: [patch-request]
body:
- type: textarea
attributes:
label: Application
description: Which application is this patch for?
validations:
required: true
- type: textarea
attributes:
label: Issue
description: What is the issue this patch should solve?
validations:
required: true
- type: textarea
attributes:
label: Patch
description: "Describe the patch you'd like to see in detail."
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Why should your patch request should be considered? What makes it valuable to the community?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I filled out all of the requested information in this issue properly.
required: true

2
.github/config.yml vendored
View File

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

View File

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

View File

@@ -1,31 +0,0 @@
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

View File

@@ -1,4 +1,4 @@
name: Open a PR to main
name: PR to main
on:
push:
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
jobs:
pull-request:
@@ -15,17 +15,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: main
destination_branch: 'main'
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: |
This pull request will ${{ env.MESSAGE }}.
## Dependencies before merge
## Before merging this PR
- [ ] Pull translations from Crowdin
- [ ] https://github.com/revanced/revanced-integrations
pr_draft: true

View File

@@ -1,36 +0,0 @@
name: Pull strings
on:
workflow_dispatch:
jobs:
pull:
name: Pull strings
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: dev
- name: Pull strings
uses: crowdin/github-action@v2
with:
config: crowdin.yml
download_translations: true
localization_branch_name: feat/translations
create_pull_request: true
pull_request_title: "chore: Sync translations"
pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
pull_request_base_branch_name: "dev"
commit_message: "chore: Sync translations"
github_user_name: revanced-bot
github_user_email: github@revanced.app
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -1,28 +0,0 @@
name: Push strings
on:
workflow_dispatch:
push:
branches:
- dev
paths:
- patches/src/main/resources/addresources/values/strings.xml
jobs:
push:
name: Push strings
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push strings
uses: crowdin/github-action@v2
with:
config: crowdin.yml
upload_sources: true
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -6,54 +6,47 @@ on:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
release:
name: Release
permissions:
contents: write
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
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
- name: Setup JDK
uses: actions/setup-java@v3
with:
distribution: "temurin"
java-version: "17"
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
java-version: '17'
distribution: 'zulu'
cache: gradle
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Setup Android SDK
uses: amyu/setup-android@v2
with:
cache-disabled: false
sdk-version: '33'
build-tools-version: '33.0.1'
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build clean
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: 'npm'
- name: Install dependencies
run: ./gradlew generateMeta clean --no-daemon
- name: Setup semantic-release
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 }}
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npm exec semantic-release

View File

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

5
.gitignore vendored
View File

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

View File

@@ -7,13 +7,7 @@
}
],
"plugins": [
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"gradle-semantic-release-plugin",
@@ -21,10 +15,11 @@
"@semantic-release/git",
{
"assets": [
"README.md",
"CHANGELOG.md",
"gradle.properties",
],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
"patches.json"
]
}
],
[
@@ -32,8 +27,11 @@
{
"assets": [
{
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
"path": "build/libs/*.jar"
},
{
"path": "patches.json"
}
],
successComment: false
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="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="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 👋 Contribution guidelines
This document describes how to contribute to ReVanced Patches.
## 📖 Resources to help you get started
* The [documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs) contains the fundamentals
of ReVanced Patcher and how to use ReVanced Patcher to create patches
* [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
* [Issues](https://github.com/ReVanced/revanced-patches/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[Feature request issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
> **Note**
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patches.
> Good motivation has to be provided for a request to be accepted.
## 🐞 Submitting a bug report
If you encounter a bug while using ReVanced Patches, open an issue using the
[Bug report issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
## 🌐 Submitting translations
You can contribute translations at [translate.revanced.app](https://translate.revanced.app).
## 🧑‍⚖️ Guidelines for requesting or contributing patches
To maintain a high-quality and ethical collection of patches, the following guidelines for requesting
or contributing patches are effective as of September 14, 2023. Any patches present prior to this date
are unaffected by this change.
> **Note**
> We generally adhere to the guidelines outlined below. However, we may make exceptions
> in specific cases based on our discretion. Pull requests for patches that deviate from the guidelines
> will be evaluated individually. While a patch may not align with our general guidelines,
> we will consider its acceptance on a case-by-case basis, taking into account its impact on user experience
> and ethical considerations. We reserve the right to make exceptions for patches that provide significant value.
✅ Examples for acceptable patches include:
* Customizations: Feel free to contribute patches that allow users to personalize their experience
* Ad-Blocking: Patches aimed at enhancing user privacy and blocking intrusive advertisements are appreciated
* Feature additions: Patches that add new features or change behaviour to the app are welcome
❌ Examples for unacceptable patches include:
* Payment circumvention: We do not accept patches that exist solely to bypass payment for the app or any of its features
* Malicious patches: Patches that are malicious in nature are not allowed
## 📝 How to contribute
1. Before contributing, it is recommended to open an issue to discuss your change
with the maintainers of ReVanced Patches. This will help you determine whether your change is acceptable
and whether it is worth your time to implement it
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
3. Commit your changes. In case you are contributing a new patch, make sure to follow the conventions for patches
described in the [ReVanced Patcher documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs)
4. Submit a pull request to the `dev` branch of the repository and reference issues
that your pull request closes in the description of your pull request
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
it will be merged into the `dev` branch and will be included in the next release of ReVanced Patches
❤️ Thank you for considering contributing to ReVanced Patches,
ReVanced

122
README-template.md Normal file
View File

@@ -0,0 +1,122 @@
## 🧩 Patches
The official Patch bundle provided by ReVanced and the community.
> Looking for the JSON variant of this? [Click here](patches.json).
{{ table }}
## 📝 JSON Format
This section explains the JSON format for the [patches.json](patches.json) file.
The file contains an array of objects, each object representing a patch. The object contains the following properties:
| key | description |
|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | The name of the patch. |
| `description` | The description of the patch. |
| `version` | The version of the patch. |
| `excluded` | Whether the patch is excluded by default. If `true`, the patch must never be included by default. |
| `options` | An array of options for this patch. |
| `options.key` | The key of the option. |
| `options.title` | The title of the option. |
| `options.description` | The description of the option. |
| `options.required` | Whether the option is required. |
| `options.choices?` | An array of choices of the option. This may be `null` if this option has no choices. The element type of this array may be any type. It can be a `String`, `Int` or something else. |
| `dependencies` | An array of dependencies, which are patch names. |
| `compatiblePackages` | An array of packages compatible with this patch. |
| `compatiblePackages.name` | The name of the package. |
| `compatiblePackages.versions` | An array of versions of the package compatible with this patch. If empty, all versions are seemingly compatible. |
Example:
```json
[
{
"name": "remember-video-quality",
"description": "Adds the ability to remember the video quality you chose in the video quality flyout.",
"version": "0.0.1",
"excluded": false,
"options": [],
"dependencies": [
"integrations",
"video-id-hook"
],
"compatiblePackages": [
{
"name": "com.google.android.youtube",
"versions": [
"17.22.36",
"17.24.35",
"17.26.35",
"17.27.39",
"17.28.34",
"17.29.34",
"17.32.35",
"17.33.42"
]
}
]
},
{
"name": "theme",
"description": "Enables a custom theme.",
"version": "0.0.1",
"excluded": false,
"deprecated": false,
"options": [
{
"key": "theme",
"title": "Theme",
"description": "Select a theme.",
"required": true,
"choices": [
"Amoled"
]
}
],
"dependencies": [
"locale-config-fix"
],
"compatiblePackages": [
{
"name": "com.google.android.youtube",
"versions": []
}
]
},
{
"name": "custom-branding",
"description": "Changes the YouTube launcher icon and name to your choice (defaults to ReVanced).",
"version": "0.0.1",
"excluded": false,
"deprecated": false,
"options": [
{
"key": "appName",
"title": "Application Name",
"description": "The name of the application it will show on your home screen.",
"required": true,
"choices": null
},
{
"key": "appIconPath",
"title": "Application Icon Path",
"description": "A path to the icon of the application.",
"required": false,
"choices": null
}
],
"dependencies": [
"locale-config-fix"
],
"compatiblePackages": [
{
"name": "com.google.android.youtube",
"versions": []
}
]
}
]
```

557
README.md
View File

@@ -1,105 +1,488 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="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="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
## 🧩 Patches
# 🧩 ReVanced Patches
The official Patch bundle provided by ReVanced and the community.
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-patches/release.yml)
![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)
> Looking for the JSON variant of this? [Click here](patches.json).
This repository contains a collection of ReVanced Patches.
### [📦 `com.google.android.youtube`](https://play.google.com/store/apps/details?id=com.google.android.youtube)
<details>
## ❓ About
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `always-autorepeat` | Always repeats the playing video again. | 18.08.37 |
| `client-spoof` | Spoofs a patched client to allow playback. | all |
| `comments` | Hides components related to comments. | 18.08.37 |
| `copy-video-url` | Adds buttons in player to copy video links. | 18.08.37 |
| `custom-branding` | Changes the YouTube launcher icon and name to your choice (defaults to ReVanced). | all |
| `custom-video-buffer` | Lets you change the buffers of videos. | 18.08.37 |
| `custom-video-speed` | Adds more video speed options. | 18.08.37 |
| `disable-auto-captions` | Disable forced captions from being automatically enabled. | 18.08.37 |
| `disable-fullscreen-panels` | Disables video description and comments panel in fullscreen view. | 18.08.37 |
| `disable-player-popup-panels` | Disables panels from appearing automatically when going into fullscreen (playlist or live chat). | 18.08.37 |
| `disable-shorts-on-startup` | Disables playing YouTube Shorts when launching YouTube. | 18.08.37 |
| `disable-zoom-haptics` | Disables haptics when zooming. | all |
| `downloads` | Enables downloading music and videos from YouTube. | 18.08.37 |
| `enable-debugging` | Adds debugging options. | all |
| `enable-wide-searchbar` | Replaces the search icon with a wide search bar. This will hide the YouTube logo when active. | 18.08.37 |
| `general-ads` | Removes general ads. | 18.08.37 |
| `hdr-auto-brightness` | Makes the brightness of HDR videos follow the system default. | 18.08.37 |
| `hide-album-cards` | Hides the album cards below the artist description. | 18.08.37 |
| `hide-artist-card` | Hides the artist card below the searchbar. | 18.08.37 |
| `hide-autoplay-button` | Hides the autoplay button in the video player. | 18.08.37 |
| `hide-breaking-news-shelf` | Hides the breaking news shelf on the homepage tab. | 18.08.37 |
| `hide-captions-button` | Hides the captions button on video player. | 18.08.37 |
| `hide-cast-button` | Hides the cast button in the video player. | all |
| `hide-create-button` | Hides the create button in the navigation bar. | 18.08.37 |
| `hide-crowdfunding-box` | Hides the crowdfunding box between the player and video description. | 18.08.37 |
| `hide-email-address` | Hides the email address in the account switcher. | 18.08.37 |
| `hide-endscreen-cards` | Hides the suggested video cards at the end of a video in fullscreen. | 18.08.37 |
| `hide-floating-microphone-button` | Hides the floating microphone button which appears in search. | 18.08.37 |
| `hide-info-cards` | Hides info cards in videos. | 18.08.37 |
| `hide-my-mix` | Hides mix playlists. | 18.08.37 |
| `hide-player-buttons` | Adds the option to hide video player previous and next buttons. | all |
| `hide-seekbar` | Hides the seekbar. | 18.08.37 |
| `hide-shorts-button` | Hides the shorts button on the navigation bar. | 18.08.37 |
| `hide-timestamp` | Hides timestamp in video player. | 18.08.37 |
| `hide-video-action-buttons` | Adds the options to hide action buttons under a video. | 18.08.37 |
| `hide-watch-in-vr` | Hides the option to watch in VR from the player settings flyout panel. | 18.08.37 |
| `hide-watermark` | Hides creator's watermarks on videos. | 18.08.37 |
| `minimized-playback` | Enables minimized and background playback. | 18.08.37 |
| `old-quality-layout` | Enables the original video quality flyout in the video player settings | 18.08.37 |
| `open-links-externally` | Open links outside of the app directly in your browser. | 18.08.37 |
| `premium-heading` | Shows premium branding on the home screen. | all |
| `remember-playback-speed` | Adds the ability to remember the playback speed you chose in the video playback speed flyout. | 18.08.37 |
| `remember-video-quality` | Adds the ability to remember the video quality you chose in the video quality flyout. | 18.08.37 |
| `remove-player-button-background` | Removes the background from the video player buttons. | 18.08.37 |
| `return-youtube-dislike` | Shows the dislike count of videos using the Return YouTube Dislike API. | 18.08.37 |
| `seekbar-tapping` | Enables tap-to-seek on the seekbar of the video player. | 18.08.37 |
| `sponsorblock` | Integrates SponsorBlock which allows skipping video segments such as sponsored content. | 18.08.37 |
| `spoof-app-version` | Tricks YouTube into thinking, you are running an older version of the app. One of the side effects also includes restoring the old UI. | 18.08.37 |
| `spoof-signature-verification` | Spoofs a patched client to prevent playback issues. | 18.08.37 |
| `swipe-controls` | Adds volume and brightness swipe controls. | 18.08.37 |
| `tablet-mini-player` | Enables the tablet mini player layout. | 18.08.37 |
| `theme` | Applies a custom theme. | all |
| `vanced-microg-support` | Allows YouTube ReVanced to run without root and under a different package name with Vanced MicroG. | 18.08.37 |
| `video-ads` | Removes ads in the video player. | 18.08.37 |
</details>
Patches are small modifications to Android apps that allow you to change the behavior of or add new features,
block ads, customize the appearance, and much more.
### [📦 `com.google.android.apps.youtube.music`](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music)
<details>
## 💪 Features
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `background-play` | Enables playing music in the background. | all |
| `bypass-certificate-checks` | Bypasses certificate checks which prevent YouTube Music from working on Android Auto. | 5.39.52 |
| `codecs-unlock` | Adds more audio codec options. The new audio codecs usually result in better audio quality. | all |
| `compact-header` | Hides the music category bar at the top of the homepage. | all |
| `exclusive-audio-playback` | Enables the option to play music without video. | all |
| `hide-get-premium` | Removes all "Get Premium" evidences from the avatar menu. | 5.39.52 |
| `minimized-playback-music` | Enables minimized playback on Kids music. | all |
| `music-microg-support` | Allows YouTube Music ReVanced to run without root and under a different package name. | all |
| `music-video-ads` | Removes ads in the music player. | all |
| `tasteBuilder-remover` | Removes the "Tell us which artists you like" card from the home screen. | all |
| `upgrade-button-remover` | Removes the upgrade tab from the pivot bar. | all |
</details>
Some of the features the patches provide are:
### [📦 `com.ss.android.ugc.trill`](https://play.google.com/store/apps/details?id=com.ss.android.ugc.trill)
<details>
* 🚫 **Block ads**: Say goodbye to ads
***Customize your app**: Personalize the appearance of apps with various layouts and themes
* 🪄 **Add new features**: Extend the functionality of apps with lots of new features
* ⚙️ **Miscellaneous and general purpose**: Rename packages, enable debugging, disable screen capture restrictions,
export activities, etc.
***And much more!**
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `disable-login-requirement` | Do not force login. | all |
| `downloads` | Removes download restrictions and changes the default path to download to. | 27.8.3 |
| `feed-filter` | Filters tiktok videos: removing ads, removing livestreams. | 27.8.3 |
| `fix-google-login` | Allows logging in with a Google account. | all |
| `hide-ads` | Removes ads from TikTok. | all |
| `playback-speed` | Enables the playback speed option for all videos. | all |
| `settings` | Adds ReVanced settings to TikTok. | 27.8.3 |
| `show-seekbar` | Shows progress bar for all video. | all |
| `sim-spoof` | Spoofs the information which is retrieved from the sim-card. | 27.8.3 |
</details>
For a complete list of all available patches, visit [revanced.app/patches](https://revanced.app/patches).
### [📦 `com.zhiliaoapp.musically`](https://play.google.com/store/apps/details?id=com.zhiliaoapp.musically)
<details>
## 🚀 How to get started
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `disable-login-requirement` | Do not force login. | all |
| `downloads` | Removes download restrictions and changes the default path to download to. | 27.8.3 |
| `feed-filter` | Filters tiktok videos: removing ads, removing livestreams. | 27.8.3 |
| `fix-google-login` | Allows logging in with a Google account. | all |
| `hide-ads` | Removes ads from TikTok. | all |
| `playback-speed` | Enables the playback speed option for all videos. | all |
| `settings` | Adds ReVanced settings to TikTok. | 27.8.3 |
| `show-seekbar` | Shows progress bar for all video. | all |
| `sim-spoof` | Spoofs the information which is retrieved from the sim-card. | 27.8.3 |
</details>
You can use [ReVanced CLI](https://github.com/ReVanced/revanced-cli) or [ReVanced Manager](https://github.com/ReVanced/revanced-manager) to use ReVanced Patches.
### [📦 `tv.twitch.android.app`](https://play.google.com/store/apps/details?id=tv.twitch.android.app)
<details>
## 📚 Everything else
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `block-audio-ads` | Blocks audio ads in streams and VODs. | 14.6.1 |
| `block-embedded-ads` | Blocks embedded steam ads using services like TTV.lol or PurpleAdBlocker. | 14.6.1 |
| `block-video-ads` | Blocks video ads in streams and VODs. | 14.6.1 |
| `debug-mode` | Enables Twitch's internal debugging mode. | all |
| `settings` | Adds settings menu to Twitch. | all |
| `show-deleted-messages` | Shows deleted chat messages behind a clickable spoiler. | all |
</details>
### 📙 Contributing
### [📦 `com.twitter.android`](https://play.google.com/store/apps/details?id=com.twitter.android)
<details>
Thank you for considering contributing to ReVanced Patches. You can find the contribution guidelines [here](CONTRIBUTING.md).
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `dynamic-color` | Replaces the default Twitter Blue with the users Material You palette. | all |
| `hide-ads` | Hides ads. | all |
| `hide-recommended-users` | Hides recommended users. | all |
| `hide-views-stats` | Hides the view stats under tweets. | 9.71.0-release.0 |
</details>
### 🛠️ Building
### [📦 `com.spotify.music`](https://play.google.com/store/apps/details?id=com.spotify.music)
<details>
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `disable-capture-restriction` | Allows capturing Spotify's audio output while screen sharing or screen recording. | all |
| `hide-premium-navbar` | Removes the premium tab from the navbar. | all |
| `spotify-theme` | Applies a custom theme. | all |
</details>
## 📜 Licence
### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage)
<details>
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
Any modifications to ReVanced Patches must also be made available under the GPL,
along with build & install instructions.
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `general-reddit-ads` | Removes general ads from the Reddit frontpage and subreddits. | 2023.12.0 |
| `hide-subreddit-banner` | Hides banner ads from comments on subreddits. | 2023.12.0 |
| `premium-icon-reddit` | Unlocks premium Reddit app icons. | all |
</details>
### [📦 `com.vanced.android.youtube`](https://play.google.com/store/apps/details?id=com.vanced.android.youtube)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `client-spoof` | Spoofs a patched client to allow playback. | all |
| `hide-ads` | Removes general ads. | all |
</details>
### [📦 `at.gv.bmf.bmf2go`](https://play.google.com/store/apps/details?id=at.gv.bmf.bmf2go)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `remove-bootloader-detection` | Removes the check for an unlocked bootloader. | 2.2.0 |
| `remove-root-detection` | Removes the check for root permissions. | 2.2.0 |
</details>
### [📦 `at.gv.oe.app`](https://play.google.com/store/apps/details?id=at.gv.oe.app)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `remove-root-detection` | Removes the check for root permissions and unlocked bootloader. | 2.6.0 |
| `spoof-signature` | Spoofs the signature of the app. | 2.6.0 |
</details>
### [📦 `com.myprog.hexedit`](https://play.google.com/store/apps/details?id=com.myprog.hexedit)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `disable-ads` | Disables ads in HexEditor. | all |
</details>
### [📦 `com.spotify.lite`](https://play.google.com/store/apps/details?id=com.spotify.lite)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `enable-on-demand` | Enables listening to songs on-demand, allowing to play any song from playlists, albums or artists without limitations. This does not remove ads. | all |
</details>
### [📦 `com.nis.app`](https://play.google.com/store/apps/details?id=com.nis.app)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `hide-ads` | Removes ads from Inshorts. | all |
</details>
### [📦 `com.facebook.orca`](https://play.google.com/store/apps/details?id=com.facebook.orca)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `hide-inbox-ads` | Hides ads in inbox. | all |
</details>
### [📦 `com.instagram.android`](https://play.google.com/store/apps/details?id=com.instagram.android)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `hide-timeline-ads` | Removes ads from the timeline. | all |
</details>
### [📦 `org.citra.citra_emu`](https://play.google.com/store/apps/details?id=org.citra.citra_emu)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `premium-unlock` | Unlocks premium functions. | all |
</details>
### [📦 `org.citra.citra_emu.canary`](https://play.google.com/store/apps/details?id=org.citra.citra_emu.canary)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `premium-unlock` | Unlocks premium functions. | all |
</details>
### [📦 `com.backdrops.wallpapers`](https://play.google.com/store/apps/details?id=com.backdrops.wallpapers)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `pro-unlock` | Unlocks pro-only functions. | 4.52 |
</details>
### [📦 `de.dwd.warnapp`](https://play.google.com/store/apps/details?id=de.dwd.warnapp)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `promo-code-unlock` | Disables the validation of promo code. Any code will work to unlock all features. | all |
</details>
### [📦 `net.binarymode.android.irplus`](https://play.google.com/store/apps/details?id=net.binarymode.android.irplus)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `remove-ads` | Removes all ads from the app. | all |
</details>
### [📦 `eu.faircode.netguard`](https://play.google.com/store/apps/details?id=eu.faircode.netguard)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `remove-broadcasts-restriction` | Enables starting/stopping NetGuard via broadcasts. | all |
</details>
### [📦 `com.dci.dev.androidtwelvewidgets`](https://play.google.com/store/apps/details?id=com.dci.dev.androidtwelvewidgets)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-paid-widgets` | Unlocks paid widgets of the app | all |
</details>
### [📦 `com.microblink.photomath`](https://play.google.com/store/apps/details?id=com.microblink.photomath)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-plus` | Unlocks plus features. | 8.9.0 |
</details>
### [📦 `io.yuka.android`](https://play.google.com/store/apps/details?id=io.yuka.android)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-premium` | Unlocks premium features. | all |
</details>
### [📦 `com.teslacoilsw.launcher`](https://play.google.com/store/apps/details?id=com.teslacoilsw.launcher)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-prime` | Unlocks Nova Prime and all functions of the app. | all |
</details>
### [📦 `ginlemon.iconpackstudio`](https://play.google.com/store/apps/details?id=ginlemon.iconpackstudio)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-pro` | Unlocks all pro features. | all |
</details>
### [📦 `co.windyapp.android`](https://play.google.com/store/apps/details?id=co.windyapp.android)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-pro` | Unlocks all pro features. | all |
</details>
### [📦 `com.ithebk.expensemanager`](https://play.google.com/store/apps/details?id=com.ithebk.expensemanager)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-pro` | Unlocks pro features. | all |
</details>
### [📦 `org.totschnig.myexpenses`](https://play.google.com/store/apps/details?id=org.totschnig.myexpenses)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-pro` | Unlocks all professional features. | 3.4.9 |
</details>
### [📦 `com.zombodroid.MemeGenerator`](https://play.google.com/store/apps/details?id=com.zombodroid.MemeGenerator)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-pro` | Unlocks pro features. | 4.6364 |
</details>
### [📦 `com.awedea.nyx`](https://play.google.com/store/apps/details?id=com.awedea.nyx)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-pro` | Unlocks all pro features. | all |
</details>
### [📦 `com.ticktick.task`](https://play.google.com/store/apps/details?id=com.ticktick.task)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-themes` | Unlocks all themes that are inaccessible until a certain level is reached. | all |
</details>
### [📦 `net.dinglisch.android.taskerm`](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm)
<details>
| 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:|
| `unlock-trial` | Unlocks the trial version. | all |
</details>
## 📝 JSON Format
This section explains the JSON format for the [patches.json](patches.json) file.
The file contains an array of objects, each object representing a patch. The object contains the following properties:
| key | description |
|-------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | The name of the patch. |
| `description` | The description of the patch. |
| `version` | The version of the patch. |
| `excluded` | Whether the patch is excluded by default. If `true`, the patch must never be included by default. |
| `options` | An array of options for this patch. |
| `options.key` | The key of the option. |
| `options.title` | The title of the option. |
| `options.description` | The description of the option. |
| `options.required` | Whether the option is required. |
| `options.choices?` | An array of choices of the option. This may be `null` if this option has no choices. The element type of this array may be any type. It can be a `String`, `Int` or something else. |
| `dependencies` | An array of dependencies, which are patch names. |
| `compatiblePackages` | An array of packages compatible with this patch. |
| `compatiblePackages.name` | The name of the package. |
| `compatiblePackages.versions` | An array of versions of the package compatible with this patch. If empty, all versions are seemingly compatible. |
Example:
```json
[
{
"name": "remember-video-quality",
"description": "Adds the ability to remember the video quality you chose in the video quality flyout.",
"version": "0.0.1",
"excluded": false,
"options": [],
"dependencies": [
"integrations",
"video-id-hook"
],
"compatiblePackages": [
{
"name": "com.google.android.youtube",
"versions": [
"17.22.36",
"17.24.35",
"17.26.35",
"17.27.39",
"17.28.34",
"17.29.34",
"17.32.35",
"17.33.42"
]
}
]
},
{
"name": "theme",
"description": "Enables a custom theme.",
"version": "0.0.1",
"excluded": false,
"deprecated": false,
"options": [
{
"key": "theme",
"title": "Theme",
"description": "Select a theme.",
"required": true,
"choices": [
"Amoled"
]
}
],
"dependencies": [
"locale-config-fix"
],
"compatiblePackages": [
{
"name": "com.google.android.youtube",
"versions": []
}
]
},
{
"name": "custom-branding",
"description": "Changes the YouTube launcher icon and name to your choice (defaults to ReVanced).",
"version": "0.0.1",
"excluded": false,
"deprecated": false,
"options": [
{
"key": "appName",
"title": "Application Name",
"description": "The name of the application it will show on your home screen.",
"required": true,
"choices": null
},
{
"key": "appIconPath",
"title": "Application Icon Path",
"description": "A path to the icon of the application.",
"required": false,
"choices": null
}
],
"dependencies": [
"locale-config-fix"
],
"compatiblePackages": [
{
"name": "com.google.android.youtube",
"versions": []
}
]
}
]
```

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

66
build.gradle.kts Normal file
View File

@@ -0,0 +1,66 @@
plugins {
kotlin("jvm") version "1.8.10"
}
group = "app.revanced"
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
repositories {
mavenCentral()
mavenLocal()
maven {
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = githubUsername
password = githubPassword
}
}
}
dependencies {
implementation("app.revanced:revanced-patcher:7.0.0")
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
// Required for meta
implementation("com.google.code.gson:gson:2.10.1")
}
tasks {
register<DefaultTask>("generateBundle") {
description = "Generate dex files from build and bundle them in the jar file"
dependsOn(build)
doLast {
val androidHome = System.getenv("ANDROID_HOME") ?: throw GradleException("ANDROID_HOME not found")
val d8 = "${androidHome}/build-tools/33.0.1/d8"
val input = configurations.archives.get().allArtifacts.files.files.first().absolutePath
val work = File("${buildDir}/libs")
exec {
workingDir = work
commandLine = listOf(d8, input)
}
exec {
workingDir = work
commandLine = listOf("zip", "-u", input, "classes.dex")
}
}
}
register<JavaExec>("generateMeta") {
description = "Generate metadata for this bundle"
dependsOn(build)
classpath = sourceSets["main"].runtimeClasspath
mainClass.set("app.revanced.meta.PatchesFileGenerator")
}
// Dummy task to fix the Gradle semantic-release plugin.
// Remove this if you forked it to support building only.
// Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
register<DefaultTask>("publish") {
group = "publish"
description = "Dummy task"
dependsOn(named("generateBundle"), named("generateMeta"))
}
}

View File

@@ -1,8 +0,0 @@
project_id_env: "CROWDIN_PROJECT_ID"
api_token_env: "CROWDIN_PERSONAL_TOKEN"
preserve_hierarchy: false
files:
- source: patches/src/main/resources/addresources/values/strings.xml
translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml
skip_untranslated_strings: true

View File

@@ -1,11 +0,0 @@
extension {
name = "extensions/all/screencapture/remove-screen-capture-restriction.rve"
}
android {
namespace = "app.revanced.extension"
}
dependencies {
compileOnly(libs.annotation)
}

View File

@@ -1,21 +0,0 @@
package app.revanced.extension.all.screencapture.removerestriction;
import android.media.AudioAttributes;
import android.os.Build;
import androidx.annotation.RequiresApi;
public final class RemoveScreencaptureRestrictionPatch {
// Member of AudioAttributes.Builder
@RequiresApi(api = Build.VERSION_CODES.Q)
public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) {
builder.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL);
return builder;
}
// Member of AudioManager static class
public static void setAllowedCapturePolicy(final int capturePolicy) {
// Ignore request
}
}

View File

@@ -1,7 +0,0 @@
extension {
name = "extensions/all/screenshot/remove-screenshot-restriction.rve"
}
android {
namespace = "app.revanced.extension"
}

View File

@@ -1,15 +0,0 @@
package app.revanced.extension.all.screenshot.removerestriction;
import android.view.Window;
import android.view.WindowManager;
public class RemoveScreenshotRestrictionPatch {
public static void addFlags(Window window, int flags) {
window.addFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE);
}
public static void setFlags(Window window, int flags, int mask) {
window.setFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE, mask & ~WindowManager.LayoutParams.FLAG_SECURE);
}
}

View File

@@ -1,22 +0,0 @@
extension {
name = "extensions/shared.rve"
}
android {
namespace = "app.revanced.extension"
buildTypes {
release {
isMinifyEnabled = true
}
}
}
dependencies {
compileOnly(libs.appcompat)
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
compileOnly(libs.retrofit)
compileOnly(project(":extensions:shared:stub"))
}

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
package app.revanced.extension.boostforreddit;
import com.rubenmayayo.reddit.ui.activities.WebViewActivity;
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
/** @noinspection unused*/
public class FixSLinksPatch extends BaseFixSLinksPatch {
static {
INSTANCE = new FixSLinksPatch();
}
private FixSLinksPatch() {
webViewActivityClass = WebViewActivity.class;
}
public static boolean patchResolveSLink(String link) {
return INSTANCE.resolveSLink(link);
}
public static void patchSetAccessToken(String accessToken) {
INSTANCE.setAccessToken(accessToken);
}
}

View File

@@ -1,23 +0,0 @@
package app.revanced.extension.reddit.patches;
import com.reddit.domain.model.ILink;
import java.util.ArrayList;
import java.util.List;
public final class FilterPromotedLinksPatch {
/**
* Filters list from promoted links.
**/
public static List<?> filterChildren(final Iterable<?> links) {
final List<Object> filteredList = new ArrayList<>();
for (Object item : links) {
if (item instanceof ILink && ((ILink) item).getPromoted()) continue;
filteredList.add(item);
}
return filteredList;
}
}

View File

@@ -1,158 +0,0 @@
package app.revanced.extension.shared;
import static app.revanced.extension.shared.StringRef.str;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.RequiresApi;
import java.net.MalformedURLException;
import java.net.URL;
/**
* @noinspection unused
*/
public class GmsCoreSupport {
public static final String ORIGINAL_UNPATCHED_PACKAGE_NAME = "com.google.android.youtube";
private static final String GMS_CORE_PACKAGE_NAME
= getGmsCoreVendorGroupId() + ".android.gms";
private static final Uri GMS_CORE_PROVIDER
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
private static final String DONT_KILL_MY_APP_LINK
= "https://dontkillmyapp.com";
private static void open(String queryOrLink) {
Intent intent;
try {
// Check if queryOrLink is a valid URL.
new URL(queryOrLink);
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
} catch (MalformedURLException e) {
intent = new Intent(Intent.ACTION_WEB_SEARCH);
intent.putExtra(SearchManager.QUERY, queryOrLink);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Utils.getContext().startActivity(intent);
// Gracefully exit, otherwise the broken app will continue to run.
System.exit(0);
}
private static void showBatteryOptimizationDialog(Activity context,
String dialogMessageRef,
String positiveButtonStringRef,
DialogInterface.OnClickListener onPositiveClickListener) {
// Do not set cancelable to false, to allow using back button to skip the action,
// just in case the check can never be satisfied.
var dialog = new AlertDialog.Builder(context)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("gms_core_dialog_title"))
.setMessage(str(dialogMessageRef))
.setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener)
.create();
Utils.showDialog(context, dialog);
}
/**
* Injection point.
*/
@RequiresApi(api = Build.VERSION_CODES.N)
public static void checkGmsCore(Activity context) {
try {
// Verify the user has not included GmsCore for a root installation.
// GmsCore Support changes the package name, but with a mounted installation
// all manifest changes are ignored and the original package name is used.
if (context.getPackageName().equals(ORIGINAL_UNPATCHED_PACKAGE_NAME)) {
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
// Cannot use localize text here, since the app will load
// resources from the unpatched app and all patch strings are missing.
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
// 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 = context.getPackageManager();
manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
} catch (PackageManager.NameNotFoundException exception) {
Logger.printInfo(() -> "GmsCore was not found");
// Cannot show a dialog and must show a toast,
// because on some installations the app crashes before a dialog can be displayed.
Utils.showToastLong(str("gms_core_toast_not_installed_message"));
open(getGmsCoreDownload());
return;
}
// Check if GmsCore is running in the background.
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
if (client == null) {
Logger.printInfo(() -> "GmsCore is not running in the background");
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
"gms_core_dialog_open_website_text",
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
return;
}
}
// Check if GmsCore is whitelisted from battery optimizations.
if (batteryOptimizationsEnabled(context)) {
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
"gms_core_dialog_continue_text",
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
}
} catch (Exception ex) {
Logger.printException(() -> "checkGmsCore failure", ex);
}
}
@SuppressLint("BatteryLife") // Permission is part of GmsCore
private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null));
activity.startActivityForResult(intent, 0);
}
/**
* @return If GmsCore is not whitelisted from battery optimizations.
*/
private static boolean batteryOptimizationsEnabled(Context context) {
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
}
private static String getGmsCoreDownload() {
final var vendorGroupId = getGmsCoreVendorGroupId();
//noinspection SwitchStatementWithTooFewBranches
switch (vendorGroupId) {
case "app.revanced":
return "https://github.com/revanced/gmscore/releases/latest";
default:
return vendorGroupId + ".android.gms";
}
}
// Modified by a patch. Do not touch.
private static String getGmsCoreVendorGroupId() {
return "app.revanced";
}
}

View File

@@ -1,156 +0,0 @@
package app.revanced.extension.shared;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.settings.BaseSettings;
import java.io.PrintWriter;
import java.io.StringWriter;
import static app.revanced.extension.shared.settings.BaseSettings.*;
public class Logger {
/**
* Log messages using lambdas.
*/
@FunctionalInterface
public interface LogMessage {
@NonNull
String buildMessageString();
/**
* @return For outer classes, this returns {@link Class#getSimpleName()}.
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
* <br>
* For example, each of these classes return 'SomethingView':
* <code>
* com.company.SomethingView
* com.company.SomethingView$StaticClass
* com.company.SomethingView$1
* </code>
*/
private String findOuterClassSimpleName() {
var selfClass = this.getClass();
String fullClassName = selfClass.getName();
final int dollarSignIndex = fullClassName.indexOf('$');
if (dollarSignIndex < 0) {
return selfClass.getSimpleName(); // Already an outer class.
}
// Class is inner, static, or anonymous.
// Parse the simple name full name.
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
}
}
private static final String REVANCED_LOG_PREFIX = "revanced: ";
/**
* Logs debug messages under the outer class name of the code calling this method.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message) {
printDebug(message, null);
}
/**
* Logs debug messages under the outer class name of the code calling this method.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) {
String logMessage = message.buildMessageString();
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
if (DEBUG_STACKTRACE.get()) {
var builder = new StringBuilder(logMessage);
var sw = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(sw));
builder.append('\n').append(sw);
logMessage = builder.toString();
}
if (ex == null) {
Log.d(logTag, logMessage);
} else {
Log.d(logTag, logMessage, ex);
}
}
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
public static void printInfo(@NonNull LogMessage message) {
printInfo(message, null);
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
String logMessage = message.buildMessageString();
if (ex == null) {
Log.i(logTag, logMessage);
} else {
Log.i(logTag, logMessage, ex);
}
}
/**
* Logs exceptions under the outer class name of the code calling this method.
*/
public static void printException(@NonNull LogMessage message) {
printException(message, null);
}
/**
* Logs exceptions under the outer class name of the code calling this method.
* <p>
* If the calling code is showing it's own error toast,
* instead use {@link #printInfo(LogMessage, Exception)}
*
* @param message log message
* @param ex exception (optional)
*/
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
String messageString = message.buildMessageString();
String outerClassSimpleName = message.findOuterClassSimpleName();
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
if (ex == null) {
Log.e(logMessage, messageString);
} else {
Log.e(logMessage, messageString, ex);
}
if (DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
}
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationInfo(@NonNull Class<?> callingClass, @NonNull String message) {
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationException(@NonNull Class<?> callingClass, @NonNull String message,
@Nullable Exception ex) {
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
}
}

View File

@@ -1,122 +0,0 @@
package app.revanced.extension.shared;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class StringRef {
private static Resources resources;
private static String packageName;
// must use a thread safe map, as this class is used both on and off the main thread
private static final Map<String, StringRef> strings = Collections.synchronizedMap(new HashMap<>());
/**
* Returns a cached instance.
* Should be used if the same String could be loaded more than once.
*
* @param id string resource name/id
* @see #sf(String)
*/
@NonNull
public static StringRef sfc(@NonNull String id) {
StringRef ref = strings.get(id);
if (ref == null) {
ref = new StringRef(id);
strings.put(id, ref);
}
return ref;
}
/**
* Creates a new instance, but does not cache the value.
* Should be used for Strings that are loaded exactly once.
*
* @param id string resource name/id
* @see #sfc(String)
*/
@NonNull
public static StringRef sf(@NonNull String id) {
return new StringRef(id);
}
/**
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code>
*
* @param id string resource name/id
* @return String value from string.xml
*/
@NonNull
public static String str(@NonNull String id) {
return sfc(id).toString();
}
/**
* Gets string value by string id, shorthand for <code>sfc(id).toString()</code> and formats the string
* with given args.
*
* @param id string resource name/id
* @param args the args to format the string with
* @return String value from string.xml formatted with given args
*/
@NonNull
public static String str(@NonNull String id, Object... args) {
return String.format(str(id), args);
}
/**
* Creates a StringRef object that'll not change it's value
*
* @param value value which toString() method returns when invoked on returned object
* @return Unique StringRef instance, its value will never change
*/
@NonNull
public static StringRef constant(@NonNull String value) {
final StringRef ref = new StringRef(value);
ref.resolved = true;
return ref;
}
/**
* Shorthand for <code>constant("")</code>
* Its value always resolves to empty string
*/
@NonNull
public static final StringRef empty = constant("");
@NonNull
private String value;
private boolean resolved;
public StringRef(@NonNull String resName) {
this.value = resName;
}
@Override
@NonNull
public String toString() {
if (!resolved) {
if (resources == null || packageName == null) {
Context context = Utils.getContext();
resources = context.getResources();
packageName = context.getPackageName();
}
resolved = true;
if (resources != null) {
final int identifier = resources.getIdentifier(value, "string", packageName);
if (identifier == 0)
Logger.printException(() -> "Resource not found: " + value);
else
value = resources.getString(identifier);
} else {
Logger.printException(() -> "Could not resolve resources!");
}
}
return value;
}
}

View File

@@ -1,770 +0,0 @@
package app.revanced.extension.shared;
import android.annotation.SuppressLint;
import android.app.*;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.text.Bidi;
import java.util.*;
import java.util.regex.Pattern;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
public class Utils {
@SuppressLint("StaticFieldLeak")
private static Context context;
private static String versionName;
private Utils() {
} // utility class
/**
* Injection point.
*
* @return The manifest 'Version' entry of the patches.jar used during patching.
*/
@SuppressWarnings("SameReturnValue")
public static String getPatchesReleaseVersion() {
return ""; // Value is replaced during patching.
}
/**
* @return The version name of the app, such as 19.11.43
*/
public static String getAppVersionName() {
if (versionName == null) {
try {
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageInfo = packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
);
} else {
packageInfo = packageManager.getPackageInfo(
packageName,
0
);
}
versionName = packageInfo.versionName;
} catch (Exception ex) {
Logger.printException(() -> "Failed to get package info", ex);
versionName = "Unknown";
}
}
return versionName;
}
/**
* Hide a view by setting its layout height and width to 1dp.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) {
if (hideViewBy0dpUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
/**
* Hide a view by setting its layout height and width to 0dp.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) {
if (condition) {
hideViewByLayoutParams(view);
return true;
}
return false;
}
/**
* Hide a view by setting its visibility to GONE.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static void hideViewUnderCondition(BooleanSetting condition, View view) {
if (hideViewUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
/**
* Hide a view by setting its visibility to GONE.
*
* @param condition The setting to check for hiding the view.
* @param view The view to hide.
*/
public static boolean hideViewUnderCondition(boolean condition, View view) {
if (condition) {
view.setVisibility(View.GONE);
return true;
}
return false;
}
public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) {
if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) {
Logger.printDebug(() -> "View hidden by setting: " + condition);
}
}
public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) {
if (setting) {
ViewParent parent = view.getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(view);
return true;
}
}
return false;
}
/**
* General purpose pool for network calls and other background tasks.
* All tasks run at max thread priority.
*/
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
3, // 3 threads always ready to go
Integer.MAX_VALUE,
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
TimeUnit.SECONDS,
new SynchronousQueue<>(),
r -> { // ThreadFactory
Thread t = new Thread(r);
t.setPriority(Thread.MAX_PRIORITY); // run at max priority
return t;
});
public static void runOnBackgroundThread(@NonNull Runnable task) {
backgroundThreadPool.execute(task);
}
@NonNull
public static <T> Future<T> submitOnBackgroundThread(@NonNull Callable<T> call) {
return backgroundThreadPool.submit(call);
}
/**
* Simulates a delay by doing meaningless calculations.
* Used for debugging to verify UI timeout logic.
*/
@SuppressWarnings("UnusedReturnValue")
public static long doNothingForDuration(long amountOfTimeToWaste) {
final long timeCalculationStarted = System.currentTimeMillis();
Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms");
long meaninglessValue = 0;
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random()));
}
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
// leaving an empty loop that hammers on the System.currentTimeMillis native call
return meaninglessValue;
}
public static boolean containsAny(@NonNull String value, @NonNull String... targets) {
return indexOfFirstFound(value, targets) >= 0;
}
public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) {
for (String string : targets) {
if (!string.isEmpty()) {
final int indexOf = value.indexOf(string);
if (indexOf >= 0) return indexOf;
}
}
return -1;
}
/**
* @return zero, if the resource is not found
*/
@SuppressLint("DiscouragedApi")
public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) {
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
}
/**
* @return zero, if the resource is not found
*/
public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) {
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
}
public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
}
@NonNull
public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
}
public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
//noinspection deprecation
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
}
public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
}
public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException {
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
}
public interface MatchFilter<T> {
boolean matches(T object);
}
/**
* Includes sub children.
*
* @noinspection unchecked
*/
public static <R extends View> R getChildViewByResourceName(@NonNull View view, @NonNull String str) {
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
if (child != null) {
return (R) child;
}
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
}
/**
* @param searchRecursively If children ViewGroups should also be
* recursively searched using depth first search.
* @return The first child view that matches the filter.
*/
@Nullable
public static <T extends View> T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively,
@NonNull MatchFilter<View> filter) {
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
View childAt = viewGroup.getChildAt(i);
if (filter.matches(childAt)) {
//noinspection unchecked
return (T) childAt;
}
// Must do recursive after filter check, in case the filter is looking for a ViewGroup.
if (searchRecursively && childAt instanceof ViewGroup) {
T match = getChildView((ViewGroup) childAt, true, filter);
if (match != null) return match;
}
}
return null;
}
@Nullable
public static ViewParent getParentView(@NonNull View view, int nthParent) {
ViewParent parent = view.getParent();
int currentDepth = 0;
while (++currentDepth < nthParent && parent != null) {
parent = parent.getParent();
}
if (currentDepth == nthParent) {
return parent;
}
final int currentDepthLog = currentDepth;
Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent
+ " and instead found at: " + currentDepthLog + " view: " + view);
return null;
}
public static void restartApp(@NonNull Context context) {
String packageName = context.getPackageName();
Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName);
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
// Required for API 34 and later
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
mainIntent.setPackage(packageName);
context.startActivity(mainIntent);
System.exit(0);
}
public static Context getContext() {
if (context == null) {
Logger.initializationException(Utils.class, "Context is null, returning null!", null);
}
return context;
}
public static void setContext(Context appContext) {
context = appContext;
// In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies.
// Calling the regular printDebug method here can cause a Settings context null pointer exception,
// even though the context is already set before the call.
//
// The initialization logger methods do not directly or indirectly
// reference the Context or any Settings and are unaffected by this problem.
//
// Info level also helps debug if a patch hook is called before
// the context is set since debug logging is off by default.
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
}
public static void setClipboard(@NonNull String text) {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
clipboard.setPrimaryClip(clip);
}
public static boolean isTablet() {
return context.getResources().getConfiguration().smallestScreenWidthDp >= 600;
}
@Nullable
private static Boolean isRightToLeftTextLayout;
/**
* If the device language uses right to left text layout (hebrew, arabic, etc)
*/
public static boolean isRightToLeftTextLayout() {
if (isRightToLeftTextLayout == null) {
String displayLanguage = Locale.getDefault().getDisplayLanguage();
isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
}
return isRightToLeftTextLayout;
}
/**
* @return if the text contains at least 1 number character,
* including any unicode numbers such as Arabic.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean containsNumber(@NonNull CharSequence text) {
for (int index = 0, length = text.length(); index < length;) {
final int codePoint = Character.codePointAt(text, index);
if (Character.isDigit(codePoint)) {
return true;
}
index += Character.charCount(codePoint);
}
return false;
}
/**
* Ignore this class. It must be public to satisfy Android requirements.
*/
@SuppressWarnings("deprecation")
public static final class DialogFragmentWrapper extends DialogFragment {
private Dialog dialog;
@Nullable
private DialogFragmentOnStartAction onStartAction;
@Override
public void onSaveInstanceState(Bundle outState) {
// Do not call super method to prevent state saving.
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return dialog;
}
@Override
public void onStart() {
try {
super.onStart();
if (onStartAction != null) {
onStartAction.onStart((AlertDialog) getDialog());
}
} catch (Exception ex) {
Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex);
}
}
}
/**
* Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}.
*/
@FunctionalInterface
public interface DialogFragmentOnStartAction {
void onStart(AlertDialog dialog);
}
public static void showDialog(Activity activity, AlertDialog dialog) {
showDialog(activity, dialog, true, null);
}
/**
* Utility method to allow showing an AlertDialog on top of other alert dialogs.
* Calling this will always display the dialog on top of all other dialogs
* previously called using this method.
* <br>
* Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app.
*<br>
* This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top.
*<br>
* For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog.
*/
@SuppressWarnings("deprecation")
public static void showDialog(Activity activity,
AlertDialog dialog,
boolean isCancelable,
@Nullable DialogFragmentOnStartAction onStartAction) {
verifyOnMainThread();
DialogFragmentWrapper fragment = new DialogFragmentWrapper();
fragment.dialog = dialog;
fragment.onStartAction = onStartAction;
fragment.setCancelable(isCancelable);
fragment.show(activity.getFragmentManager(), null);
}
/**
* Safe to call from any thread
*/
public static void showToastShort(@NonNull String messageToToast) {
showToast(messageToToast, Toast.LENGTH_SHORT);
}
/**
* Safe to call from any thread
*/
public static void showToastLong(@NonNull String messageToToast) {
showToast(messageToToast, Toast.LENGTH_LONG);
}
private static void showToast(@NonNull String messageToToast, int toastDuration) {
Objects.requireNonNull(messageToToast);
runOnMainThreadNowOrLater(() -> {
if (context == null) {
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
} else {
Logger.printDebug(() -> "Showing toast: " + messageToToast);
Toast.makeText(context, messageToToast, toastDuration).show();
}
}
);
}
/**
* Automatically logs any exceptions the runnable throws.
*
* @see #runOnMainThreadNowOrLater(Runnable)
*/
public static void runOnMainThread(@NonNull Runnable runnable) {
runOnMainThreadDelayed(runnable, 0);
}
/**
* Automatically logs any exceptions the runnable throws
*/
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
Runnable loggingRunnable = () -> {
try {
runnable.run();
} catch (Exception ex) {
Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex);
}
};
new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis);
}
/**
* If called from the main thread, the code is run immediately.<p>
* If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}.
*/
public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) {
if (isCurrentlyOnMainThread()) {
runnable.run();
} else {
runOnMainThread(runnable);
}
}
/**
* @return if the calling thread is on the main thread
*/
public static boolean isCurrentlyOnMainThread() {
return Looper.getMainLooper().isCurrentThread();
}
/**
* @throws IllegalStateException if the calling thread is _off_ the main thread
*/
public static void verifyOnMainThread() throws IllegalStateException {
if (!isCurrentlyOnMainThread()) {
throw new IllegalStateException("Must call _on_ the main thread");
}
}
/**
* @throws IllegalStateException if the calling thread is _on_ the main thread
*/
public static void verifyOffMainThread() throws IllegalStateException {
if (isCurrentlyOnMainThread()) {
throw new IllegalStateException("Must call _off_ the main thread");
}
}
public enum NetworkType {
NONE,
MOBILE,
OTHER,
}
public static boolean isNetworkConnected() {
NetworkType networkType = getNetworkType();
return networkType == NetworkType.MOBILE
|| networkType == NetworkType.OTHER;
}
@SuppressLint("MissingPermission") // permission already included in YouTube
public static NetworkType getNetworkType() {
Context networkContext = getContext();
if (networkContext == null) {
return NetworkType.NONE;
}
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
var networkInfo = cm.getActiveNetworkInfo();
if (networkInfo == null || !networkInfo.isConnected()) {
return NetworkType.NONE;
}
var type = networkInfo.getType();
return (type == ConnectivityManager.TYPE_MOBILE)
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
}
/**
* Hide a view by setting its layout params to 0x0
* @param view The view to hide.
*/
public static void hideViewByLayoutParams(View view) {
if (view instanceof LinearLayout) {
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams);
} else if (view instanceof FrameLayout) {
FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams2);
} else if (view instanceof RelativeLayout) {
RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0);
view.setLayoutParams(layoutParams3);
} else if (view instanceof Toolbar) {
Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0);
view.setLayoutParams(layoutParams4);
} else if (view instanceof ViewGroup) {
ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0);
view.setLayoutParams(layoutParams5);
} else {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.width = 0;
params.height = 0;
view.setLayoutParams(params);
}
}
/**
* {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles.
*/
private enum Sort {
/**
* Sort by the localized preference title.
*/
BY_TITLE("_sort_by_title"),
/**
* Sort by the preference keys.
*/
BY_KEY("_sort_by_key"),
/**
* Unspecified sorting.
*/
UNSORTED("_sort_by_unsorted");
final String keySuffix;
Sort(String keySuffix) {
this.keySuffix = keySuffix;
}
@NonNull
static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) {
if (key != null) {
for (Sort sort : values()) {
if (key.endsWith(sort.keySuffix)) {
return sort;
}
}
}
return defaultSort;
}
}
private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+");
/**
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
*/
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
if (original == null) return "";
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
}
/**
* Sort a PreferenceGroup and all it's sub groups by title or key.
*
* Sort order is determined by the preferences key {@link Sort} suffix.
*
* If a preference has no key or no {@link Sort} suffix,
* then the preferences are left unsorted.
*/
@SuppressWarnings("deprecation")
public static void sortPreferenceGroups(@NonNull PreferenceGroup group) {
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
SortedMap<String, Preference> preferences = new TreeMap<>();
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference preference = group.getPreference(i);
final Sort preferenceSort;
if (preference instanceof PreferenceGroup) {
sortPreferenceGroups((PreferenceGroup) preference);
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
} else {
// Allow individual preferences to set a key sorting.
// Used to force a preference to the top or bottom of a group.
preferenceSort = Sort.fromKey(preference.getKey(), groupSort);
}
final String sortValue;
switch (preferenceSort) {
case BY_TITLE:
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
break;
case BY_KEY:
sortValue = preference.getKey();
break;
case UNSORTED:
continue; // Keep original sorting.
default:
throw new IllegalStateException();
}
preferences.put(sortValue, preference);
}
int index = 0;
for (Preference pref : preferences.values()) {
int order = index++;
// Move any screens, intents, and the one off About preference to the top.
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|| pref.getIntent() != null) {
// Arbitrary high number.
order -= 1000;
}
pref.setOrder(order);
}
}
/**
* Set all preferences to multiline titles if the device is not using an English variant.
* The English strings are heavily scrutinized and all titles fit on screen
* except 2 or 3 preference strings and those do not affect readability.
*
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
* and visually it looks better to clip the text and keep all titles 1 line.
*/
@SuppressWarnings("deprecation")
public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
String deviceLanguage = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
if (deviceLanguage.equals("en")) {
return;
}
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
Preference pref = group.getPreference(i);
pref.setSingleLineTitle(false);
if (pref instanceof PreferenceGroup) {
setPreferenceTitlesToMultiLineIfNeeded((PreferenceGroup) pref);
}
}
}
/**
* If {@link Fragment} uses [Android library] rather than [AndroidX library],
* the Dialog theme corresponding to [Android library] should be used.
* <p>
* If not, the following issues will occur:
* <a href="https://github.com/ReVanced/revanced-patches/issues/3061">ReVanced/revanced-patches#3061</a>
* <p>
* To prevent these issues, apply the Dialog theme corresponding to [Android library].
*/
public static void setEditTextDialogTheme(AlertDialog.Builder builder) {
final int editTextDialogStyle = getResourceIdentifier(
"revanced_edit_text_dialog_style", "style");
if (editTextDialogStyle != 0) {
builder.getContext().setTheme(editTextDialogStyle);
}
}
}

View File

@@ -1,164 +0,0 @@
package app.revanced.extension.shared.checks;
import static android.text.Html.FROM_HTML_MODE_COMPACT;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.text.Html;
import android.widget.Button;
import androidx.annotation.Nullable;
import java.util.Collection;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
abstract class Check {
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15;
private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10;
private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app");
/**
* @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed.
*/
@Nullable
protected abstract Boolean check();
protected abstract String failureReason();
/**
* Specifies a sorting order for displaying the checks that failed.
* A lower value indicates to show first before other checks.
*/
public abstract int uiSortingValue();
/**
* For debugging and development only.
* Forces all checks to be performed and the check failed dialog to be shown.
* Can be enabled by importing settings text with {@link Settings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
* set to -1.
*/
static boolean debugAlwaysShowWarning() {
final boolean alwaysShowWarning = Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
if (alwaysShowWarning) {
Logger.printInfo(() -> "Debug forcing environment check warning to show");
}
return alwaysShowWarning;
}
static boolean shouldRun() {
return Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
}
static void disableForever() {
Logger.printInfo(() -> "Environment checks disabled forever");
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
}
@SuppressLint("NewApi")
static void issueWarning(Activity activity, Collection<Check> failedChecks) {
final var reasons = new StringBuilder();
reasons.append("<ul>");
for (var check : failedChecks) {
// Add a non breaking space to fix bullet points spacing issue.
reasons.append("<li>&nbsp;").append(check.failureReason());
}
reasons.append("</ul>");
var message = Html.fromHtml(
str("revanced_check_environment_failed_message", reasons.toString()),
FROM_HTML_MODE_COMPACT
);
Utils.runOnMainThreadDelayed(() -> {
AlertDialog alert = new AlertDialog.Builder(activity)
.setCancelable(false)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setTitle(str("revanced_check_environment_failed_title"))
.setMessage(message)
.setPositiveButton(
" ",
(dialog, which) -> {
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
// Shutdown to prevent the user from navigating back to this app,
// which is no longer showing a warning dialog.
activity.finishAffinity();
System.exit(0);
}
).setNegativeButton(
" ",
(dialog, which) -> {
// Cleanup data if the user incorrectly imported a huge negative number.
final int current = Math.max(0, Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
dialog.dismiss();
}
).create();
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
boolean hasRun;
@Override
public void onStart(AlertDialog dialog) {
// Only run this once, otherwise if the user changes to a different app
// then changes back, this handler will run again and disable the buttons.
if (hasRun) {
return;
}
hasRun = true;
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
openWebsiteButton.setEnabled(false);
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
dismissButton.setEnabled(false);
getCountdownRunnable(dismissButton, openWebsiteButton).run();
}
});
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
}
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) {
return new Runnable() {
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
@Override
public void run() {
Utils.verifyOnMainThread();
if (secondsRemaining > 0) {
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
openWebsiteButton.setEnabled(true);
}
secondsRemaining--;
Utils.runOnMainThreadDelayed(this, 1000);
} else {
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
dismissButton.setEnabled(true);
}
}
};
}
}

View File

@@ -1,341 +0,0 @@
package app.revanced.extension.shared.checks;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.checks.Check.debugAlwaysShowWarning;
import static app.revanced.extension.shared.checks.PatchInfo.Build.*;
/**
* This class is used to check if the app was patched by the user
* and not downloaded pre-patched, because pre-patched apps are difficult to trust.
* <br>
* Various indicators help to detect if the app was patched by the user.
*/
@SuppressWarnings("unused")
public final class CheckEnvironmentPatch {
private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning();
private enum InstallationType {
/**
* CLI patching, manual installation of a previously patched using adb,
* or root installation if stock app is first installed using adb.
*/
ADB((String) null),
ROOT_MOUNT_ON_APP_STORE("com.android.vending"),
MANAGER("app.revanced.manager.flutter",
"app.revanced.manager",
"app.revanced.manager.debug");
@Nullable
static InstallationType installTypeFromPackageName(@Nullable String packageName) {
for (InstallationType type : values()) {
for (String installPackageName : type.packageNames) {
if (Objects.equals(installPackageName, packageName)) {
return type;
}
}
}
return null;
}
/**
* Array elements can be null.
*/
final String[] packageNames;
InstallationType(String... packageNames) {
this.packageNames = packageNames;
}
}
/**
* Check if the app is installed by the manager, the app store, or through adb/CLI.
* <br>
* Does not conclusively
* If the app is installed by the manager or the app store, it is likely, the app was patched using the manager,
* or installed manually via ADB (in the case of ReVanced CLI for example).
* <br>
* If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched
* and installed by the browser or another unknown app.
*/
private static class CheckExpectedInstaller extends Check {
@Nullable
InstallationType installerFound;
@NonNull
@Override
protected Boolean check() {
final var context = Utils.getContext();
final var installerPackageName =
context.getPackageManager().getInstallerPackageName(context.getPackageName());
Logger.printInfo(() -> "Installed by: " + installerPackageName);
installerFound = InstallationType.installTypeFromPackageName(installerPackageName);
final boolean passed = (installerFound != null);
Logger.printInfo(() -> passed
? "Apk was not installed from an unknown source"
: "Apk was installed from an unknown source");
return passed;
}
@Override
protected String failureReason() {
return str("revanced_check_environment_manager_not_expected_installer");
}
@Override
public int uiSortingValue() {
return -100; // Show first.
}
}
/**
* Check if the build properties are the same as during the patch.
* <br>
* If the build properties are the same as during the patch, it is likely, the app was patched on the same device.
* <br>
* If the build properties are different, the app was likely downloaded pre-patched or patched on another device.
*/
private static class CheckWasPatchedOnSameDevice extends Check {
@SuppressLint({"NewApi", "HardwareIds"})
@Override
protected Boolean check() {
if (PATCH_BOARD.isEmpty()) {
// Did not patch with Manager, and cannot conclusively say where this was from.
Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device");
return null;
}
//noinspection deprecation
final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) &
buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) &
buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) &
buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) &
buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) &
buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) &
buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) &
buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) &
buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) &
buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) &
buildFieldEqualsHash("ID", Build.ID, PATCH_ID) &
buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) &
buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) &
buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) &
buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) &
buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) &
buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) &
buildFieldEqualsHash("USER", Build.USER, PATCH_USER);
Logger.printInfo(() -> passed
? "Device hardware signature matches current device"
: "Device hardware signature does not match current device");
return passed;
}
@Override
protected String failureReason() {
return str("revanced_check_environment_not_same_patching_device");
}
@Override
public int uiSortingValue() {
return 0; // Show in the middle.
}
}
/**
* Check if the app was installed within the last 30 minutes after being patched.
* <br>
* If the app was installed within the last 30 minutes, it is likely, the app was patched by the user.
* <br>
* If the app was installed much later than the patch time, it is likely the app was
* downloaded pre-patched or the user waited too long to install the app.
*/
private static class CheckIsNearPatchTime extends Check {
/**
* How soon after patching the app must be installed to pass.
*/
static final int INSTALL_AFTER_PATCHING_DURATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes.
/**
* Milliseconds between the time the app was patched, and when it was installed/updated.
*/
long durationBetweenPatchingAndInstallation;
@NonNull
@Override
protected Boolean check() {
try {
Context context = Utils.getContext();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
// Duration since initial install or last update, which ever is sooner.
durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME;
Logger.printInfo(() -> "App was installed/updated: "
+ (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching"));
if (durationBetweenPatchingAndInstallation < 0) {
// Patch time is in the future and clearly wrong.
return false;
}
if (durationBetweenPatchingAndInstallation < INSTALL_AFTER_PATCHING_DURATION_THRESHOLD) {
return true;
}
} catch (PackageManager.NameNotFoundException ex) {
Logger.printException(() -> "Package name not found exception", ex); // Will never happen.
}
// User installed more than 30 minutes after patching.
return false;
}
@Override
protected String failureReason() {
if (durationBetweenPatchingAndInstallation < 0) {
// Could happen if the user has their device clock incorrectly set in the past,
// but assume that isn't the case and the apk was patched on a device with the wrong system time.
return str("revanced_check_environment_not_near_patch_time_invalid");
}
// If patched over 1 day ago, show how old this pre-patched apk is.
// Showing the age can help convey it's better to patch yourself and know it's the latest.
final long oneDay = 24 * 60 * 60 * 1000;
final long daysSincePatching = durationBetweenPatchingAndInstallation / oneDay;
if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings.
return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching);
}
return str("revanced_check_environment_not_near_patch_time");
}
@Override
public int uiSortingValue() {
return 100; // Show last.
}
}
/**
* Injection point.
*/
public static void check(Activity context) {
// If the warning was already issued twice, or if the check was successful in the past,
// do not run the checks again.
if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
Logger.printDebug(() -> "Environment checks are disabled");
return;
}
Utils.runOnBackgroundThread(() -> {
try {
Logger.printInfo(() -> "Running environment checks");
List<Check> failedChecks = new ArrayList<>();
CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice();
Boolean hardwareCheckPassed = sameHardware.check();
if (hardwareCheckPassed != null) {
if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Patched on the same device using Manager,
// and no further checks are needed.
Check.disableForever();
return;
}
failedChecks.add(sameHardware);
}
CheckExpectedInstaller installerCheck = new CheckExpectedInstaller();
if (installerCheck.check() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// If the installer package is Manager but this code is reached,
// that means it must not be the right Manager otherwise the hardware hash
// signatures would be present and this check would not have run.
if (installerCheck.installerFound == InstallationType.MANAGER) {
failedChecks.add(installerCheck);
// Also could not have been patched on this device.
failedChecks.add(sameHardware);
} else if (failedChecks.isEmpty()) {
// ADB install of CLI build. Allow even if patched a long time ago.
Check.disableForever();
return;
}
} else {
failedChecks.add(installerCheck);
}
CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime();
Boolean timeCheckPassed = nearPatchTime.check();
if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Allow installing recently patched apks,
// even if the install source is not Manager or ADB.
Check.disableForever();
return;
} else {
failedChecks.add(nearPatchTime);
}
if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) {
// Show all failures for debugging layout.
failedChecks = Arrays.asList(
sameHardware,
nearPatchTime,
installerCheck
);
}
//noinspection ComparatorCombinators
Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue());
Check.issueWarning(
context,
failedChecks
);
} catch (Exception ex) {
Logger.printException(() -> "check failure", ex);
}
});
}
private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) {
try {
final var sha1 = MessageDigest.getInstance("SHA-1")
.digest(buildFieldValue.getBytes(StandardCharsets.UTF_8));
// Must be careful to use same base64 encoding Kotlin uses.
String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1);
final boolean equals = runtimeHash.equals(hash);
if (!equals) {
Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue
+ "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'");
}
return equals;
} catch (NoSuchAlgorithmException ex) {
Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen.
return false;
}
}
}

View File

@@ -1,32 +0,0 @@
package app.revanced.extension.shared.checks;
/**
* Fields are set by the patch. Do not modify.
* Fields are not final, because the compiler is inlining them.
*
* @noinspection CanBeFinal
*/
final class PatchInfo {
static long PATCH_TIME = 0L;
final static class Build {
static String PATCH_BOARD = "";
static String PATCH_BOOTLOADER = "";
static String PATCH_BRAND = "";
static String PATCH_CPU_ABI = "";
static String PATCH_CPU_ABI2 = "";
static String PATCH_DEVICE = "";
static String PATCH_DISPLAY = "";
static String PATCH_FINGERPRINT = "";
static String PATCH_HARDWARE = "";
static String PATCH_HOST = "";
static String PATCH_ID = "";
static String PATCH_MANUFACTURER = "";
static String PATCH_MODEL = "";
static String PATCH_PRODUCT = "";
static String PATCH_RADIO = "";
static String PATCH_TAGS = "";
static String PATCH_TYPE = "";
static String PATCH_USER = "";
}
}

View File

@@ -1,208 +0,0 @@
package app.revanced.extension.shared.fixes.slink;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Objects;
import static app.revanced.extension.shared.Utils.getContext;
/**
* Base class to implement /s/ link resolution in 3rd party Reddit apps.
* <br>
* <br>
* Usage:
* <br>
* <br>
* An implementation of this class must have two static methods that are called by the app:
* <ul>
* <li>public static boolean patchResolveSLink(String link)</li>
* <li>public static void patchSetAccessToken(String accessToken)</li>
* </ul>
* The static methods must call the instance methods of the base class.
* <br>
* The singleton pattern can be used to access the instance of the class:
* <pre>
* {@code
* {
* INSTANCE = new FixSLinksPatch();
* }
* }
* </pre>
* Set the app's web view activity class as a fallback to open /s/ links if the resolution fails:
* <pre>
* {@code
* private FixSLinksPatch() {
* webViewActivityClass = WebViewActivity.class;
* }
* }
* </pre>
* Hook the app's navigation handler to call this method before doing any of its own resolution:
* <pre>
* {@code
* public static boolean patchResolveSLink(Context context, String link) {
* return INSTANCE.resolveSLink(context, link);
* }
* }
* </pre>
* If this method returns true, the app should early return and not do any of its own resolution.
* <br>
* <br>
* Hook the app's access token so that this class can use it to resolve /s/ links:
* <pre>
* {@code
* public static void patchSetAccessToken(String accessToken) {
* INSTANCE.setAccessToken(access_token);
* }
* }
* </pre>
*/
public abstract class BaseFixSLinksPatch {
/**
* The class of the activity used to open links in a web view if resolving them fails.
*/
protected Class<? extends Activity> webViewActivityClass;
/**
* The access token used to resolve the /s/ link.
*/
protected String accessToken;
/**
* The URL that was trying to be resolved before the access token was set.
* If this is not null, the URL will be resolved right after the access token is set.
*/
protected String pendingUrl;
/**
* The singleton instance of the class.
*/
protected static BaseFixSLinksPatch INSTANCE;
public boolean resolveSLink(String link) {
switch (resolveLink(link)) {
case ACCESS_TOKEN_START: {
pendingUrl = link;
return true;
}
case DO_NOTHING:
return true;
default:
return false;
}
}
private ResolveResult resolveLink(String link) {
Context context = getContext();
if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) {
// A link ends with #bypass if it failed to resolve below.
// resolveLink is called with the same link again but this time with #bypass
// so that the link is opened in the app browser instead of trying to resolve it again.
if (link.endsWith("#bypass")) {
openInAppBrowser(context, link);
return ResolveResult.DO_NOTHING;
}
Logger.printDebug(() -> "Resolving " + link);
if (accessToken == null) {
// This is not optimal.
// However, an accessToken is necessary to make an authenticated request to Reddit.
// in case Reddit has banned the IP - e.g. VPN.
Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
context.startActivity(startIntent);
return ResolveResult.ACCESS_TOKEN_START;
}
Utils.runOnBackgroundThread(() -> {
String bypassLink = link + "#bypass";
String finalLocation = bypassLink;
try {
HttpURLConnection connection = getHttpURLConnection(link, accessToken);
connection.connect();
String location = connection.getHeaderField("location");
connection.disconnect();
Objects.requireNonNull(location, "Location is null");
finalLocation = location;
Logger.printDebug(() -> "Resolved " + link + " to " + location);
} catch (SocketTimeoutException e) {
Logger.printException(() -> "Timeout when trying to resolve " + link, e);
finalLocation = bypassLink;
} catch (Exception e) {
Logger.printException(() -> "Failed to resolve " + link, e);
finalLocation = bypassLink;
} finally {
Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation));
startIntent.setPackage(context.getPackageName());
startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(startIntent);
}
});
return ResolveResult.DO_NOTHING;
}
return ResolveResult.CONTINUE;
}
public void setAccessToken(String accessToken) {
Logger.printDebug(() -> "Setting access token");
this.accessToken = accessToken;
// In case a link was trying to be resolved before access token was set.
// The link is resolved now, after the access token is set.
if (pendingUrl != null) {
String link = pendingUrl;
pendingUrl = null;
Logger.printDebug(() -> "Opening pending URL");
resolveLink(link);
}
}
private void openInAppBrowser(Context context, String link) {
Intent intent = new Intent(context, webViewActivityClass);
intent.putExtra("url", link);
context.startActivity(intent);
}
@NonNull
private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException {
URL url = new URL(link);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("HEAD");
connection.setConnectTimeout(2000);
connection.setReadTimeout(2000);
if (accessToken != null) {
Logger.printDebug(() -> "Setting access token to make /s/ request");
connection.setRequestProperty("Authorization", "Bearer " + accessToken);
} else {
Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null");
}
return connection;
}
}

View File

@@ -1,10 +0,0 @@
package app.revanced.extension.shared.fixes.slink;
public enum ResolveResult {
// Let app handle rest of stuff
CONTINUE,
// Start app, to make it cache its access_token
ACCESS_TOKEN_START,
// Don't do anything - we started resolving
DO_NOTHING
}

View File

@@ -1,17 +0,0 @@
package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.settings.Setting.parent;
/**
* Settings shared across multiple apps.
*
* To ensure this class is loaded when the UI is created, app specific setting bundles should extend
* or reference this class.
*/
public class BaseSettings {
public static final BooleanSetting DEBUG = new BooleanSetting("revanced_debug", FALSE);
public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG));
public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message");
}

View File

@@ -1,79 +0,0 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class BooleanSetting extends Setting<Boolean> {
public BooleanSetting(String key, Boolean defaultValue) {
super(key, defaultValue);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public BooleanSetting(String key, Boolean defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Boolean)} was intnded.
*/
public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) {
setting.value = Objects.requireNonNull(newValue);
}
@Override
protected void load() {
value = preferences.getBoolean(key, defaultValue);
}
@Override
protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getBoolean(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Boolean.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Boolean newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveBoolean(key, newValue);
}
@NonNull
@Override
public Boolean get() {
return value;
}
}

View File

@@ -1,117 +0,0 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Locale;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
/**
* If an Enum value is removed or changed, any saved or imported data using the
* non-existent value will be reverted to the default value
* (the event is logged, but no user error is displayed).
*
* All saved JSON text is converted to lowercase to keep the output less obnoxious.
*/
@SuppressWarnings("unused")
public class EnumSetting<T extends Enum<?>> extends Setting<T> {
public EnumSetting(String key, T defaultValue) {
super(key, defaultValue);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public EnumSetting(String key, T defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public EnumSetting(String key, T defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getEnum(key, defaultValue);
}
@Override
protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException {
String enumName = json.getString(importExportKey);
try {
return getEnumFromString(enumName);
} catch (IllegalArgumentException ex) {
// Info level to allow removing enum values in the future without showing any user errors.
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex);
return defaultValue;
}
}
@Override
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
// Use lowercase to keep the output less ugly.
json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH));
}
@NonNull
private T getEnumFromString(String enumName) {
//noinspection ConstantConditions
for (Enum<?> value : defaultValue.getClass().getEnumConstants()) {
if (value.name().equalsIgnoreCase(enumName)) {
// noinspection unchecked
return (T) value;
}
}
throw new IllegalArgumentException("Unknown enum value: " + enumName);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = getEnumFromString(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull T newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveEnumAsString(key, newValue);
}
@NonNull
@Override
public T get() {
return value;
}
/**
* Availability based on if this setting is currently set to any of the provided types.
*/
@SafeVarargs
public final Setting.Availability availability(@NonNull T... types) {
return () -> {
T currentEnumType = get();
for (T enumType : types) {
if (currentEnumType == enumType) return true;
}
return false;
};
}
}

View File

@@ -1,69 +0,0 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class FloatSetting extends Setting<Float> {
public FloatSetting(String key, Float defaultValue) {
super(key, defaultValue);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public FloatSetting(String key, Float defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public FloatSetting(String key, Float defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getFloatString(key, defaultValue);
}
@Override
protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return (float) json.getDouble(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Float.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Float newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveFloatString(key, newValue);
}
@NonNull
@Override
public Float get() {
return value;
}
}

View File

@@ -1,69 +0,0 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class IntegerSetting extends Setting<Integer> {
public IntegerSetting(String key, Integer defaultValue) {
super(key, defaultValue);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public IntegerSetting(String key, Integer defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getIntegerString(key, defaultValue);
}
@Override
protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getInt(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Integer.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Integer newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveIntegerString(key, newValue);
}
@NonNull
@Override
public Integer get() {
return value;
}
}

View File

@@ -1,69 +0,0 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class LongSetting extends Setting<Long> {
public LongSetting(String key, Long defaultValue) {
super(key, defaultValue);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public LongSetting(String key, Long defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public LongSetting(String key, Long defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getLongString(key, defaultValue);
}
@Override
protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getLong(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Long.valueOf(Objects.requireNonNull(newValue));
}
@Override
public void save(@NonNull Long newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveLongString(key, newValue);
}
@NonNull
@Override
public Long get() {
return value;
}
}

View File

@@ -1,437 +0,0 @@
package app.revanced.extension.shared.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.StringRef;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
import org.jetbrains.annotations.NotNull;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.*;
import static app.revanced.extension.shared.StringRef.str;
@SuppressWarnings("unused")
public abstract class Setting<T> {
/**
* Indicates if a {@link Setting} is available to edit and use.
* Typically this is dependent upon other BooleanSetting(s) set to 'true',
* but this can be used to call into extension code and check other conditions.
*/
public interface Availability {
boolean isAvailable();
}
/**
* Availability based on a single parent setting being enabled.
*/
@NonNull
public static Availability parent(@NonNull BooleanSetting parent) {
return parent::get;
}
/**
* Availability based on all parents being enabled.
*/
@NonNull
public static Availability parentsAll(@NonNull BooleanSetting... parents) {
return () -> {
for (BooleanSetting parent : parents) {
if (!parent.get()) return false;
}
return true;
};
}
/**
* Availability based on any parent being enabled.
*/
@NonNull
public static Availability parentsAny(@NonNull BooleanSetting... parents) {
return () -> {
for (BooleanSetting parent : parents) {
if (parent.get()) return true;
}
return false;
};
}
/**
* All settings that were instantiated.
* When a new setting is created, it is automatically added to this list.
*/
private static final List<Setting<?>> SETTINGS = new ArrayList<>();
/**
* Map of setting path to setting object.
*/
private static final Map<String, Setting<?>> PATH_TO_SETTINGS = new HashMap<>();
/**
* Preference all instances are saved to.
*/
public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced_prefs");
@Nullable
public static Setting<?> getSettingFromPath(@NonNull String str) {
return PATH_TO_SETTINGS.get(str);
}
/**
* @return All settings that have been created.
*/
@NonNull
public static List<Setting<?>> allLoadedSettings() {
return Collections.unmodifiableList(SETTINGS);
}
/**
* @return All settings that have been created, sorted by keys.
*/
@NonNull
private static List<Setting<?>> allLoadedSettingsSorted() {
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
return allLoadedSettings();
}
/**
* The key used to store the value in the shared preferences.
*/
@NonNull
public final String key;
/**
* The default value of the setting.
*/
@NonNull
public final T defaultValue;
/**
* If the app should be rebooted, if this setting is changed
*/
public final boolean rebootApp;
/**
* If this setting should be included when importing/exporting settings.
*/
public final boolean includeWithImportExport;
/**
* If this setting is available to edit and use.
* Not to be confused with it's status returned from {@link #get()}.
*/
@Nullable
private final Availability availability;
/**
* Confirmation message to display, if the user tries to change the setting from the default value.
* Currently this works only for Boolean setting types.
*/
@Nullable
public final StringRef userDialogMessage;
// Must be volatile, as some settings are read/write from different threads.
// Of note, the object value is persistently stored using SharedPreferences (which is thread safe).
/**
* The value of the setting.
*/
@NonNull
protected volatile T value;
public Setting(String key, T defaultValue) {
this(key, defaultValue, false, true, null, null);
}
public Setting(String key, T defaultValue, boolean rebootApp) {
this(key, defaultValue, rebootApp, true, null, null);
}
public Setting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) {
this(key, defaultValue, rebootApp, includeWithImportExport, null, null);
}
public Setting(String key, T defaultValue, String userDialogMessage) {
this(key, defaultValue, false, true, userDialogMessage, null);
}
public Setting(String key, T defaultValue, Availability availability) {
this(key, defaultValue, false, true, null, availability);
}
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) {
this(key, defaultValue, rebootApp, true, userDialogMessage, null);
}
public Setting(String key, T defaultValue, boolean rebootApp, Availability availability) {
this(key, defaultValue, rebootApp, true, null, availability);
}
public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
this(key, defaultValue, rebootApp, true, userDialogMessage, availability);
}
/**
* A setting backed by a shared preference.
*
* @param key The key used to store the value in the shared preferences.
* @param defaultValue The default value of the setting.
* @param rebootApp If the app should be rebooted, if this setting is changed.
* @param includeWithImportExport If this setting should be shown in the import/export dialog.
* @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value.
* @param availability Condition that must be true, for this setting to be available to configure.
*/
public Setting(@NonNull String key,
@NonNull T defaultValue,
boolean rebootApp,
boolean includeWithImportExport,
@Nullable String userDialogMessage,
@Nullable Availability availability
) {
this.key = Objects.requireNonNull(key);
this.value = this.defaultValue = Objects.requireNonNull(defaultValue);
this.rebootApp = rebootApp;
this.includeWithImportExport = includeWithImportExport;
this.userDialogMessage = (userDialogMessage == null) ? null : new StringRef(userDialogMessage);
this.availability = availability;
SETTINGS.add(this);
if (PATH_TO_SETTINGS.put(key, this) != null) {
// Debug setting may not be created yet so using Logger may cause an initialization crash.
// Show a toast instead.
Utils.showToastLong(this.getClass().getSimpleName()
+ " error: Duplicate Setting key found: " + key);
}
load();
}
/**
* Migrate a setting value if the path is renamed but otherwise the old and new settings are identical.
*/
public static <T> void migrateOldSettingToNew(@NonNull Setting<T> oldSetting, @NonNull Setting<T> newSetting) {
if (oldSetting == newSetting) throw new IllegalArgumentException();
if (!oldSetting.isSetToDefault()) {
Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting);
newSetting.save(oldSetting.value);
oldSetting.resetToDefault();
}
}
/**
* Migrate an old Setting value previously stored in a different SharedPreference.
*
* This method will be deleted in the future.
*/
public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) {
if (!oldPrefs.preferences.contains(settingKey)) {
return; // Nothing to do.
}
Object newValue = setting.get();
final Object migratedValue;
if (setting instanceof BooleanSetting) {
migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue);
} else if (setting instanceof IntegerSetting) {
migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue);
} else if (setting instanceof LongSetting) {
migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue);
} else if (setting instanceof FloatSetting) {
migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue);
} else if (setting instanceof StringSetting) {
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
} else {
Logger.printException(() -> "Unknown setting: " + setting);
// Remove otherwise it'll show a toast on every launch
oldPrefs.preferences.edit().remove(settingKey).apply();
return;
}
oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting.
if (migratedValue.equals(newValue)) {
Logger.printDebug(() -> "Value does not need migrating: " + settingKey);
return; // Old value is already equal to the new setting value.
}
Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey);
//noinspection unchecked
setting.save(migratedValue);
}
/**
* Sets, but does _not_ persistently save the value.
* This method is only to be used by the Settings preference code.
*
* This intentionally is a static method to deter
* accidental usage when {@link #save(Object)} was intended.
*/
public static void privateSetValueFromString(@NonNull Setting<?> setting, @NonNull String newValue) {
setting.setValueFromString(newValue);
}
/**
* Sets the value of {@link #value}, but do not save to {@link #preferences}.
*/
protected abstract void setValueFromString(@NonNull String newValue);
/**
* Load and set the value of {@link #value}.
*/
protected abstract void load();
/**
* Persistently saves the value.
*/
public abstract void save(@NonNull T newValue);
@NonNull
public abstract T get();
/**
* Identical to calling {@link #save(Object)} using {@link #defaultValue}.
*/
public void resetToDefault() {
save(defaultValue);
}
/**
* @return if this setting can be configured and used.
*/
public boolean isAvailable() {
return availability == null || availability.isAvailable();
}
/**
* @return if the currently set value is the same as {@link #defaultValue}
*/
public boolean isSetToDefault() {
return value.equals(defaultValue);
}
@NotNull
@Override
public String toString() {
return key + "=" + get();
}
// region Import / export
/**
* If a setting path has this prefix, then remove it before importing/exporting.
*/
private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_";
/**
* The path, minus any 'revanced' prefix to keep json concise.
*/
private String getImportExportKey() {
if (key.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) {
return key.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length());
}
return key;
}
/**
* @param importExportKey The JSON key. The JSONObject parameter will contain data for this key.
* @return the value stored using the import/export key. Do not set any values in this method.
*/
protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException;
/**
* Saves this instance to JSON.
* <p>
* To keep the JSON simple and readable,
* subclasses should not write out any embedded types (such as JSON Array or Dictionaries).
* <p>
* If this instance is not a type supported natively by JSON (ie: it's not a String/Integer/Float/Long),
* then subclasses can override this method and write out a String value representing the value.
*/
protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException {
json.put(importExportKey, value);
}
@NonNull
public static String exportToJson(@Nullable Context alertDialogContext) {
try {
JSONObject json = new JSONObject();
for (Setting<?> setting : allLoadedSettingsSorted()) {
String importExportKey = setting.getImportExportKey();
if (json.has(importExportKey)) {
throw new IllegalArgumentException("duplicate key found: " + importExportKey);
}
final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI.
//noinspection ConstantValue
if (setting.includeWithImportExport && (!setting.isSetToDefault() || exportDefaultValues)) {
setting.writeToJSON(json, importExportKey);
}
}
SponsorBlockSettings.showExportWarningIfNeeded(alertDialogContext);
if (json.length() == 0) {
return "";
}
String export = json.toString(0);
// Remove the outer JSON braces to make the output more compact,
// and leave less chance of the user forgetting to copy it
return export.substring(2, export.length() - 2);
} catch (JSONException e) {
Logger.printException(() -> "Export failure", e); // should never happen
return "";
}
}
/**
* @return if any settings that require a reboot were changed.
*/
public static boolean importFromJSON(@NonNull String settingsJsonString) {
try {
if (!settingsJsonString.matches("[\\s\\S]*\\{")) {
settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces
}
JSONObject json = new JSONObject(settingsJsonString);
boolean rebootSettingChanged = false;
int numberOfSettingsImported = 0;
for (Setting setting : SETTINGS) {
String key = setting.getImportExportKey();
if (json.has(key)) {
Object value = setting.readFromJSON(json, key);
if (!setting.get().equals(value)) {
rebootSettingChanged |= setting.rebootApp;
//noinspection unchecked
setting.save(value);
}
numberOfSettingsImported++;
} else if (setting.includeWithImportExport && !setting.isSetToDefault()) {
Logger.printDebug(() -> "Resetting to default: " + setting);
rebootSettingChanged |= setting.rebootApp;
setting.resetToDefault();
}
}
// SB Enum categories are saved using StringSettings.
// Which means they need to reload again if changed by other code (such as here).
// This call could be removed by creating a custom Setting class that manages the
// "String <-> Enum" logic or by adding an event hook of when settings are imported.
// But for now this is simple and works.
SponsorBlockSettings.updateFromImportedSettings();
Utils.showToastLong(numberOfSettingsImported == 0
? str("revanced_settings_import_reset")
: str("revanced_settings_import_success", numberOfSettingsImported));
return rebootSettingChanged;
} catch (JSONException | IllegalArgumentException ex) {
Utils.showToastLong(str("revanced_settings_import_failure_parse", ex.getMessage()));
Logger.printInfo(() -> "", ex);
} catch (Exception ex) {
Logger.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen
}
return false;
}
// End import / export
}

View File

@@ -1,69 +0,0 @@
package app.revanced.extension.shared.settings;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Objects;
@SuppressWarnings("unused")
public class StringSetting extends Setting<String> {
public StringSetting(String key, String defaultValue) {
super(key, defaultValue);
}
public StringSetting(String key, String defaultValue, boolean rebootApp) {
super(key, defaultValue, rebootApp);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) {
super(key, defaultValue, rebootApp, includeWithImportExport);
}
public StringSetting(String key, String defaultValue, String userDialogMessage) {
super(key, defaultValue, userDialogMessage);
}
public StringSetting(String key, String defaultValue, Availability availability) {
super(key, defaultValue, availability);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) {
super(key, defaultValue, rebootApp, userDialogMessage);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) {
super(key, defaultValue, rebootApp, availability);
}
public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) {
super(key, defaultValue, rebootApp, userDialogMessage, availability);
}
public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) {
super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability);
}
@Override
protected void load() {
value = preferences.getString(key, defaultValue);
}
@Override
protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException {
return json.getString(importExportKey);
}
@Override
protected void setValueFromString(@NonNull String newValue) {
value = Objects.requireNonNull(newValue);
}
@Override
public void save(@NonNull String newValue) {
// Must set before saving to preferences (otherwise importing fails to update UI correctly).
value = Objects.requireNonNull(newValue);
preferences.saveString(key, newValue);
}
@NonNull
@Override
public String get() {
return value;
}
}

View File

@@ -1,352 +0,0 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Insets;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.preference.*;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.ThemeHelper;
@SuppressWarnings("deprecation")
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/**
* Indicates that if a preference changes,
* to apply the change from the Setting to the UI component.
*/
public static boolean settingImportInProgress;
/**
* Confirm and restart dialog button text and title.
* Set by subclasses if Strings cannot be added as a resource.
*/
@Nullable
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle;
/**
* Used to prevent showing reboot dialog, if user cancels a setting user dialog.
*/
private boolean showingUserDialogMessage;
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
try {
Setting<?> setting = Setting.getSettingFromPath(str);
if (setting == null) {
return;
}
Preference pref = findPreference(str);
if (pref == null) {
return;
}
Logger.printDebug(() -> "Preference changed: " + setting.key);
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
updatePreference(pref, setting, true, settingImportInProgress);
// Update any other preference availability that may now be different.
updateUIAvailability();
if (settingImportInProgress) {
return;
}
if (!showingUserDialogMessage) {
if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) {
showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting);
} else if (setting.rebootApp) {
showRestartDialog(getContext());
}
}
} catch (Exception ex) {
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
}
};
@SuppressLint("UseCompatLoadingForDrawables")
public static Drawable getBackButtonDrawable() {
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
? "yt_outline_arrow_left_white_24"
: "yt_outline_arrow_left_black_24",
"drawable");
return Utils.getContext().getResources().getDrawable(backButtonResource);
}
/**
* Initialize this instance, and do any custom behavior.
* <p>
* To ensure all {@link Setting} instances are correctly synced to the UI,
* it is important that subclasses make a call or otherwise reference their Settings class bundle
* so all app specific {@link Setting} instances are loaded before this method returns.
*/
protected void initialize() {
final var identifier = Utils.getResourceIdentifier("revanced_prefs", "xml");
if (identifier == 0) return;
addPreferencesFromResource(identifier);
PreferenceScreen screen = getPreferenceScreen();
Utils.sortPreferenceGroups(screen);
Utils.setPreferenceTitlesToMultiLineIfNeeded(screen);
}
private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) {
Utils.verifyOnMainThread();
final var context = getContext();
if (confirmDialogTitle == null) {
confirmDialogTitle = str("revanced_settings_confirm_user_dialog_title");
}
showingUserDialogMessage = true;
new AlertDialog.Builder(context)
.setTitle(confirmDialogTitle)
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
if (setting.rebootApp) {
showRestartDialog(context);
}
})
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
})
.setOnDismissListener(dialog -> {
showingUserDialogMessage = false;
})
.setCancelable(false)
.show();
}
/**
* Updates all Preferences values and their availability using the current values in {@link Setting}.
*/
protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true,true);
}
/**
* Updates Preferences availability only using the status of {@link Setting}.
*/
protected void updateUIAvailability() {
updatePreferenceScreen(getPreferenceScreen(), false, false);
}
/**
* Syncs all UI Preferences to any {@link Setting} they represent.
*/
private void updatePreferenceScreen(@NonNull PreferenceScreen screen,
boolean syncSettingValue,
boolean applySettingToPreference) {
// Alternatively this could iterate thru all Settings and check for any matching Preferences,
// but there are many more Settings than UI preferences so it's more efficient to only check
// the Preferences.
for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) {
Preference pref = screen.getPreference(i);
if (pref instanceof PreferenceScreen) {
updatePreferenceScreen((PreferenceScreen) pref, syncSettingValue, applySettingToPreference);
} else if (pref.hasKey()) {
String key = pref.getKey();
Setting<?> setting = Setting.getSettingFromPath(key);
if (setting != null) {
updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
} else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference
|| pref instanceof EditTextPreference || pref instanceof ListPreference)) {
// Probably a typo in the patches preference declaration.
Logger.printException(() -> "Preference key has no setting: " + key);
}
}
}
}
/**
* Handles syncing a UI Preference with the {@link Setting} that backs it.
* If needed, subclasses can override this to handle additional UI Preference types.
*
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/
protected void syncSettingWithPreference(@NonNull Preference pref,
@NonNull Setting<?> setting,
boolean applySettingToPreference) {
if (pref instanceof SwitchPreference) {
SwitchPreference switchPref = (SwitchPreference) pref;
BooleanSetting boolSetting = (BooleanSetting) setting;
if (applySettingToPreference) {
switchPref.setChecked(boolSetting.get());
} else {
BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked());
}
} else if (pref instanceof EditTextPreference) {
EditTextPreference editPreference = (EditTextPreference) pref;
if (applySettingToPreference) {
editPreference.setText(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, editPreference.getText());
}
} else if (pref instanceof ListPreference) {
ListPreference listPref = (ListPreference) pref;
if (applySettingToPreference) {
listPref.setValue(setting.get().toString());
} else {
Setting.privateSetValueFromString(setting, listPref.getValue());
}
updateListPreferenceSummary(listPref, setting);
} else {
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
}
}
/**
* Updates a UI Preference with the {@link Setting} that backs it.
*
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference.
*/
private void updatePreference(@NonNull Preference pref, @NonNull Setting<?> setting,
boolean syncSetting, boolean applySettingToPreference) {
if (!syncSetting && applySettingToPreference) {
throw new IllegalArgumentException();
}
if (syncSetting) {
syncSettingWithPreference(pref, setting, applySettingToPreference);
}
updatePreferenceAvailability(pref, setting);
}
protected void updatePreferenceAvailability(@NonNull Preference pref, @NonNull Setting<?> setting) {
pref.setEnabled(setting.isAvailable());
}
protected void updateListPreferenceSummary(ListPreference listPreference, Setting<?> setting) {
String objectStringValue = setting.get().toString();
final int entryIndex = listPreference.findIndexOfValue(objectStringValue);
if (entryIndex >= 0) {
listPreference.setSummary(listPreference.getEntries()[entryIndex]);
} else {
// Value is not an available option.
// User manually edited import data, or options changed and current selection is no longer available.
// Still show the value in the summary, so it's clear that something is selected.
listPreference.setSummary(objectStringValue);
}
}
public static void showRestartDialog(@NonNull final Context context) {
Utils.verifyOnMainThread();
if (restartDialogTitle == null) {
restartDialogTitle = str("revanced_settings_restart_title");
}
if (restartDialogButtonText == null) {
restartDialogButtonText = str("revanced_settings_restart");
}
new AlertDialog.Builder(context)
.setMessage(restartDialogTitle)
.setPositiveButton(restartDialogButtonText, (dialog, id)
-> Utils.restartApp(context))
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.show();
}
@SuppressLint("ResourceType")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setSharedPreferencesName(Setting.preferences.name);
// Must initialize before adding change listener,
// otherwise the syncing of Setting -> UI
// causes a callback to the listener even though nothing changed.
initialize();
updateUIToSettingValues();
setPreferenceScreenToolbar(getPreferenceScreen());
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
} catch (Exception ex) {
Logger.printException(() -> "onCreate() failure", ex);
}
}
@Override
public void onDestroy() {
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
super.onDestroy();
}
private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
for (int i = 0, preferenceCount = parentScreen.getPreferenceCount(); i < preferenceCount; i++) {
Preference childPreference = parentScreen.getPreference(i);
if (childPreference instanceof PreferenceScreen) {
// Recursively set sub preferences.
setPreferenceScreenToolbar((PreferenceScreen) childPreference);
childPreference.setOnPreferenceClickListener(
childScreen -> {
Dialog preferenceScreenDialog = ((PreferenceScreen) childScreen).getDialog();
ViewGroup rootView = (ViewGroup) preferenceScreenDialog
.findViewById(android.R.id.content)
.getParent();
// Fix required for Android 15 and YT 19.45+
// FIXME:
// On Android 15 the text layout is not aligned the same as the parent
// screen and it looks a little off. Otherwise this works.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
rootView.setOnApplyWindowInsetsListener((v, insets) -> {
Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
v.setPadding(0, statusInsets.top, 0, 0);
return insets;
});
}
Toolbar toolbar = new Toolbar(childScreen.getContext());
toolbar.setTitle(childScreen.getTitle());
toolbar.setNavigationIcon(getBackButtonDrawable());
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
final int margin = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
);
toolbar.setTitleMargin(margin, 0, margin, 0);
}
TextView toolbarTextView = Utils.getChildView(toolbar,
true, TextView.class::isInstance);
if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor());
}
rootView.addView(toolbar, 0);
return false;
}
);
}
}
}
}

View File

@@ -1,99 +0,0 @@
package app.revanced.extension.shared.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Build;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.text.InputType;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.EditText;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import static app.revanced.extension.shared.StringRef.str;
@SuppressWarnings({"unused", "deprecation"})
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
private String existingSettings;
private void init() {
setSelectable(true);
EditText editText = getEditText();
editText.setTextIsSelectable(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
editText.setAutofillHints((String) null);
}
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap.
setOnPreferenceClickListener(this);
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public ImportExportPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ImportExportPreference(Context context) {
super(context);
init();
}
@Override
public boolean onPreferenceClick(Preference preference) {
try {
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
existingSettings = Setting.exportToJson(getContext());
getEditText().setText(existingSettings);
} catch (Exception ex) {
Logger.printException(() -> "showDialog failure", ex);
}
return true;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
// Show the user the settings in JSON format.
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
Utils.setClipboard(getEditText().getText().toString());
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
importSettings(getEditText().getText().toString());
});
} catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
}
}
private void importSettings(String replacementSettings) {
try {
if (replacementSettings.equals(existingSettings)) {
return;
}
AbstractPreferenceFragment.settingImportInProgress = true;
final boolean rebootNeeded = Setting.importFromJSON(replacementSettings);
if (rebootNeeded) {
AbstractPreferenceFragment.showRestartDialog(getContext());
}
} catch (Exception ex) {
Logger.printException(() -> "importSettings failure", ex);
} finally {
AbstractPreferenceFragment.settingImportInProgress = false;
}
}
}

View File

@@ -1,361 +0,0 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.youtube.requests.Route.Method.GET;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.Window;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.requests.Requester;
import app.revanced.extension.youtube.requests.Route;
/**
* Opens a dialog showing official links.
*/
@SuppressWarnings({"unused", "deprecation"})
public class ReVancedAboutPreference extends Preference {
private static String useNonBreakingHyphens(String text) {
// Replace any dashes with non breaking dashes, so the English text 'pre-release'
// and the dev release number does not break and cover two lines.
return text.replace("-", "&#8209;"); // #8209 = non breaking hyphen.
}
private static String getColorHexString(int color) {
return String.format("#%06X", (0x00FFFFFF & color));
}
protected boolean isDarkModeEnabled() {
Configuration config = getContext().getResources().getConfiguration();
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
}
/**
* Subclasses can override this and provide a themed color.
*/
protected int getLightColor() {
return Color.WHITE;
}
/**
* Subclasses can override this and provide a themed color.
*/
protected int getDarkColor() {
return Color.BLACK;
}
/**
* Apps that do not support bundling resources must override this.
*
* @return A localized string to display for the key.
*/
protected String getString(String key, Object ... args) {
return str(key, args);
}
private String createDialogHtml(WebLink[] aboutLinks) {
final boolean isNetworkConnected = Utils.isNetworkConnected();
StringBuilder builder = new StringBuilder();
builder.append("<html>");
builder.append("<body style=\"text-align: center; padding: 10px;\">");
final boolean isDarkMode = isDarkModeEnabled();
String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor());
String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor());
// Apply light/dark mode colors.
builder.append(String.format(
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
backgroundColorHex, foregroundColorHex, foregroundColorHex));
if (isNetworkConnected) {
builder.append("<img style=\"width: 100px; height: 100px;\" "
// Hide the image if it does not load.
+ "onerror=\"this.style.display='none';\" "
+ "src=\"").append(AboutLinksRoutes.aboutLogoUrl).append("\" />");
}
String patchesVersion = Utils.getPatchesReleaseVersion();
// Add the title.
builder.append("<h1>")
.append("ReVanced")
.append("</h1>");
builder.append("<p>")
// Replace hyphens with non breaking dashes so the version number does not break lines.
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_body", patchesVersion)))
.append("</p>");
// Add a disclaimer if using a dev release.
if (patchesVersion.contains("dev")) {
builder.append("<h3>")
// English text 'Pre-release' can break lines.
.append(useNonBreakingHyphens(getString("revanced_settings_about_links_dev_header")))
.append("</h3>");
builder.append("<p>")
.append(getString("revanced_settings_about_links_dev_body"))
.append("</p>");
}
builder.append("<h2 style=\"margin-top: 30px;\">")
.append(getString("revanced_settings_about_links_header"))
.append("</h2>");
builder.append("<div>");
for (WebLink link : aboutLinks) {
builder.append("<div style=\"margin-bottom: 20px;\">");
builder.append(String.format("<a href=\"%s\">%s</a>", link.url, link.name));
builder.append("</div>");
}
builder.append("</div>");
builder.append("</body></html>");
return builder.toString();
}
{
setOnPreferenceClickListener(pref -> {
// Show a progress spinner if the social links are not fetched yet.
if (!AboutLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
// Show a progress spinner, but only if the api fetch takes more than a half a second.
final long delayToShowProgressSpinner = 500;
ProgressDialog progress = new ProgressDialog(getContext());
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
Handler handler = new Handler(Looper.getMainLooper());
Runnable showDialogRunnable = progress::show;
handler.postDelayed(showDialogRunnable, delayToShowProgressSpinner);
Utils.runOnBackgroundThread(() ->
fetchLinksAndShowDialog(handler, showDialogRunnable, progress));
} else {
// No network call required and can run now.
fetchLinksAndShowDialog(null, null, null);
}
return false;
});
}
private void fetchLinksAndShowDialog(@Nullable Handler handler,
Runnable showDialogRunnable,
@Nullable ProgressDialog progress) {
WebLink[] links = AboutLinksRoutes.fetchAboutLinks();
String htmlDialog = createDialogHtml(links);
// Enable to randomly force a delay to debug the spinner logic.
final boolean debugSpinnerDelayLogic = false;
//noinspection ConstantConditions
if (debugSpinnerDelayLogic && handler != null && Math.random() < 0.5f) {
Utils.doNothingForDuration((long) (Math.random() * 4000));
}
Utils.runOnMainThreadNowOrLater(() -> {
if (handler != null) {
handler.removeCallbacks(showDialogRunnable);
}
if (progress != null) {
progress.dismiss();
}
new WebViewDialog(getContext(), htmlDialog).show();
});
}
public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReVancedAboutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReVancedAboutPreference(Context context) {
super(context);
}
}
/**
* Displays html content as a dialog. Any links a user taps on are opened in an external browser.
*/
class WebViewDialog extends Dialog {
private final String htmlContent;
public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) {
super(context);
this.htmlContent = htmlContent;
}
// JS required to hide any broken images. No remote javascript is ever loaded.
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
WebView webView = new WebView(getContext());
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new OpenLinksExternallyWebClient());
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
setContentView(webView);
}
private class OpenLinksExternallyWebClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
getContext().startActivity(intent);
} catch (Exception ex) {
Logger.printException(() -> "Open link failure", ex);
}
// Dismiss the about dialog using a delay,
// otherwise without a delay the UI looks hectic with the dialog dismissing
// to show the settings while simultaneously a web browser is opening.
Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500);
return true;
}
}
}
class WebLink {
final boolean preferred;
String name;
final String url;
WebLink(JSONObject json) throws JSONException {
this(json.getBoolean("preferred"),
json.getString("name"),
json.getString("url")
);
}
WebLink(boolean preferred, String name, String url) {
this.preferred = preferred;
this.name = name;
this.url = url;
}
@NonNull
@Override
public String toString() {
return "WebLink{" +
"preferred=" + preferred +
", name='" + name + '\'' +
", url='" + url + '\'' +
'}';
}
}
class AboutLinksRoutes {
/**
* Backup icon url if the API call fails.
*/
public static volatile String aboutLogoUrl = "https://revanced.app/favicon.ico";
/**
* Links to use if fetch links api call fails.
*/
private static final WebLink[] NO_CONNECTION_STATIC_LINKS = {
new WebLink(true, "ReVanced.app", "https://revanced.app")
};
private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v4";
private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/about").compile();
@Nullable
private static volatile WebLink[] fetchedLinks;
static boolean hasFetchedLinks() {
return fetchedLinks != null;
}
static WebLink[] fetchAboutLinks() {
try {
if (hasFetchedLinks()) return fetchedLinks;
// Check if there is no internet connection.
if (!Utils.isNetworkConnected()) return NO_CONNECTION_STATIC_LINKS;
HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(SOCIAL_LINKS_PROVIDER, GET_SOCIAL);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
Logger.printDebug(() -> "Fetching social links from: " + connection.getURL());
// Do not show an exception toast if the server is down
final int responseCode = connection.getResponseCode();
if (responseCode != 200) {
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
return NO_CONNECTION_STATIC_LINKS;
}
JSONObject json = Requester.parseJSONObjectAndDisconnect(connection);
aboutLogoUrl = json.getJSONObject("branding").getString("logo");
List<WebLink> links = new ArrayList<>();
JSONArray donations = json.getJSONObject("donations").getJSONArray("links");
for (int i = 0, length = donations.length(); i < length; i++) {
WebLink link = new WebLink(donations.getJSONObject(i));
if (link.preferred) {
// This could be localized, but TikTok does not support localized resources.
// All link names returned by the api are also non localized.
link.name = "Donate";
links.add(link);
}
}
JSONArray socials = json.getJSONArray("socials");
for (int i = 0, length = socials.length(); i < length; i++) {
WebLink link = new WebLink(socials.getJSONObject(i));
links.add(link);
}
Logger.printDebug(() -> "links: " + links);
return fetchedLinks = links.toArray(new WebLink[0]);
} catch (SocketTimeoutException ex) {
Logger.printInfo(() -> "Could not fetch social links", ex); // No toast.
} catch (JSONException ex) {
Logger.printException(() -> "Could not parse about information", ex);
} catch (Exception ex) {
Logger.printException(() -> "Failed to get about information", ex);
}
return NO_CONNECTION_STATIC_LINKS;
}
}

View File

@@ -1,67 +0,0 @@
package app.revanced.extension.shared.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.Logger;
import java.util.Objects;
import static app.revanced.extension.shared.StringRef.str;
@SuppressWarnings({"unused", "deprecation"})
public class ResettableEditTextPreference extends EditTextPreference {
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ResettableEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ResettableEditTextPreference(Context context) {
super(context);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
Utils.setEditTextDialogTheme(builder);
Setting<?> setting = Setting.getSettingFromPath(getKey());
if (setting != null) {
builder.setNeutralButton(str("revanced_settings_reset"), null);
}
}
@Override
protected void showDialog(Bundle state) {
super.showDialog(state);
// Override the button click listener to prevent dismissing the dialog.
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
if (button == null) {
return;
}
button.setOnClickListener(v -> {
try {
Setting<?> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey()));
String defaultStringValue = setting.defaultValue.toString();
EditText editText = getEditText();
editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
} catch (Exception ex) {
Logger.printException(() -> "reset failure", ex);
}
});
}
}

View File

@@ -1,190 +0,0 @@
package app.revanced.extension.shared.settings.preference;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceFragment;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import java.util.Objects;
/**
* Shared categories, and helper methods.
*
* The various save methods store numbers as Strings,
* which is required if using {@link PreferenceFragment}.
*
* If saved numbers will not be used with a preference fragment,
* then store the primitive numbers using the {@link #preferences} itself.
*/
public class SharedPrefCategory {
@NonNull
public final String name;
@NonNull
public final SharedPreferences preferences;
public SharedPrefCategory(@NonNull String name) {
this.name = Objects.requireNonNull(name);
preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE);
}
private void removeConflictingPreferenceKeyValue(@NonNull String key) {
Logger.printException(() -> "Found conflicting preference: " + key);
removeKey(key);
}
private void saveObjectAsString(@NonNull String key, @Nullable Object value) {
preferences.edit().putString(key, (value == null ? null : value.toString())).apply();
}
/**
* Removes any preference data type that has the specified key.
*/
public void removeKey(@NonNull String key) {
preferences.edit().remove(Objects.requireNonNull(key)).apply();
}
public void saveBoolean(@NonNull String key, boolean value) {
preferences.edit().putBoolean(key, value).apply();
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveEnumAsString(@NonNull String key, @Nullable Enum<?> value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveIntegerString(@NonNull String key, @Nullable Integer value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveLongString(@NonNull String key, @Nullable Long value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveFloatString(@NonNull String key, @Nullable Float value) {
saveObjectAsString(key, value);
}
/**
* @param value a NULL parameter removes the value from the preferences
*/
public void saveString(@NonNull String key, @Nullable String value) {
saveObjectAsString(key, value);
}
@NonNull
public String getString(@NonNull String key, @NonNull String _default) {
Objects.requireNonNull(_default);
try {
return preferences.getString(key, _default);
} catch (ClassCastException ex) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
return _default;
}
}
@NonNull
public <T extends Enum<?>> T getEnum(@NonNull String key, @NonNull T _default) {
Objects.requireNonNull(_default);
try {
String enumName = preferences.getString(key, null);
if (enumName != null) {
try {
// noinspection unchecked
return (T) Enum.valueOf(_default.getClass(), enumName);
} catch (IllegalArgumentException ex) {
// Info level to allow removing enum values in the future without showing any user errors.
Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName);
removeKey(key);
}
}
} catch (ClassCastException ex) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
}
return _default;
}
public boolean getBoolean(@NonNull String key, boolean _default) {
try {
return preferences.getBoolean(key, _default);
} catch (ClassCastException ex) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
return _default;
}
}
@NonNull
public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) {
try {
String value = preferences.getString(key, null);
if (value != null) {
return Integer.valueOf(value);
}
} catch (ClassCastException | NumberFormatException ex) {
try {
// Old data previously stored as primitive.
return preferences.getInt(key, _default);
} catch (ClassCastException ex2) {
// Value stored is a completely different type (should never happen).
removeConflictingPreferenceKeyValue(key);
}
}
return _default;
}
@NonNull
public Long getLongString(@NonNull String key, @NonNull Long _default) {
try {
String value = preferences.getString(key, null);
if (value != null) {
return Long.valueOf(value);
}
} catch (ClassCastException | NumberFormatException ex) {
try {
return preferences.getLong(key, _default);
} catch (ClassCastException ex2) {
removeConflictingPreferenceKeyValue(key);
}
}
return _default;
}
@NonNull
public Float getFloatString(@NonNull String key, @NonNull Float _default) {
try {
String value = preferences.getString(key, null);
if (value != null) {
return Float.valueOf(value);
}
} catch (ClassCastException | NumberFormatException ex) {
try {
return preferences.getFloat(key, _default);
} catch (ClassCastException ex2) {
removeConflictingPreferenceKeyValue(key);
}
}
return _default;
}
@NonNull
@Override
public String toString() {
return name;
}
}

View File

@@ -1,77 +0,0 @@
package app.revanced.extension.syncforreddit;
import android.util.Pair;
import androidx.annotation.Nullable;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
/**
* @noinspection unused
*/
public class FixRedditVideoDownloadPatch {
private static @Nullable Pair<Integer, String> getBestMpEntry(Element element) {
var representations = element.getElementsByTagName("Representation");
var entries = new ArrayList<Pair<Integer, String>>();
for (int i = 0; i < representations.getLength(); i++) {
Element representation = (Element) representations.item(i);
var bandwidthStr = representation.getAttribute("bandwidth");
try {
var bandwidth = Integer.parseInt(bandwidthStr);
var baseUrl = representation.getElementsByTagName("BaseURL").item(0);
if (baseUrl != null) {
entries.add(new Pair<>(bandwidth, baseUrl.getTextContent()));
}
} catch (NumberFormatException ignored) {
}
}
if (entries.isEmpty()) {
return null;
}
Collections.sort(entries, (e1, e2) -> e2.first - e1.first);
return entries.get(0);
}
private static String[] parse(byte[] data) throws ParserConfigurationException, IOException, SAXException {
var adaptionSets = DocumentBuilderFactory
.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream(data))
.getElementsByTagName("AdaptationSet");
String videoUrl = null;
String audioUrl = null;
for (int i = 0; i < adaptionSets.getLength(); i++) {
Element element = (Element) adaptionSets.item(i);
var contentType = element.getAttribute("contentType");
var bestEntry = getBestMpEntry(element);
if (bestEntry == null) continue;
if (contentType.equalsIgnoreCase("video")) {
videoUrl = bestEntry.second;
} else if (contentType.equalsIgnoreCase("audio")) {
audioUrl = bestEntry.second;
}
}
return new String[]{videoUrl, audioUrl};
}
public static String[] getLinks(byte[] data) {
try {
return parse(data);
} catch (ParserConfigurationException | IOException | SAXException e) {
return new String[]{null, null};
}
}
}

View File

@@ -1,24 +0,0 @@
package app.revanced.extension.syncforreddit;
import com.laurencedawson.reddit_sync.ui.activities.WebViewActivity;
import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch;
/** @noinspection unused*/
public class FixSLinksPatch extends BaseFixSLinksPatch {
static {
INSTANCE = new FixSLinksPatch();
}
private FixSLinksPatch() {
webViewActivityClass = WebViewActivity.class;
}
public static boolean patchResolveSLink(String link) {
return INSTANCE.resolveSLink(link);
}
public static void patchSetAccessToken(String accessToken) {
INSTANCE.setAccessToken(accessToken);
}
}

View File

@@ -1,25 +0,0 @@
package app.revanced.extension.tiktok;
import app.revanced.extension.shared.settings.StringSetting;
public class Utils {
// Edit: This could be handled using a custom Setting<Long[]> class
// that saves its value to preferences and JSON using the formatted String created here.
public static long[] parseMinMax(StringSetting setting) {
final String[] minMax = setting.get().split("-");
if (minMax.length == 2) {
try {
final long min = Long.parseLong(minMax[0]);
final long max = Long.parseLong(minMax[1]);
if (min <= max && min >= 0) return new long[]{min, max};
} catch (NumberFormatException ignored) {
}
}
setting.save("0-" + Long.MAX_VALUE);
return new long[]{0L, Long.MAX_VALUE};
}
}

View File

@@ -1,13 +0,0 @@
package app.revanced.extension.tiktok.cleardisplay;
import app.revanced.extension.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class RememberClearDisplayPatch {
public static boolean getClearDisplayState() {
return Settings.CLEAR_DISPLAY.get();
}
public static void rememberClearDisplayState(boolean newState) {
Settings.CLEAR_DISPLAY.save(newState);
}
}

View File

@@ -1,14 +0,0 @@
package app.revanced.extension.tiktok.download;
import app.revanced.extension.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class DownloadsPatch {
public static String getDownloadPath() {
return Settings.DOWNLOAD_PATH.get();
}
public static boolean shouldRemoveWatermark() {
return Settings.DOWNLOAD_WATERMARK.get();
}
}

View File

@@ -1,16 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class AdsFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.REMOVE_ADS.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.isAd() || item.isWithPromotionalMusic();
}
}

View File

@@ -1,34 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.FeedItemList;
import java.util.Iterator;
import java.util.List;
public final class FeedItemsFilter {
private static final List<IFilter> FILTERS = List.of(
new AdsFilter(),
new LiveFilter(),
new StoryFilter(),
new ImageVideoFilter(),
new ViewCountFilter(),
new LikeCountFilter()
);
public static void filter(FeedItemList feedItemList) {
Iterator<Aweme> feedItemListIterator = feedItemList.items.iterator();
while (feedItemListIterator.hasNext()) {
Aweme item = feedItemListIterator.next();
if (item == null) continue;
for (IFilter filter : FILTERS) {
boolean enabled = filter.getEnabled();
if (enabled && filter.getFiltered(item)) {
feedItemListIterator.remove();
break;
}
}
}
}
}

View File

@@ -1,9 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public interface IFilter {
boolean getEnabled();
boolean getFiltered(Aweme item);
}

View File

@@ -1,16 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class ImageVideoFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.HIDE_IMAGE.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.isImage() || item.isPhotoMode();
}
}

View File

@@ -1,32 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
import static app.revanced.extension.tiktok.Utils.parseMinMax;
public final class LikeCountFilter implements IFilter {
final long minLike;
final long maxLike;
LikeCountFilter() {
long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES);
minLike = minMax[0];
maxLike = minMax[1];
}
@Override
public boolean getEnabled() {
return true;
}
@Override
public boolean getFiltered(Aweme item) {
AwemeStatistics statistics = item.getStatistics();
if (statistics == null) return false;
long likeCount = statistics.getDiggCount();
return likeCount < minLike || likeCount > maxLike;
}
}

View File

@@ -1,16 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class LiveFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.HIDE_LIVE.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.isLive() || item.isLiveReplay();
}
}

View File

@@ -1,16 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
public class StoryFilter implements IFilter {
@Override
public boolean getEnabled() {
return Settings.HIDE_STORY.get();
}
@Override
public boolean getFiltered(Aweme item) {
return item.getIsTikTokStory();
}
}

View File

@@ -1,32 +0,0 @@
package app.revanced.extension.tiktok.feedfilter;
import app.revanced.extension.tiktok.settings.Settings;
import com.ss.android.ugc.aweme.feed.model.Aweme;
import com.ss.android.ugc.aweme.feed.model.AwemeStatistics;
import static app.revanced.extension.tiktok.Utils.parseMinMax;
public class ViewCountFilter implements IFilter {
final long minView;
final long maxView;
ViewCountFilter() {
long[] minMax = parseMinMax(Settings.MIN_MAX_VIEWS);
minView = minMax[0];
maxView = minMax[1];
}
@Override
public boolean getEnabled() {
return true;
}
@Override
public boolean getFiltered(Aweme item) {
AwemeStatistics statistics = item.getStatistics();
if (statistics == null) return false;
long playCount = statistics.getPlayCount();
return playCount < minView || playCount > maxView;
}
}

View File

@@ -1,82 +0,0 @@
package app.revanced.extension.tiktok.settings;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.tiktok.settings.preference.ReVancedPreferenceFragment;
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
/**
* Hooks AdPersonalizationActivity.
* <p>
* This class is responsible for injecting our own fragment by replacing the AdPersonalizationActivity.
*
* @noinspection unused
*/
public class AdPersonalizationActivityHook {
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
try {
Class<?> entryClazz = Class.forName(entryClazzName);
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
return entryConstructor.newInstance(buttonInfo);
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
InstantiationException e) {
throw new RuntimeException(e);
}
}
/***
* Initialize the settings menu.
* @param base The activity to initialize the settings menu on.
* @return Whether the settings menu should be initialized.
*/
public static boolean initialize(AdPersonalizationActivity base) {
Bundle extras = base.getIntent().getExtras();
if (extras != null && !extras.getBoolean("revanced", false)) return false;
SettingsStatus.load();
LinearLayout linearLayout = new LinearLayout(base);
linearLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setFitsSystemWindows(true);
linearLayout.setTransitionGroup(true);
FrameLayout fragment = new FrameLayout(base);
fragment.setLayoutParams(new FrameLayout.LayoutParams(-1, -1));
int fragmentId = View.generateViewId();
fragment.setId(fragmentId);
linearLayout.addView(fragment);
base.setContentView(linearLayout);
PreferenceFragment preferenceFragment = new ReVancedPreferenceFragment();
base.getFragmentManager().beginTransaction().replace(fragmentId, preferenceFragment).commit();
return true;
}
private static void startSettingsActivity() {
Context appContext = Utils.getContext();
if (appContext != null) {
Intent intent = new Intent(appContext, AdPersonalizationActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("revanced", true);
appContext.startActivity(intent);
} else {
Logger.printDebug(() -> "Utils.getContext() return null");
}
}
}

View File

@@ -1,26 +0,0 @@
package app.revanced.extension.tiktok.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;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.StringSetting;
public class Settings extends BaseSettings {
public static final BooleanSetting REMOVE_ADS = new BooleanSetting("remove_ads", TRUE, true);
public static final BooleanSetting HIDE_LIVE = new BooleanSetting("hide_live", FALSE, true);
public static final BooleanSetting HIDE_STORY = new BooleanSetting("hide_story", FALSE, true);
public static final BooleanSetting HIDE_IMAGE = new BooleanSetting("hide_image", FALSE, true);
public static final StringSetting MIN_MAX_VIEWS = new StringSetting("min_max_views", "0-" + Long.MAX_VALUE, true);
public static final StringSetting MIN_MAX_LIKES = new StringSetting("min_max_likes", "0-" + Long.MAX_VALUE, true);
public static final StringSetting DOWNLOAD_PATH = new StringSetting("down_path", "DCIM/TikTok");
public static final BooleanSetting DOWNLOAD_WATERMARK = new BooleanSetting("down_watermark", TRUE);
public static final BooleanSetting CLEAR_DISPLAY = new BooleanSetting("clear_display", FALSE);
public static final FloatSetting REMEMBERED_SPEED = new FloatSetting("REMEMBERED_SPEED", 1.0f);
public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", TRUE, true);
public static final StringSetting SIM_SPOOF_ISO = new StringSetting("simspoof_iso", "us");
public static final StringSetting SIMSPOOF_MCCMNC = new StringSetting("simspoof_mccmnc", "310160");
public static final StringSetting SIMSPOOF_OP_NAME = new StringSetting("simspoof_op_name", "T-Mobile");
}

View File

@@ -1,23 +0,0 @@
package app.revanced.extension.tiktok.settings;
public class SettingsStatus {
public static boolean feedFilterEnabled = false;
public static boolean downloadEnabled = false;
public static boolean simSpoofEnabled = false;
public static void enableFeedFilter() {
feedFilterEnabled = true;
}
public static void enableDownload() {
downloadEnabled = true;
}
public static void enableSimSpoof() {
simSpoofEnabled = true;
}
public static void load() {
}
}

View File

@@ -1,124 +0,0 @@
package app.revanced.extension.tiktok.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Environment;
import android.preference.DialogPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import app.revanced.extension.shared.settings.StringSetting;
@SuppressWarnings("deprecation")
public class DownloadPathPreference extends DialogPreference {
private final Context context;
private final String[] entryValues = {"DCIM", "Movies", "Pictures"};
private String mValue;
private boolean mValueSet;
private int mediaPathIndex;
private String childDownloadPath;
public DownloadPathPreference(Context context, String title, StringSetting setting) {
super(context);
this.context = context;
this.setTitle(title);
this.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.get());
this.setKey(setting.key);
this.setValue(setting.get());
}
public String getValue() {
return this.mValue;
}
public void setValue(String value) {
final boolean changed = !TextUtils.equals(mValue, value);
if (changed || !mValueSet) {
mValue = value;
mValueSet = true;
persistString(value);
if (changed) {
notifyDependencyChange(shouldDisableDependents());
notifyChanged();
}
}
}
@Override
protected View onCreateDialogView() {
String currentMedia = getValue().split("/")[0];
childDownloadPath = getValue().substring(getValue().indexOf("/") + 1);
mediaPathIndex = findIndexOf(currentMedia);
LinearLayout dialogView = new LinearLayout(context);
RadioGroup mediaPath = new RadioGroup(context);
mediaPath.setLayoutParams(new RadioGroup.LayoutParams(-1, -2));
for (String entryValue : entryValues) {
RadioButton radioButton = new RadioButton(context);
radioButton.setText(entryValue);
radioButton.setId(View.generateViewId());
mediaPath.addView(radioButton);
}
mediaPath.setOnCheckedChangeListener((radioGroup, id) -> {
RadioButton radioButton = radioGroup.findViewById(id);
mediaPathIndex = findIndexOf(radioButton.getText().toString());
});
mediaPath.check(mediaPath.getChildAt(mediaPathIndex).getId());
EditText downloadPath = new EditText(context);
downloadPath.setInputType(InputType.TYPE_CLASS_TEXT);
downloadPath.setText(childDownloadPath);
downloadPath.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
childDownloadPath = editable.toString();
}
});
dialogView.setLayoutParams(new LinearLayout.LayoutParams(-1, -1));
dialogView.setOrientation(LinearLayout.VERTICAL);
dialogView.addView(mediaPath);
dialogView.addView(downloadPath);
return dialogView;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
builder.setTitle("Download Path");
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNegativeButton(android.R.string.cancel, null);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult && mediaPathIndex >= 0) {
String newValue = entryValues[mediaPathIndex] + "/" + childDownloadPath;
setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + newValue);
setValue(newValue);
}
}
private int findIndexOf(String str) {
for (int i = 0; i < entryValues.length; i++) {
if (str.equals(entryValues[i])) return i;
}
return -1;
}
}

View File

@@ -1,17 +0,0 @@
package app.revanced.extension.tiktok.settings.preference;
import android.content.Context;
import android.preference.EditTextPreference;
import app.revanced.extension.shared.settings.StringSetting;
public class InputTextPreference extends EditTextPreference {
public InputTextPreference(Context context, String title, String summary, StringSetting setting) {
super(context);
this.setTitle(title);
this.setSummary(summary);
this.setKey(setting.key);
this.setText(setting.get());
}
}

View File

@@ -1,130 +0,0 @@
package app.revanced.extension.tiktok.settings.preference;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.preference.DialogPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import app.revanced.extension.shared.settings.StringSetting;
@SuppressWarnings("deprecation")
public class RangeValuePreference extends DialogPreference {
private final Context context;
private String minValue;
private String maxValue;
private String mValue;
private boolean mValueSet;
public RangeValuePreference(Context context, String title, String summary, StringSetting setting) {
super(context);
this.context = context;
setTitle(title);
setSummary(summary);
setKey(setting.key);
setValue(setting.get());
}
public void setValue(String value) {
final boolean changed = !TextUtils.equals(mValue, value);
if (changed || !mValueSet) {
mValue = value;
mValueSet = true;
persistString(value);
if (changed) {
notifyDependencyChange(shouldDisableDependents());
notifyChanged();
}
}
}
public String getValue() {
return mValue;
}
@Override
protected View onCreateDialogView() {
minValue = getValue().split("-")[0];
maxValue = getValue().split("-")[1];
LinearLayout dialogView = new LinearLayout(context);
dialogView.setOrientation(LinearLayout.VERTICAL);
LinearLayout minView = new LinearLayout(context);
minView.setOrientation(LinearLayout.HORIZONTAL);
TextView min = new TextView(context);
min.setText("Min: ");
minView.addView(min);
EditText minEditText = new EditText(context);
minEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
minEditText.setText(minValue);
minView.addView(minEditText);
dialogView.addView(minView);
LinearLayout maxView = new LinearLayout(context);
maxView.setOrientation(LinearLayout.HORIZONTAL);
TextView max = new TextView(context);
max.setText("Max: ");
maxView.addView(max);
EditText maxEditText = new EditText(context);
maxEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
maxEditText.setText(maxValue);
maxView.addView(maxEditText);
dialogView.addView(maxView);
minEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
minValue = editable.toString();
}
});
maxEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
maxValue = editable.toString();
}
});
return dialogView;
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNegativeButton(android.R.string.cancel, null);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
String newValue = minValue + "-" + maxValue;
setValue(newValue);
}
}
}

View File

@@ -1,54 +0,0 @@
package app.revanced.extension.tiktok.settings.preference;
import android.preference.Preference;
import android.preference.PreferenceScreen;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.extension.tiktok.settings.preference.categories.DownloadsPreferenceCategory;
import app.revanced.extension.tiktok.settings.preference.categories.FeedFilterPreferenceCategory;
import app.revanced.extension.tiktok.settings.preference.categories.ExtensionPreferenceCategory;
import app.revanced.extension.tiktok.settings.preference.categories.SimSpoofPreferenceCategory;
import org.jetbrains.annotations.NotNull;
/**
* 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) {
if (pref instanceof RangeValuePreference) {
RangeValuePreference rangeValuePref = (RangeValuePreference) pref;
Setting.privateSetValueFromString(setting, rangeValuePref.getValue());
} else if (pref instanceof DownloadPathPreference) {
DownloadPathPreference downloadPathPref = (DownloadPathPreference) pref;
Setting.privateSetValueFromString(setting, downloadPathPref.getValue());
} else {
super.syncSettingWithPreference(pref, setting, applySettingToPreference);
}
}
@Override
protected void initialize() {
final var context = getContext();
// Currently no resources can be compiled for TikTok (fails with aapt error).
// So all TikTok Strings are hard coded in the extension.
restartDialogTitle = "Refresh and restart";
restartDialogButtonText = "Restart";
confirmDialogTitle = "Do you wish to proceed?";
PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context);
setPreferenceScreen(preferenceScreen);
// Custom categories reference app specific Settings class.
new FeedFilterPreferenceCategory(context, preferenceScreen);
new DownloadsPreferenceCategory(context, preferenceScreen);
new SimSpoofPreferenceCategory(context, preferenceScreen);
new ExtensionPreferenceCategory(context, preferenceScreen);
}
}

View File

@@ -1,56 +0,0 @@
package app.revanced.extension.tiktok.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import java.util.Map;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
@SuppressWarnings("unused")
public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
/**
* Because resources cannot be added to TikTok,
* these strings are copied from the shared strings.xml file.
*
* Changes here must also be made in strings.xml
*/
private final Map<String, String> aboutStrings = Map.of(
"revanced_settings_about_links_body", "You are using ReVanced Patches version <i>%s</i>",
"revanced_settings_about_links_dev_header", "Note",
"revanced_settings_about_links_dev_body", "This version is a pre-release and you may experience unexpected issues",
"revanced_settings_about_links_header", "Official links"
);
{
//noinspection deprecation
setTitle("About");
}
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReVancedTikTokAboutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReVancedTikTokAboutPreference(Context context) {
super(context);
}
@Override
protected String getString(String key, Object ... args) {
String format = aboutStrings.get(key);
if (format == null) {
Logger.printException(() -> "Unknown key: " + key);
return "";
}
return String.format(format, args);
}
}

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.tiktok.settings.Settings;
import app.revanced.extension.tiktok.settings.SettingsStatus;
import app.revanced.extension.tiktok.settings.preference.DownloadPathPreference;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class DownloadsPreferenceCategory extends ConditionalPreferenceCategory {
public DownloadsPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Downloads");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.downloadEnabled;
}
@Override
public void addPreferences(Context context) {
addPreference(new DownloadPathPreference(
context,
"Download path",
Settings.DOWNLOAD_PATH
));
addPreference(new TogglePreference(
context,
"Remove watermark", "",
Settings.DOWNLOAD_WATERMARK
));
}
}

View File

@@ -1,32 +0,0 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.tiktok.settings.preference.ReVancedTikTokAboutPreference;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory {
public ExtensionPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Miscellaneous");
}
@Override
public boolean getSettingsStatus() {
return true;
}
@Override
public void addPreferences(Context context) {
addPreference(new ReVancedTikTokAboutPreference(context));
addPreference(new TogglePreference(context,
"Enable debug log",
"Show extension debug log.",
BaseSettings.DEBUG
));
}
}

View File

@@ -1,55 +0,0 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.tiktok.settings.preference.RangeValuePreference;
import app.revanced.extension.tiktok.settings.Settings;
import app.revanced.extension.tiktok.settings.SettingsStatus;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory {
public FeedFilterPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Feed filter");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.feedFilterEnabled;
}
@Override
public void addPreferences(Context context) {
addPreference(new TogglePreference(
context,
"Remove feed ads", "Remove ads from feed.",
Settings.REMOVE_ADS
));
addPreference(new TogglePreference(
context,
"Hide livestreams", "Hide livestreams from feed.",
Settings.HIDE_LIVE
));
addPreference(new TogglePreference(
context,
"Hide story", "Hide story from feed.",
Settings.HIDE_STORY
));
addPreference(new TogglePreference(
context,
"Hide image video", "Hide image video from feed.",
Settings.HIDE_IMAGE
));
addPreference(new RangeValuePreference(
context,
"Min/Max views", "The minimum or maximum views of a video to show.",
Settings.MIN_MAX_VIEWS
));
addPreference(new RangeValuePreference(
context,
"Min/Max likes", "The minimum or maximum likes of a video to show.",
Settings.MIN_MAX_LIKES
));
}
}

View File

@@ -1,47 +0,0 @@
package app.revanced.extension.tiktok.settings.preference.categories;
import android.content.Context;
import android.preference.PreferenceScreen;
import app.revanced.extension.tiktok.settings.Settings;
import app.revanced.extension.tiktok.settings.SettingsStatus;
import app.revanced.extension.tiktok.settings.preference.InputTextPreference;
import app.revanced.extension.tiktok.settings.preference.TogglePreference;
@SuppressWarnings("deprecation")
public class SimSpoofPreferenceCategory extends ConditionalPreferenceCategory {
public SimSpoofPreferenceCategory(Context context, PreferenceScreen screen) {
super(context, screen);
setTitle("Bypass regional restriction");
}
@Override
public boolean getSettingsStatus() {
return SettingsStatus.simSpoofEnabled;
}
@Override
public void addPreferences(Context context) {
addPreference(new TogglePreference(
context,
"Fake sim card info",
"Bypass regional restriction by fake sim card information.",
Settings.SIM_SPOOF
));
addPreference(new InputTextPreference(
context,
"Country ISO", "us, uk, jp, ...",
Settings.SIM_SPOOF_ISO
));
addPreference(new InputTextPreference(
context,
"Operator mcc+mnc", "mcc+mnc",
Settings.SIMSPOOF_MCCMNC
));
addPreference(new InputTextPreference(
context,
"Operator name", "Name of the operator.",
Settings.SIMSPOOF_OP_NAME
));
}
}

View File

@@ -1,13 +0,0 @@
package app.revanced.extension.tiktok.speed;
import app.revanced.extension.tiktok.settings.Settings;
public class PlaybackSpeedPatch {
public static void rememberPlaybackSpeed(float newSpeed) {
Settings.REMEMBERED_SPEED.save(newSpeed);
}
public static float getPlaybackSpeed() {
return Settings.REMEMBERED_SPEED.get();
}
}

View File

@@ -1,37 +0,0 @@
package app.revanced.extension.tiktok.spoof.sim;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.tiktok.settings.Settings;
@SuppressWarnings("unused")
public class SpoofSimPatch {
private static final boolean ENABLED = Settings.SIM_SPOOF.get();
public static String getCountryIso(String value) {
if (ENABLED) {
String iso = Settings.SIM_SPOOF_ISO.get();
Logger.printDebug(() -> "Spoofing sim ISO from: " + value + " to: " + iso);
return iso;
}
return value;
}
public static String getOperator(String value) {
if (ENABLED) {
String mcc_mnc = Settings.SIMSPOOF_MCCMNC.get();
Logger.printDebug(() -> "Spoofing sim MCC-MNC from: " + value + " to: " + mcc_mnc);
return mcc_mnc;
}
return value;
}
public static String getOperatorName(String value) {
if (ENABLED) {
String operator = Settings.SIMSPOOF_OP_NAME.get();
Logger.printDebug(() -> "Spoofing sim operator from: " + value + " to: " + operator);
return operator;
}
return value;
}
}

View File

@@ -1,43 +0,0 @@
package app.revanced.extension.tudortmund.lockscreen;
import android.content.Context;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.view.Display;
import android.view.Window;
import androidx.appcompat.app.AppCompatActivity;
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
public class ShowOnLockscreenPatch {
public static Window getWindow(AppCompatActivity activity, float brightness) {
Window window = activity.getWindow();
if (brightness >= 0) {
// High brightness set, therefore show on lockscreen.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(true);
else window.addFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD);
} else {
// Ignore brightness reset when the screen is turned off.
DisplayManager displayManager = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE);
boolean isScreenOn = false;
for (Display display : displayManager.getDisplays()) {
if (display.getState() == Display.STATE_OFF) continue;
isScreenOn = true;
break;
}
if (isScreenOn) {
// Hide on lockscreen.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(false);
else window.clearFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD);
}
}
return window;
}
}

View File

@@ -1,32 +0,0 @@
package app.revanced.extension.tumblr.patches;
import com.tumblr.rumblr.model.TimelineObject;
import com.tumblr.rumblr.model.Timelineable;
import java.util.HashSet;
import java.util.List;
public final class TimelineFilterPatch {
private static final HashSet<String> blockedObjectTypes = new HashSet<>();
static {
// This dummy gets removed by the TimelineFilterPatch and in its place,
// equivalent instructions with a different constant string
// will be inserted for each Timeline object type filter.
// Modifying this line may break the patch.
blockedObjectTypes.add("BLOCKED_OBJECT_DUMMY");
}
// Calls to this method are injected where the list of Timeline objects is first received.
// We modify the list filter out elements that we want to hide.
public static void filterTimeline(final List<TimelineObject<? extends Timelineable>> timelineObjects) {
final var iterator = timelineObjects.iterator();
while (iterator.hasNext()) {
var timelineElement = iterator.next();
if (timelineElement == null) continue;
String elementType = timelineElement.getData().getTimelineObjectType().toString();
if (blockedObjectTypes.contains(elementType)) iterator.remove();
}
}
}

View File

@@ -1,14 +0,0 @@
package app.revanced.extension.twitch;
public class Utils {
/* Called from SettingsPatch smali */
public static int getStringId(String name) {
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "string");
}
/* Called from SettingsPatch smali */
public static int getDrawableId(String name) {
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "drawable");
}
}

View File

@@ -1,26 +0,0 @@
package app.revanced.extension.twitch.adblock;
import okhttp3.Request;
public interface IAdblockService {
String friendlyName();
Integer maxAttempts();
Boolean isAvailable();
Request rewriteHlsRequest(Request originalRequest);
static boolean isVod(Request request){
return request.url().pathSegments().contains("vod");
}
static String channelName(Request request) {
for (String pathSegment : request.url().pathSegments()) {
if (pathSegment.endsWith(".m3u8")) {
return pathSegment.replace(".m3u8", "");
}
}
return null;
}
}

View File

@@ -1,47 +0,0 @@
package app.revanced.extension.twitch.adblock;
import app.revanced.extension.shared.Logger;
import okhttp3.HttpUrl;
import okhttp3.Request;
import static app.revanced.extension.shared.StringRef.str;
public class LuminousService implements IAdblockService {
@Override
public String friendlyName() {
return str("revanced_proxy_luminous");
}
@Override
public Integer maxAttempts() {
return 2;
}
@Override
public Boolean isAvailable() {
return true;
}
@Override
public Request rewriteHlsRequest(Request originalRequest) {
var type = IAdblockService.isVod(originalRequest) ? "vod" : "playlist";
var url = HttpUrl.parse("https://eu.luminous.dev/" +
type +
"/" +
IAdblockService.channelName(originalRequest) +
".m3u8" +
"%3Fallow_source%3Dtrue%26allow_audio_only%3Dtrue%26fast_bread%3Dtrue"
);
if (url == null) {
Logger.printException(() -> "Failed to parse rewritten URL");
return null;
}
// Overwrite old request
return new Request.Builder()
.get()
.url(url)
.build();
}
}

View File

@@ -1,96 +0,0 @@
package app.revanced.extension.twitch.adblock;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.twitch.api.RetrofitClient;
import okhttp3.HttpUrl;
import okhttp3.Request;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static app.revanced.extension.shared.StringRef.str;
public class PurpleAdblockService implements IAdblockService {
private final Map<String, Boolean> tunnels = new HashMap<>() {{
put("https://eu1.jupter.ga", false);
put("https://eu2.jupter.ga", false);
}};
@Override
public String friendlyName() {
return str("revanced_proxy_purpleadblock");
}
@Override
public Integer maxAttempts() {
return 3;
}
@Override
public Boolean isAvailable() {
for (String tunnel : tunnels.keySet()) {
var success = true;
try {
var response = RetrofitClient.getInstance().getPurpleAdblockApi().ping(tunnel).execute();
if (!response.isSuccessful()) {
Logger.printException(() ->
"PurpleAdBlock tunnel $tunnel returned an error: HTTP code " + response.code()
);
Logger.printDebug(response::message);
try (var errorBody = response.errorBody()) {
if (errorBody != null) {
Logger.printDebug(() -> {
try {
return errorBody.string();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
success = false;
}
} catch (Exception ex) {
Logger.printException(() -> "PurpleAdBlock tunnel $tunnel is unavailable", ex);
success = false;
}
// Cache availability data
tunnels.put(tunnel, success);
if (success)
return true;
}
return false;
}
@Override
public Request rewriteHlsRequest(Request originalRequest) {
for (Map.Entry<String, Boolean> entry : tunnels.entrySet()) {
if (!entry.getValue()) continue;
var server = entry.getKey();
// Compose new URL
var url = HttpUrl.parse(server + "/channel/" + IAdblockService.channelName(originalRequest));
if (url == null) {
Logger.printException(() -> "Failed to parse rewritten URL");
return null;
}
// Overwrite old request
return new Request.Builder()
.get()
.url(url)
.build();
}
Logger.printException(() -> "No tunnels are available");
return null;
}
}

View File

@@ -1,12 +0,0 @@
package app.revanced.extension.twitch.api;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Url;
/* only used for service pings */
public interface PurpleAdblockApi {
@GET /* root */
Call<ResponseBody> ping(@Url String baseUrl);
}

View File

@@ -1,120 +0,0 @@
package app.revanced.extension.twitch.api;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.twitch.adblock.IAdblockService;
import app.revanced.extension.twitch.adblock.LuminousService;
import app.revanced.extension.twitch.adblock.PurpleAdblockService;
import app.revanced.extension.twitch.settings.Settings;
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
import static app.revanced.extension.shared.StringRef.str;
public class RequestInterceptor implements Interceptor {
private IAdblockService activeService = null;
private static final String PROXY_DISABLED = str("revanced_block_embedded_ads_entry_1");
private static final String LUMINOUS_SERVICE = str("revanced_block_embedded_ads_entry_2");
private static final String PURPLE_ADBLOCK_SERVICE = str("revanced_block_embedded_ads_entry_3");
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
var originalRequest = chain.request();
if (Settings.BLOCK_EMBEDDED_ADS.get().equals(PROXY_DISABLED)) {
return chain.proceed(originalRequest);
}
Logger.printDebug(() -> "Intercepted request to URL:" + originalRequest.url());
// Skip if not HLS manifest request
if (!originalRequest.url().host().contains("usher.ttvnw.net")) {
return chain.proceed(originalRequest);
}
final String isVod;
if (IAdblockService.isVod(originalRequest)) isVod = "yes";
else isVod = "no";
Logger.printDebug(() -> "Found HLS manifest request. Is VOD? " +
isVod +
"; Channel: " +
IAdblockService.channelName(originalRequest)
);
// None of the services support VODs currently
if (IAdblockService.isVod(originalRequest)) return chain.proceed(originalRequest);
updateActiveService();
if (activeService != null) {
var available = activeService.isAvailable();
var rewritten = activeService.rewriteHlsRequest(originalRequest);
if (!available || rewritten == null) {
Utils.showToastShort(String.format(
str("revanced_embedded_ads_service_unavailable"), activeService.friendlyName()
));
return chain.proceed(originalRequest);
}
Logger.printDebug(() -> "Rewritten HLS stream URL: " + rewritten.url());
var maxAttempts = activeService.maxAttempts();
for (var i = 1; i <= maxAttempts; i++) {
// Execute rewritten request and close body to allow multiple proceed() calls
var response = chain.proceed(rewritten);
response.close();
if (!response.isSuccessful()) {
int attempt = i;
Logger.printException(() -> "Request failed (attempt " +
attempt +
"/" + maxAttempts + "): HTTP error " +
response.code() +
" (" + response.message() + ")"
);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Logger.printException(() -> "Failed to sleep", e);
}
} else {
// Accept response from ad blocker
Logger.printDebug(() -> "Ad-blocker used");
return chain.proceed(rewritten);
}
}
// maxAttempts exceeded; giving up on using the ad blocker
Utils.showToastLong(String.format(
str("revanced_embedded_ads_service_failed"),
activeService.friendlyName())
);
}
// Adblock disabled
return chain.proceed(originalRequest);
}
private void updateActiveService() {
var current = Settings.BLOCK_EMBEDDED_ADS.get();
if (current.equals(LUMINOUS_SERVICE) && !(activeService instanceof LuminousService))
activeService = new LuminousService();
else if (current.equals(PURPLE_ADBLOCK_SERVICE) && !(activeService instanceof PurpleAdblockService))
activeService = new PurpleAdblockService();
else if (current.equals(PROXY_DISABLED))
activeService = null;
}
}

View File

@@ -1,25 +0,0 @@
package app.revanced.extension.twitch.api;
import retrofit2.Retrofit;
public class RetrofitClient {
private static RetrofitClient instance = null;
private final PurpleAdblockApi purpleAdblockApi;
private RetrofitClient() {
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost" /* dummy */).build();
purpleAdblockApi = retrofit.create(PurpleAdblockApi.class);
}
public static synchronized RetrofitClient getInstance() {
if (instance == null) {
instance = new RetrofitClient();
}
return instance;
}
public PurpleAdblockApi getPurpleAdblockApi() {
return purpleAdblockApi;
}
}

View File

@@ -1,10 +0,0 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.settings.Settings;
@SuppressWarnings("unused")
public class AudioAdsPatch {
public static boolean shouldBlockAudioAds() {
return Settings.BLOCK_AUDIO_ADS.get();
}
}

View File

@@ -1,10 +0,0 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.settings.Settings;
@SuppressWarnings("unused")
public class AutoClaimChannelPointsPatch {
public static boolean shouldAutoClaim() {
return Settings.AUTO_CLAIM_CHANNEL_POINTS.get();
}
}

View File

@@ -1,10 +0,0 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.settings.Settings;
@SuppressWarnings("unused")
public class DebugModePatch {
public static boolean isDebugModeEnabled() {
return Settings.TWITCH_DEBUG_MODE.get();
}
}

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