mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-08 18:33:57 +01:00
Compare commits
36 Commits
v5.0.1-dev
...
v2.165.0-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ce648f8a9 | ||
|
|
88b9acb137 | ||
|
|
418655311b | ||
|
|
78e77e445a | ||
|
|
491ba809bf | ||
|
|
5c0868e944 | ||
|
|
af2888fd69 | ||
|
|
61830b193c | ||
|
|
136201cea6 | ||
|
|
5fd74a1369 | ||
|
|
0bc2ee11e2 | ||
|
|
e9cb633350 | ||
|
|
a66bcb1d33 | ||
|
|
d8c9a0f95b | ||
|
|
66c3070d6e | ||
|
|
6b71456a42 | ||
|
|
ec5bb79f97 | ||
|
|
f63190ae31 | ||
|
|
263aaf1800 | ||
|
|
4c580b9fa0 | ||
|
|
618f6746cc | ||
|
|
24cb30f520 | ||
|
|
b379911b8b | ||
|
|
766172b8ff | ||
|
|
e21ce083e7 | ||
|
|
cbd733c226 | ||
|
|
a7e53adaa1 | ||
|
|
9d62835db4 | ||
|
|
59647d7cb2 | ||
|
|
aaaca326e7 | ||
|
|
f3ab06beea | ||
|
|
bc43d9c76f | ||
|
|
3bb9e3c504 | ||
|
|
5ff256509a | ||
|
|
dd16fbf0b2 | ||
|
|
e584da9cbf |
@@ -1,3 +0,0 @@
|
|||||||
[*.{kt,kts}]
|
|
||||||
ktlint_code_style = intellij_idea
|
|
||||||
ktlint_standard_no-wildcard-imports = disabled
|
|
||||||
9
.gitattributes
vendored
9
.gitattributes
vendored
@@ -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
73
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal 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
|
||||||
110
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
110
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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>
|
|
||||||
<a href="https://github.com/ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
|
||||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="http://revanced.app/discord">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://reddit.com/r/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://t.me/app_revanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://x.com/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Continuing the legacy of Vanced
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# ReVanced Patches bug report
|
|
||||||
|
|
||||||
Before creating a new bug report, please keep the following in mind:
|
|
||||||
|
|
||||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22).
|
|
||||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Bug description
|
|
||||||
description: |
|
|
||||||
- Describe your bug in detail
|
|
||||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
|
||||||
- Add images and videos if possible
|
|
||||||
- List used patches if applicable
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Error logs
|
|
||||||
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
|
|
||||||
render: shell
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Solution
|
|
||||||
description: If applicable, add a possible solution to the bug.
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Additional context
|
|
||||||
description: Add additional context here.
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Your bug report will be closed if you don't follow the checklist below.
|
|
||||||
options:
|
|
||||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
|
||||||
required: true
|
|
||||||
- label: I have chosen an appropriate title.
|
|
||||||
required: true
|
|
||||||
- label: All requested information has been provided properly.
|
|
||||||
required: true
|
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,8 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
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
|
- name: 🗨 Discussions
|
||||||
url: https://github.com/revanced/revanced-suggestions/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!
|
||||||
106
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
106
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -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>
|
|
||||||
<a href="https://github.com/ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
|
||||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="http://revanced.app/discord">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://reddit.com/r/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://t.me/app_revanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://x.com/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Continuing the legacy of Vanced
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# ReVanced Patches feature request
|
|
||||||
|
|
||||||
Before creating a new feature request, please keep the following in mind:
|
|
||||||
|
|
||||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22).
|
|
||||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md).
|
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Feature description
|
|
||||||
description: |
|
|
||||||
- Describe your feature in detail
|
|
||||||
- Add images, videos, links, examples, references, etc. if possible
|
|
||||||
- Add the target application name in case you request a new patch
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Motivation
|
|
||||||
description: |
|
|
||||||
A strong motivation is necessary for a feature request to be considered.
|
|
||||||
|
|
||||||
- Why should this feature be implemented?
|
|
||||||
- What is the explicit use case?
|
|
||||||
- What are the benefits?
|
|
||||||
- What makes this feature important?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: checkboxes
|
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
|
||||||
label: Acknowledgements
|
|
||||||
description: Your feature request will be closed if you don't follow the checklist below.
|
|
||||||
options:
|
|
||||||
- label: I have checked all open and closed feature requests and this is not a duplicate
|
|
||||||
required: true
|
|
||||||
- label: I have chosen an appropriate title.
|
|
||||||
required: true
|
|
||||||
- label: All requested information has been provided properly.
|
|
||||||
required: true
|
|
||||||
41
.github/ISSUE_TEMPLATE/patch-request.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/patch-request.yml
vendored
Normal 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
2
.github/config.yml
vendored
@@ -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.
|
|
||||||
22
.github/dependabot.yml
vendored
22
.github/dependabot.yml
vendored
@@ -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
|
|
||||||
31
.github/workflows/build_pull_request.yml
vendored
31
.github/workflows/build_pull_request.yml
vendored
@@ -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
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Open a PR to main
|
name: PR to main
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pull-request:
|
pull-request:
|
||||||
@@ -15,17 +15,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
with:
|
with:
|
||||||
destination_branch: main
|
destination_branch: 'main'
|
||||||
pr_title: 'chore: ${{ env.MESSAGE }}'
|
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||||
pr_body: |
|
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||||
This pull request will ${{ env.MESSAGE }}.
|
|
||||||
|
|
||||||
## Before merging this PR
|
|
||||||
|
|
||||||
- [ ] Pull translations from Crowdin
|
|
||||||
pr_draft: true
|
pr_draft: true
|
||||||
36
.github/workflows/pull_strings.yml
vendored
36
.github/workflows/pull_strings.yml
vendored
@@ -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 }}
|
|
||||||
28
.github/workflows/push_strings.yml
vendored
28
.github/workflows/push_strings.yml
vendored
@@ -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 }}
|
|
||||||
57
.github/workflows/release.yml
vendored
57
.github/workflows/release.yml
vendored
@@ -6,54 +6,47 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- dev
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
# Make sure the release step uses its own credentials:
|
# Make sure the release step uses its own credentials:
|
||||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
- name: Setup JDK
|
||||||
- name: Setup Java
|
uses: actions/setup-java@v3
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
java-version: '17'
|
||||||
java-version: "17"
|
distribution: 'zulu'
|
||||||
|
cache: gradle
|
||||||
- name: Cache Gradle
|
- name: Setup Node.js
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
- name: Build
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew build clean
|
run: ./gradlew generateMeta clean --no-daemon
|
||||||
|
- name: Setup semantic-release
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "lts/*"
|
|
||||||
cache: 'npm'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm install
|
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
|
- name: Release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||||
run: npm exec semantic-release
|
run: npm exec semantic-release
|
||||||
|
|||||||
18
.github/workflows/update-gradle-wrapper.yml
vendored
18
.github/workflows/update-gradle-wrapper.yml
vendored
@@ -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
5
.gitignore
vendored
@@ -122,8 +122,5 @@ gradle-app.setting
|
|||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
# Gradle properties, due to Github token
|
# gradle properties, due to Github token
|
||||||
./gradle.properties
|
./gradle.properties
|
||||||
|
|
||||||
# One package is called the same as the Gradle build folder
|
|
||||||
!**/src/**/build/
|
|
||||||
18
.releaserc
18
.releaserc
@@ -7,13 +7,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[
|
"@semantic-release/commit-analyzer",
|
||||||
"@semantic-release/commit-analyzer", {
|
|
||||||
"releaseRules": [
|
|
||||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/changelog",
|
"@semantic-release/changelog",
|
||||||
"gradle-semantic-release-plugin",
|
"gradle-semantic-release-plugin",
|
||||||
@@ -21,10 +15,11 @@
|
|||||||
"@semantic-release/git",
|
"@semantic-release/git",
|
||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
|
"README.md",
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"gradle.properties",
|
"gradle.properties",
|
||||||
],
|
"patches.json"
|
||||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -32,8 +27,11 @@
|
|||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
"path": "build/libs/*.jar"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "patches.json"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
successComment: false
|
successComment: false
|
||||||
}
|
}
|
||||||
|
|||||||
5739
CHANGELOG.md
5739
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
128
CONTRIBUTING.md
128
CONTRIBUTING.md
@@ -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>
|
|
||||||
<a href="https://github.com/ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
|
||||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="http://revanced.app/discord">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://reddit.com/r/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://t.me/app_revanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://x.com/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Continuing the legacy of Vanced
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# 👋 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
122
README-template.md
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
533
README.md
533
README.md
@@ -1,105 +1,464 @@
|
|||||||
<p align="center">
|
## 🧩 Patches
|
||||||
<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>
|
|
||||||
<a href="https://github.com/ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
|
||||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="http://revanced.app/discord">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://reddit.com/r/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://t.me/app_revanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://x.com/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Continuing the legacy of Vanced
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# 🧩 ReVanced Patches
|
The official Patch bundle provided by ReVanced and the community.
|
||||||
|
|
||||||

|
> 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.05.40 |
|
||||||
|
| `client-spoof` | Spoofs the YouTube or Vanced client to prevent playback issues. | all |
|
||||||
|
| `comments` | Hides components related to comments. | 18.05.40 |
|
||||||
|
| `copy-video-url` | Adds buttons in player to copy video links. | 18.05.40 |
|
||||||
|
| `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.05.40 |
|
||||||
|
| `custom-video-speed` | Adds more video speed options. | 18.05.40 |
|
||||||
|
| `debugging` | Adds debugging options. | all |
|
||||||
|
| `disable-auto-captions` | Disable forced captions from being automatically enabled. | 18.05.40 |
|
||||||
|
| `disable-fullscreen-panels` | Disables video description and comments panel in fullscreen view. | 18.05.40 |
|
||||||
|
| `disable-player-popup-panels` | Disables panels from appearing automatically when going into fullscreen (playlist or live chat). | 18.05.40 |
|
||||||
|
| `disable-shorts-on-startup` | Disables playing YouTube Shorts when launching YouTube. | 18.05.40 |
|
||||||
|
| `disable-zoom-haptics` | Disables haptics when zooming. | all |
|
||||||
|
| `downloads` | Enables downloading music and videos from YouTube. | 18.05.40 |
|
||||||
|
| `enable-wide-searchbar` | Replaces the search icon with a wide search bar. This will hide the YouTube logo when active. | 18.05.40 |
|
||||||
|
| `general-ads` | Removes general ads. | 18.05.40 |
|
||||||
|
| `hdr-auto-brightness` | Makes the brightness of HDR videos follow the system default. | 18.05.40 |
|
||||||
|
| `hide-album-cards` | Hides the album cards below the artist description. | 18.05.40 |
|
||||||
|
| `hide-artist-card` | Hides the artist card below the searchbar. | 18.05.40 |
|
||||||
|
| `hide-autoplay-button` | Hides the autoplay button in the video player. | 18.05.40 |
|
||||||
|
| `hide-breaking-news-shelf` | Hides the breaking news shelf on the homepage tab. | 18.05.40 |
|
||||||
|
| `hide-captions-button` | Hides the captions button on video player. | 18.05.40 |
|
||||||
|
| `hide-cast-button` | Hides the cast button in the video player. | all |
|
||||||
|
| `hide-create-button` | Hides the create button in the navigation bar. | 18.05.40 |
|
||||||
|
| `hide-crowdfunding-box` | Hides the crowdfunding box between the player and video description. | 18.05.40 |
|
||||||
|
| `hide-email-address` | Hides the email address in the account switcher. | 18.05.40 |
|
||||||
|
| `hide-endscreen-cards` | Hides the suggested video cards at the end of a video in fullscreen. | 18.05.40 |
|
||||||
|
| `hide-floating-microphone-button` | Hides the floating microphone button which appears in search. | 18.05.40 |
|
||||||
|
| `hide-info-cards` | Hides info cards in videos. | 18.05.40 |
|
||||||
|
| `hide-my-mix` | Hides mix playlists. | 18.05.40 |
|
||||||
|
| `hide-player-buttons` | Adds the option to hide video player previous and next buttons. | all |
|
||||||
|
| `hide-seekbar` | Hides the seekbar. | 18.05.40 |
|
||||||
|
| `hide-shorts-button` | Hides the shorts button on the navigation bar. | 18.05.40 |
|
||||||
|
| `hide-timestamp` | Hides timestamp in video player. | 18.05.40 |
|
||||||
|
| `hide-video-action-buttons` | Adds the options to hide action buttons under a video. | 18.05.40 |
|
||||||
|
| `hide-watch-in-vr` | Hides the option to watch in VR from the player settings flyout panel. | 18.05.40 |
|
||||||
|
| `hide-watermark` | Hides creator's watermarks on videos. | 18.05.40 |
|
||||||
|
| `microg-support` | Allows YouTube ReVanced to run without root and under a different package name with Vanced MicroG. | 18.05.40 |
|
||||||
|
| `minimized-playback` | Enables minimized and background playback. | 18.05.40 |
|
||||||
|
| `old-quality-layout` | Enables the original video quality flyout in the video player settings | 18.05.40 |
|
||||||
|
| `open-links-externally` | Open links outside of the app directly in your browser. | 18.05.40 |
|
||||||
|
| `premium-heading` | Shows premium branding on the home screen. | all |
|
||||||
|
| `remember-playback-rate` | Adds the ability to remember the playback rate you chose in the video playback rate flyout. | 18.05.40 |
|
||||||
|
| `remember-video-quality` | Adds the ability to remember the video quality you chose in the video quality flyout. | 18.05.40 |
|
||||||
|
| `remove-player-button-background` | Removes the background from the video player buttons. | 18.05.40 |
|
||||||
|
| `return-youtube-dislike` | Shows the dislike count of videos using the Return YouTube Dislike API. | 18.05.40 |
|
||||||
|
| `seekbar-tapping` | Enables tap-to-seek on the seekbar of the video player. | 18.05.40 |
|
||||||
|
| `sponsorblock` | Integrates SponsorBlock which allows skipping video segments such as sponsored content. | 18.05.40 |
|
||||||
|
| `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.05.40 |
|
||||||
|
| `spoof-signature-verification` | Spoofs the client to prevent playback issues. | 18.05.40 |
|
||||||
|
| `swipe-controls` | Adds volume and brightness swipe controls. | 18.05.40 |
|
||||||
|
| `tablet-mini-player` | Enables the tablet mini player layout. | 18.05.40 |
|
||||||
|
| `theme` | Applies a custom theme. | all |
|
||||||
|
| `video-ads` | Removes ads in the video player. | 18.05.40 |
|
||||||
|
</details>
|
||||||
|
|
||||||
Patches are small modifications to Android apps that allow you to change the behavior of or add new features,
|
### [📦 `com.google.android.apps.youtube.music`](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music)
|
||||||
block ads, customize the appearance, and much more.
|
<details>
|
||||||
|
|
||||||
## 💪 Features
|
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
||||||
|
|:--------:|:--------------:|:-----------------:|
|
||||||
|
| `background-play` | Enables playing music in the background. | all |
|
||||||
|
| `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
|
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
||||||
* ⭐ **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
|
| `disable-login-requirement` | Do not force login. | all |
|
||||||
* ⚙️ **Miscellaneous and general purpose**: Rename packages, enable debugging, disable screen capture restrictions,
|
| `downloads` | Removes download restrictions and changes the default path to download to. | 27.8.3 |
|
||||||
export activities, etc.
|
| `feed-filter` | Filters tiktok videos: removing ads, removing livestreams. | 27.8.3 |
|
||||||
* ✨ **And much more!**
|
| `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 |
|
||||||
|
| `monochrome-icon` | Adds a monochrome icon. | all |
|
||||||
|
</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.
|
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
||||||
[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,
|
| `general-reddit-ads` | Removes general ads from the Reddit frontpage and subreddits. | 2023.09.1 |
|
||||||
along with build & install instructions.
|
| `hide-subreddit-banner` | Hides banner ads from comments on subreddits. | 2023.09.1 |
|
||||||
|
| `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 the YouTube or Vanced client to prevent playback issues. | 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.5.2 |
|
||||||
|
| `spoof-signature` | Spoofs the signature of the app. | 2.5.2 |
|
||||||
|
</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.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. | all |
|
||||||
|
</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>
|
||||||
|
|
||||||
|
### [📦 `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>
|
||||||
|
|
||||||
|
### [📦 `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>
|
||||||
|
|
||||||
|
### [📦 `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>
|
||||||
|
|
||||||
|
### [📦 `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.ticktick.task`](https://play.google.com/store/apps/details?id=com.ticktick.task)
|
||||||
|
<details>
|
||||||
|
|
||||||
|
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
||||||
|
|:--------:|:--------------:|:-----------------:|
|
||||||
|
| `unlock-themes` | Unlocks all themes. | 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 |
@@ -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
66
build.gradle.kts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.7.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
extension {
|
|
||||||
name = "extensions/all/screencapture/remove-screen-capture-restriction.rve"
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
extension {
|
|
||||||
name = "extensions/all/screenshot/remove-screenshot-restriction.rve"
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
9
extensions/shared/proguard-rules.pro
vendored
9
extensions/shared/proguard-rules.pro
vendored
@@ -1,9 +0,0 @@
|
|||||||
-dontobfuscate
|
|
||||||
-dontoptimize
|
|
||||||
-keepattributes *
|
|
||||||
-keep class app.revanced.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class com.google.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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> ").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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +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.
|
|
||||||
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 = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.*;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
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 static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(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();
|
|
||||||
|
|
||||||
preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onCreate() failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.sf;
|
|
||||||
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.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 the links from {@link SocialLinksRoutes}.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
|
||||||
public class ReVancedAboutPreference extends Preference {
|
|
||||||
|
|
||||||
private static String useNonBreakingHyphens(String text) {
|
|
||||||
// Replace any dashes with non breaking dashes, so the English text 'pre-release'
|
|
||||||
// and the dev release number does not break and cover two lines.
|
|
||||||
return text.replace("-", "‑"); // #8209 = non breaking hyphen.
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createDialogHtml(WebLink[] socialLinks) {
|
|
||||||
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=\"https://revanced.app/favicon.ico\" />");
|
|
||||||
}
|
|
||||||
|
|
||||||
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(str("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(str("revanced_settings_about_links_dev_header")))
|
|
||||||
.append("</h3>");
|
|
||||||
|
|
||||||
builder.append("<p>")
|
|
||||||
.append(str("revanced_settings_about_links_dev_body"))
|
|
||||||
.append("</p>");
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.append("<h2 style=\"margin-top: 30px;\">")
|
|
||||||
.append(str("revanced_settings_about_links_header"))
|
|
||||||
.append("</h2>");
|
|
||||||
|
|
||||||
builder.append("<div>");
|
|
||||||
for (WebLink social : socialLinks) {
|
|
||||||
builder.append("<div style=\"margin-bottom: 20px;\">");
|
|
||||||
builder.append(String.format("<a href=\"%s\">%s</a>", social.url, social.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 (!SocialLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) {
|
|
||||||
ProgressDialog progress = new ProgressDialog(getContext());
|
|
||||||
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
|
||||||
progress.show();
|
|
||||||
Utils.runOnBackgroundThread(() -> fetchLinksAndShowDialog(progress));
|
|
||||||
} else {
|
|
||||||
// No network call required and can run now.
|
|
||||||
fetchLinksAndShowDialog(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchLinksAndShowDialog(@Nullable ProgressDialog progress) {
|
|
||||||
WebLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks();
|
|
||||||
String htmlDialog = createDialogHtml(socialLinks);
|
|
||||||
|
|
||||||
Utils.runOnMainThreadNowOrLater(() -> {
|
|
||||||
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;
|
|
||||||
final 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 "ReVancedSocialLink{" +
|
|
||||||
"preferred=" + preferred +
|
|
||||||
", name='" + name + '\'' +
|
|
||||||
", url='" + url + '\'' +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SocialLinksRoutes {
|
|
||||||
/**
|
|
||||||
* Simple link to the website donate page,
|
|
||||||
* rather than fetching and parsing the donation links using the API.
|
|
||||||
*/
|
|
||||||
public static final WebLink DONATE_LINK = new WebLink(true,
|
|
||||||
sf("revanced_settings_about_links_donate").toString(),
|
|
||||||
"https://revanced.app/donate");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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"),
|
|
||||||
DONATE_LINK,
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v2";
|
|
||||||
private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/socials").compile();
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static volatile WebLink[] fetchedLinks;
|
|
||||||
|
|
||||||
static boolean hasFetchedLinks() {
|
|
||||||
return fetchedLinks != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static WebLink[] fetchSocialLinks() {
|
|
||||||
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);
|
|
||||||
JSONArray socials = json.getJSONArray("socials");
|
|
||||||
|
|
||||||
List<WebLink> links = new ArrayList<>();
|
|
||||||
|
|
||||||
links.add(DONATE_LINK); // Show donate link first.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +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.TogglePreference;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory {
|
|
||||||
public ExtensionPreferenceCategory(Context context, PreferenceScreen screen) {
|
|
||||||
super(context, screen);
|
|
||||||
setTitle("Extension");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean getSettingsStatus() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addPreferences(Context context) {
|
|
||||||
addPreference(new TogglePreference(context,
|
|
||||||
"Enable debug log",
|
|
||||||
"Show extension debug log.",
|
|
||||||
BaseSettings.DEBUG
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +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 {
|
|
||||||
/**
|
|
||||||
* @noinspection deprecation
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package app.revanced.extension.twitch.patches;
|
|
||||||
|
|
||||||
import app.revanced.extension.twitch.api.RequestInterceptor;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class EmbeddedAdsPatch {
|
|
||||||
public static RequestInterceptor createRequestInterceptor() {
|
|
||||||
return new RequestInterceptor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user