mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-12 20:33:55 +01:00
Compare commits
5 Commits
v5.26.0-de
...
v2.201.0-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45f12d63ee | ||
|
|
5d3f62fc67 | ||
|
|
273fe59f0c | ||
|
|
1041238632 | ||
|
|
e88d6a8bba |
@@ -1,3 +0,0 @@
|
|||||||
[*.{kt,kts}]
|
|
||||||
ktlint_code_style = intellij_idea
|
|
||||||
ktlint_standard_no-wildcard-imports = disabled
|
|
||||||
@@ -70,7 +70,7 @@ body:
|
|||||||
|
|
||||||
Before creating a new bug report, please keep the following in mind:
|
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).
|
- **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-patches/labels/Bug%20report).
|
||||||
- **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).
|
- **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).
|
- **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
|
- type: textarea
|
||||||
@@ -102,7 +102,7 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your bug report will be closed if you don't follow the checklist below.
|
description: Your bug report will be closed if you don't follow the checklist below.
|
||||||
options:
|
options:
|
||||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
- label: This issue is not a duplicate of an existing bug report.
|
||||||
required: true
|
required: true
|
||||||
- label: I have chosen an appropriate title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
@@ -70,8 +70,8 @@ body:
|
|||||||
|
|
||||||
Before creating a new feature request, please keep the following in mind:
|
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).
|
- **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-patches/labels/Feature%20request).
|
||||||
- **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).
|
- **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).
|
- **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
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -98,7 +98,7 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your feature request will be closed if you don't follow the checklist below.
|
description: Your feature request will be closed if you don't follow the checklist below.
|
||||||
options:
|
options:
|
||||||
- label: I have checked all open and closed feature requests and this is not a duplicate
|
- label: This issue is not a duplicate of an existing feature request.
|
||||||
required: true
|
required: true
|
||||||
- label: I have chosen an appropriate title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
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
|
|
||||||
37
.github/workflows/build_pull_request.yml
vendored
37
.github/workflows/build_pull_request.yml
vendored
@@ -1,37 +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@v3
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: ./gradlew :patches:buildAndroid --no-daemon
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: revanced-patches
|
|
||||||
path: patches/build/libs
|
|
||||||
@@ -20,12 +20,12 @@ jobs:
|
|||||||
- 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
|
## Dependencies before merge
|
||||||
|
|
||||||
- [ ] Pull translations from Crowdin
|
- [ ] https://github.com/revanced/revanced-integrations
|
||||||
pr_draft: true
|
pr_draft: true
|
||||||
44
.github/workflows/pull_strings.yml
vendored
44
.github/workflows/pull_strings.yml
vendored
@@ -1,44 +0,0 @@
|
|||||||
name: Pull strings
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 */12 * * *"
|
|
||||||
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:
|
|
||||||
ref: dev
|
|
||||||
fetch-depth: 0
|
|
||||||
clean: true
|
|
||||||
|
|
||||||
- name: Pull strings
|
|
||||||
uses: crowdin/github-action@v2
|
|
||||||
with:
|
|
||||||
config: crowdin.yml
|
|
||||||
upload_sources: false
|
|
||||||
download_translations: true
|
|
||||||
skip_ref_checkout: true
|
|
||||||
localization_branch_name: feat/translations
|
|
||||||
create_pull_request: false
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
||||||
|
|
||||||
- name: Open pull request
|
|
||||||
if: github.event_name == 'workflow_dispatch'
|
|
||||||
uses: repo-sync/pull-request@v2
|
|
||||||
with:
|
|
||||||
source_branch: feat/translations
|
|
||||||
destination_branch: dev
|
|
||||||
pr_title: "chore: Sync translations"
|
|
||||||
pr_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
|
||||||
33
.github/workflows/push_strings.yml
vendored
33
.github/workflows/push_strings.yml
vendored
@@ -1,33 +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: Preprocess strings
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: ./gradlew clean preprocessCrowdinStrings
|
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
54
.github/workflows/release.yml
vendored
54
.github/workflows/release.yml
vendored
@@ -6,61 +6,43 @@ 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
|
|
||||||
id-token: write
|
|
||||||
attestations: write
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
# Make sure the release step uses its own credentials:
|
||||||
|
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||||
|
persist-credentials: false
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Cache Node modules
|
||||||
uses: actions/setup-java@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
path: |
|
||||||
java-version: '17'
|
node_modules
|
||||||
|
key: npm-${{ hashFiles('package-lock.json') }}
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v3
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
- name: Build
|
- name: Build with Gradle
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew :patches:buildAndroid clean
|
run: ./gradlew generateMeta clean
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup semantic-release
|
||||||
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
|
||||||
uses: cycjimmy/semantic-release-action@v4
|
|
||||||
id: release
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||||
|
run: npm exec semantic-release
|
||||||
- name: Attest
|
|
||||||
if: steps.release.outputs.new_release_published == 'true'
|
|
||||||
uses: actions/attest-build-provenance@v2
|
|
||||||
with:
|
|
||||||
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
|
||||||
subject-path: patches/build/libs/patches-*.rvp
|
|
||||||
|
|||||||
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/
|
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -4,5 +4,5 @@
|
|||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="azul-17" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="azul-17" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
||||||
18
.releaserc
18
.releaserc
@@ -21,10 +21,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,17 +33,20 @@
|
|||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
"path": "build/libs/*.jar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "patches.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"successComment": false
|
successComment: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@saithodev/semantic-release-backmerge",
|
"@saithodev/semantic-release-backmerge",
|
||||||
{
|
{
|
||||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
backmergeBranches: [{"from": "main", "to": "dev"}],
|
||||||
"clearWorkspace": true
|
clearWorkspace: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
4858
CHANGELOG.md
4858
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -64,15 +64,15 @@ This document describes how to contribute to ReVanced Patches.
|
|||||||
|
|
||||||
## 📖 Resources to help you get started
|
## 📖 Resources to help you get started
|
||||||
|
|
||||||
* The [documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs) contains the fundamentals
|
* The [documentation](https://github.com/ReVanced/revanced-patches/tree/docs/docs) provides the fundamentals of patches
|
||||||
of ReVanced Patcher and how to use ReVanced Patcher to create patches
|
and everything necessary to create your own patch from scratch
|
||||||
* [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
* [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
|
* [Issues](https://github.com/ReVanced/revanced-patches/issues) are where we keep track of bugs and feature requests
|
||||||
|
|
||||||
## 🙏 Submitting a feature request
|
## 🙏 Submitting a feature request
|
||||||
|
|
||||||
Features can be requested by opening an issue using the
|
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+).
|
[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**
|
> **Note**
|
||||||
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patches.
|
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patches.
|
||||||
@@ -81,11 +81,7 @@ Features can be requested by opening an issue using the
|
|||||||
## 🐞 Submitting a bug report
|
## 🐞 Submitting a bug report
|
||||||
|
|
||||||
If you encounter a bug while using ReVanced Patches, open an issue using the
|
If you encounter a bug while using ReVanced Patches, open an issue using the
|
||||||
[Bug report issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
|
[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
|
## 🧑⚖️ Guidelines for requesting or contributing patches
|
||||||
|
|
||||||
@@ -111,6 +107,7 @@ are unaffected by this change.
|
|||||||
* Payment circumvention: We do not accept patches that exist solely to bypass payment for the app or any of its features
|
* 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
|
* Malicious patches: Patches that are malicious in nature are not allowed
|
||||||
|
|
||||||
|
|
||||||
## 📝 How to contribute
|
## 📝 How to contribute
|
||||||
|
|
||||||
1. Before contributing, it is recommended to open an issue to discuss your change
|
1. Before contributing, it is recommended to open an issue to discuss your change
|
||||||
@@ -118,7 +115,7 @@ with the maintainers of ReVanced Patches. This will help you determine whether y
|
|||||||
and whether it is worth your time to implement it
|
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`
|
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
|
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)
|
described in the [documentation](https://github.com/ReVanced/revanced-patches/tree/docs/docs)
|
||||||
4. Submit a pull request to the `dev` branch of the repository and reference issues
|
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
|
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,
|
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -67,7 +67,7 @@ This repository contains a collection of ReVanced Patches.
|
|||||||
|
|
||||||
## ❓ About
|
## ❓ About
|
||||||
|
|
||||||
Patches are small modifications to Android apps that allow you to change the behavior of or add new features,
|
Patches are small modifications to Android apps that allow you to change the behaviour of or add new features,
|
||||||
block ads, customize the appearance, and much more.
|
block ads, customize the appearance, and much more.
|
||||||
|
|
||||||
## 💪 Features
|
## 💪 Features
|
||||||
@@ -81,7 +81,7 @@ Some of the features the patches provide are:
|
|||||||
export activities, etc.
|
export activities, etc.
|
||||||
* ✨ **And much more!**
|
* ✨ **And much more!**
|
||||||
|
|
||||||
For a complete list of all available patches, visit [revanced.app/patches](https://revanced.app/patches).
|
For a full list of all available patches, visit [revanced.app/patches](https://revanced.app/patches).
|
||||||
|
|
||||||
## 🚀 How to get started
|
## 🚀 How to get started
|
||||||
|
|
||||||
@@ -93,13 +93,17 @@ You can use [ReVanced CLI](https://github.com/ReVanced/revanced-cli) or [ReVance
|
|||||||
|
|
||||||
Thank you for considering contributing to ReVanced Patches. You can find the contribution guidelines [here](CONTRIBUTING.md).
|
Thank you for considering contributing to ReVanced Patches. You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
### 📃 Documentation
|
||||||
|
|
||||||
|
The documentation provides the fundamentals of patches and everything necessary to create your own patch from scratch.
|
||||||
|
You can find it [here](https://github.com/ReVanced/revanced-patches/tree/docs/docs).
|
||||||
|
|
||||||
### 🛠️ Building
|
### 🛠️ Building
|
||||||
|
|
||||||
To build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
In order to build ReVanced Patches, you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
||||||
|
|
||||||
## 📜 Licence
|
## 📜 Licence
|
||||||
|
|
||||||
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
|
ReVanced Patches is licensed under the GPLv3 licence. Please see the [licence file](LICENSE) for more information.
|
||||||
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
|
[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,
|
Any modifications to ReVanced Patches must also be made available under the GPL along with build & install instructions.
|
||||||
along with build & install instructions.
|
|
||||||
|
|||||||
113
build.gradle.kts
Normal file
113
build.gradle.kts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import org.gradle.kotlin.dsl.support.listFilesOrdered
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.9.10"
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
google()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
// Required for FlexVer-Java
|
||||||
|
maven {
|
||||||
|
url = uri("https://repo.sleeping.town")
|
||||||
|
content {
|
||||||
|
includeGroup("com.unascribed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.revanced.patcher)
|
||||||
|
implementation(libs.smali)
|
||||||
|
// TODO: Required because build fails without it. Find a way to remove this dependency.
|
||||||
|
implementation(libs.guava)
|
||||||
|
// Used in JsonGenerator.
|
||||||
|
implementation(libs.gson)
|
||||||
|
|
||||||
|
// A dependency to the Android library unfortunately fails the build, which is why this is required.
|
||||||
|
compileOnly(project("dummy"))
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
register<DefaultTask>("generateBundle") {
|
||||||
|
description = "Generate dex files from build and bundle them in the jar file"
|
||||||
|
|
||||||
|
dependsOn(build)
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val d8 = File(System.getenv("ANDROID_HOME")).resolve("build-tools")
|
||||||
|
.listFilesOrdered().last().resolve("d8").absolutePath
|
||||||
|
|
||||||
|
val artifacts = configurations.archives.get().allArtifacts.files.files.first().absolutePath
|
||||||
|
val workingDirectory = layout.buildDirectory.dir("libs").get().asFile
|
||||||
|
|
||||||
|
exec {
|
||||||
|
workingDir = workingDirectory
|
||||||
|
commandLine = listOf(d8, artifacts)
|
||||||
|
}
|
||||||
|
|
||||||
|
exec {
|
||||||
|
workingDir = workingDirectory
|
||||||
|
commandLine = listOf("zip", "-u", artifacts, "classes.dex")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register<JavaExec>("generateMeta") {
|
||||||
|
description = "Generate metadata for this bundle"
|
||||||
|
|
||||||
|
dependsOn(build)
|
||||||
|
|
||||||
|
classpath = sourceSets["main"].runtimeClasspath
|
||||||
|
mainClass.set("app.revanced.meta.PatchesFileGenerator")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required to run tasks because Gradle semantic-release plugin runs the publish task.
|
||||||
|
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
|
||||||
|
named("publish") {
|
||||||
|
dependsOn("generateBundle")
|
||||||
|
dependsOn("generateMeta")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("revanced-patches-publication") {
|
||||||
|
from(components["java"])
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name = "ReVanced Patches"
|
||||||
|
description = "Patches for ReVanced."
|
||||||
|
url = "https://revanced.app"
|
||||||
|
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = "GNU General Public License v3.0"
|
||||||
|
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = "ReVanced"
|
||||||
|
name = "ReVanced"
|
||||||
|
email = "contact@revanced.app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
connection = "scm:git:git://github.com/revanced/revanced-patches.git"
|
||||||
|
developerConnection = "scm:git:git@github.com:revanced/revanced-patches.git"
|
||||||
|
url = "https://github.com/revanced/revanced-patches"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
9
dummy/build.gradle.kts
Normal file
9
dummy/build.gradle.kts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(11))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
dummy/src/main/java/android/os/Environment.java
Normal file
9
dummy/src/main/java/android/os/Environment.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package android.os;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public final class Environment {
|
||||||
|
public static File getExternalStorageDirectory() {
|
||||||
|
throw new UnsupportedOperationException("Stub");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package app.revanced.extension.all.misc.hide.adb;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.provider.Settings;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class HideAdbPatch {
|
|
||||||
private static final List<String> SPOOF_SETTINGS = Arrays.asList("adb_enabled", "adb_wifi_enabled", "development_settings_enabled");
|
|
||||||
|
|
||||||
public static int getInt(ContentResolver cr, String name) throws Settings.SettingNotFoundException {
|
|
||||||
if (SPOOF_SETTINGS.contains(name)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Settings.Global.getInt(cr, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getInt(ContentResolver cr, String name, int def) {
|
|
||||||
if (SPOOF_SETTINGS.contains(name)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Settings.Global.getInt(cr, name, def);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
|
||||||
</manifest>
|
|
||||||
@@ -1,424 +0,0 @@
|
|||||||
package app.revanced.extension.all.misc.connectivity.wifi.spoof;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.net.Network;
|
|
||||||
import android.net.NetworkCapabilities;
|
|
||||||
import android.net.NetworkInfo;
|
|
||||||
import android.net.NetworkRequest;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Handler;
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "unused"})
|
|
||||||
public class SpoofWifiPatch {
|
|
||||||
|
|
||||||
// Used to check what the (real or fake) active network is (take a look at `hasTransport`).
|
|
||||||
private static ConnectivityManager CONNECTIVITY_MANAGER;
|
|
||||||
|
|
||||||
// If Wifi is not enabled, these are types that would pretend to be Wifi for android.net.Network (lower index = higher priority).
|
|
||||||
// This does not apply to android.net.NetworkInfo, because we can pretend that Wifi is always active there.
|
|
||||||
//
|
|
||||||
// VPN should be a fallback, because Reverse Tethering uses VPN.
|
|
||||||
private static final int[] FAKE_FALLBACK_NETWORKS = { NetworkCapabilities.TRANSPORT_ETHERNET, NetworkCapabilities.TRANSPORT_VPN };
|
|
||||||
|
|
||||||
// In order to initialize our own ConnectivityManager, if it isn't initialized yet.
|
|
||||||
public static Object getSystemService(Context context, String name) {
|
|
||||||
Object result = context.getSystemService(name);
|
|
||||||
if (CONNECTIVITY_MANAGER == null) {
|
|
||||||
if (Context.CONNECTIVITY_SERVICE.equals(name)) {
|
|
||||||
CONNECTIVITY_MANAGER = (ConnectivityManager) result;
|
|
||||||
} else {
|
|
||||||
CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to initialize our own ConnectivityManager, if it isn't initialized yet.
|
|
||||||
public static Object getSystemService(Context context, Class<?> serviceClass) {
|
|
||||||
Object result = context.getSystemService(serviceClass);
|
|
||||||
if (CONNECTIVITY_MANAGER == null) {
|
|
||||||
if (serviceClass == ConnectivityManager.class) {
|
|
||||||
CONNECTIVITY_MANAGER = (ConnectivityManager) result;
|
|
||||||
} else {
|
|
||||||
CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simply always return Wifi as active network.
|
|
||||||
public static NetworkInfo getActiveNetworkInfo(ConnectivityManager connectivityManager) {
|
|
||||||
for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) {
|
|
||||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
|
||||||
return networkInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return connectivityManager.getActiveNetworkInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretend Wifi is always connected.
|
|
||||||
public static boolean isConnected(NetworkInfo networkInfo) {
|
|
||||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return networkInfo.isConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretend Wifi is always connected.
|
|
||||||
public static boolean isConnectedOrConnecting(NetworkInfo networkInfo) {
|
|
||||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return networkInfo.isConnectedOrConnecting();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretend Wifi is always available.
|
|
||||||
public static boolean isAvailable(NetworkInfo networkInfo) {
|
|
||||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return networkInfo.isAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretend Wifi is always connected.
|
|
||||||
public static NetworkInfo.State getState(NetworkInfo networkInfo) {
|
|
||||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
|
||||||
return NetworkInfo.State.CONNECTED;
|
|
||||||
}
|
|
||||||
return networkInfo.getState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretend Wifi is always connected.
|
|
||||||
public static NetworkInfo.DetailedState getDetailedState(NetworkInfo networkInfo) {
|
|
||||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
|
||||||
return NetworkInfo.DetailedState.CONNECTED;
|
|
||||||
}
|
|
||||||
return networkInfo.getDetailedState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pretend Wifi is enabled, so connection isn't metered.
|
|
||||||
public static boolean isActiveNetworkMetered(ConnectivityManager connectivityManager) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the Wifi network, if Wifi is enabled.
|
|
||||||
// Otherwise if one of our fallbacks has a connection, return them.
|
|
||||||
// And as a last resort, return the default active network.
|
|
||||||
public static Network getActiveNetwork(ConnectivityManager connectivityManager) {
|
|
||||||
Network[] prioritizedNetworks = new Network[FAKE_FALLBACK_NETWORKS.length];
|
|
||||||
for (Network network : connectivityManager.getAllNetworks()) {
|
|
||||||
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
|
|
||||||
if (networkCapabilities == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
|
||||||
return network;
|
|
||||||
}
|
|
||||||
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
|
|
||||||
for (int i = 0; i < FAKE_FALLBACK_NETWORKS.length; i++) {
|
|
||||||
int transportType = FAKE_FALLBACK_NETWORKS[i];
|
|
||||||
if (networkCapabilities.hasTransport(transportType)) {
|
|
||||||
prioritizedNetworks[i] = network;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (Network network : prioritizedNetworks) {
|
|
||||||
if (network != null) {
|
|
||||||
return network;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return connectivityManager.getActiveNetwork();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the given network is a real or fake Wifi connection, return a Wifi network.
|
|
||||||
// Otherwise fallback to default implementation.
|
|
||||||
public static NetworkInfo getNetworkInfo(ConnectivityManager connectivityManager, Network network) {
|
|
||||||
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
|
|
||||||
if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) {
|
|
||||||
for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) {
|
|
||||||
if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
|
|
||||||
return networkInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return connectivityManager.getNetworkInfo(network);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are checking if the NetworkCapabilities use Wifi, return yes if
|
|
||||||
// - it is a real Wifi connection,
|
|
||||||
// - or the NetworkCapabilities are from a network pretending being a Wifi network.
|
|
||||||
// Otherwise fallback to default implementation.
|
|
||||||
public static boolean hasTransport(NetworkCapabilities networkCapabilities, int transportType) {
|
|
||||||
if (transportType == NetworkCapabilities.TRANSPORT_WIFI) {
|
|
||||||
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (CONNECTIVITY_MANAGER != null) {
|
|
||||||
Network activeNetwork = getActiveNetwork(CONNECTIVITY_MANAGER);
|
|
||||||
NetworkCapabilities activeNetworkCapabilities = CONNECTIVITY_MANAGER.getNetworkCapabilities(activeNetwork);
|
|
||||||
if (activeNetworkCapabilities != null) {
|
|
||||||
for (int fallbackTransportType : FAKE_FALLBACK_NETWORKS) {
|
|
||||||
if (activeNetworkCapabilities.hasTransport(fallbackTransportType) && networkCapabilities.hasTransport(fallbackTransportType)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return networkCapabilities.hasTransport(transportType);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the given network is a real or fake Wifi connection, pretend it has a connection (and some other things).
|
|
||||||
public static boolean hasCapability(NetworkCapabilities networkCapabilities, int capability) {
|
|
||||||
if (hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI) && (
|
|
||||||
capability == NetworkCapabilities.NET_CAPABILITY_INTERNET
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_FOREGROUND
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_METERED
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_NOT_VPN
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_TRUSTED
|
|
||||||
|| capability == NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return networkCapabilities.hasCapability(capability);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.S)
|
|
||||||
public static void registerBestMatchingNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(handler),
|
|
||||||
() -> connectivityManager.registerBestMatchingNetworkCallback(request, networkCallback, handler)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
|
||||||
public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
() -> connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(handler),
|
|
||||||
() -> connectivityManager.registerDefaultNetworkCallback(networkCallback, handler)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
() -> connectivityManager.registerNetworkCallback(request, networkCallback)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately.
|
|
||||||
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(operation),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
() -> connectivityManager.registerNetworkCallback(request, operation)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(handler),
|
|
||||||
() -> connectivityManager.registerNetworkCallback(request, networkCallback, handler)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
() -> connectivityManager.requestNetwork(request, networkCallback)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, int timeoutMs) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
() -> connectivityManager.requestNetwork(request, networkCallback, timeoutMs)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(handler),
|
|
||||||
() -> connectivityManager.requestNetwork(request, networkCallback, handler)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately.
|
|
||||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(operation),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
() -> connectivityManager.requestNetwork(request, operation)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
|
||||||
public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler, int timeoutMs) {
|
|
||||||
Utils.networkCallback(
|
|
||||||
connectivityManager,
|
|
||||||
Utils.Option.of(request),
|
|
||||||
Utils.Option.of(networkCallback),
|
|
||||||
Utils.Option.empty(),
|
|
||||||
Utils.Option.of(handler),
|
|
||||||
() -> connectivityManager.requestNetwork(request, networkCallback, handler, timeoutMs)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) {
|
|
||||||
try {
|
|
||||||
connectivityManager.unregisterNetworkCallback(networkCallback);
|
|
||||||
} catch (IllegalArgumentException ignore) {
|
|
||||||
// ignore: NetworkCallback was not registered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, PendingIntent operation) {
|
|
||||||
try {
|
|
||||||
connectivityManager.unregisterNetworkCallback(operation);
|
|
||||||
} catch (IllegalArgumentException ignore) {
|
|
||||||
// ignore: PendingIntent was not registered
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Utils {
|
|
||||||
private static class Option<T> {
|
|
||||||
private final T value;
|
|
||||||
private final boolean isPresent;
|
|
||||||
|
|
||||||
private Option(T value, boolean isPresent) {
|
|
||||||
this.value = value;
|
|
||||||
this.isPresent = isPresent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> Option<T> of(T value) {
|
|
||||||
return new Option<>(value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> Option<T> empty() {
|
|
||||||
return new Option<>(null, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void networkCallback(
|
|
||||||
ConnectivityManager connectivityManager,
|
|
||||||
Option<NetworkRequest> request,
|
|
||||||
Option<ConnectivityManager.NetworkCallback> networkCallback,
|
|
||||||
Option<PendingIntent> operation,
|
|
||||||
Option<Handler> handler,
|
|
||||||
Runnable fallback
|
|
||||||
) {
|
|
||||||
if(!request.isPresent || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && request.value != null && requestsWifiNetwork(request.value))) {
|
|
||||||
Runnable runnable = null;
|
|
||||||
if (networkCallback.isPresent && networkCallback.value != null) {
|
|
||||||
Network network = activeWifiNetwork(connectivityManager);
|
|
||||||
if (network != null) {
|
|
||||||
runnable = () -> networkCallback.value.onAvailable(network);
|
|
||||||
}
|
|
||||||
} else if (operation.isPresent && operation.value != null) {
|
|
||||||
runnable = () -> {
|
|
||||||
try {
|
|
||||||
operation.value.send();
|
|
||||||
} catch (PendingIntent.CanceledException ignore) {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (runnable != null) {
|
|
||||||
if (handler.isPresent) {
|
|
||||||
if (handler.value != null) {
|
|
||||||
handler.value.post(runnable);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
runnable.run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fallback.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an active (maybe fake) Wifi network if there is one, otherwise null.
|
|
||||||
private static Network activeWifiNetwork(ConnectivityManager connectivityManager) {
|
|
||||||
Network network = getActiveNetwork(connectivityManager);
|
|
||||||
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
|
|
||||||
if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) {
|
|
||||||
return network;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whether a Wifi network with connection is requested.
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
|
||||||
private static boolean requestsWifiNetwork(NetworkRequest request) {
|
|
||||||
return request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
|
||||||
&& (request.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
|| request.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
package app.revanced.extension.all.misc.directory.documentsprovider;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.ProviderInfo;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.os.CancellationSignal;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
|
||||||
import android.provider.DocumentsContract;
|
|
||||||
import android.provider.DocumentsProvider;
|
|
||||||
import android.system.ErrnoException;
|
|
||||||
import android.system.Os;
|
|
||||||
import android.system.StructStat;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A DocumentsProvider that allows access to the app's internal data directory.
|
|
||||||
*/
|
|
||||||
@SuppressLint("LongLogTag")
|
|
||||||
public class InternalDataDocumentsProvider extends DocumentsProvider {
|
|
||||||
private static final String[] rootColumns =
|
|
||||||
{"root_id", "mime_types", "flags", "icon", "title", "summary", "document_id"};
|
|
||||||
private static final String[] directoryColumns =
|
|
||||||
{"document_id", "mime_type", "_display_name", "last_modified", "flags",
|
|
||||||
"_size", "full_path", "lstat_info"};
|
|
||||||
private static final int S_IFLNK = 0x8000;
|
|
||||||
|
|
||||||
private String packageName;
|
|
||||||
private File dataDirectory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively delete a file or directory and all its children.
|
|
||||||
*
|
|
||||||
* @param root The file or directory to delete.
|
|
||||||
* @return True if the file or directory and all its children were successfully deleted.
|
|
||||||
*/
|
|
||||||
private static boolean deleteRecursively(File root) {
|
|
||||||
// If root is a directory, delete all children first
|
|
||||||
if (root.isDirectory()) {
|
|
||||||
try {
|
|
||||||
// Only delete recursively if the directory is not a symlink
|
|
||||||
if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) {
|
|
||||||
File[] files = root.listFiles();
|
|
||||||
if (files != null) {
|
|
||||||
for (File file : files) {
|
|
||||||
if (!deleteRecursively(file)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ErrnoException e) {
|
|
||||||
Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete file or empty directory
|
|
||||||
return root.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the MIME type of a file based on its extension.
|
|
||||||
*
|
|
||||||
* @param file The file to resolve the MIME type for.
|
|
||||||
* @return The MIME type of the file.
|
|
||||||
*/
|
|
||||||
private static String resolveMimeType(File file) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
return DocumentsContract.Document.MIME_TYPE_DIR;
|
|
||||||
}
|
|
||||||
|
|
||||||
String name = file.getName();
|
|
||||||
int indexOfExtDot = name.lastIndexOf('.');
|
|
||||||
if (indexOfExtDot < 0) {
|
|
||||||
// No extension
|
|
||||||
return "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
String extension = name.substring(indexOfExtDot + 1).toLowerCase();
|
|
||||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
|
||||||
return mimeType != null ? mimeType : "application/octet-stream";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean onCreate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void attachInfo(Context context, ProviderInfo providerInfo) {
|
|
||||||
super.attachInfo(context, providerInfo);
|
|
||||||
|
|
||||||
this.packageName = context.getPackageName();
|
|
||||||
this.dataDirectory = context.getFilesDir().getParentFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
|
|
||||||
File directory = resolveDocumentId(parentDocumentId);
|
|
||||||
File file = new File(directory, displayName);
|
|
||||||
|
|
||||||
// If file already exists, append a number to the name
|
|
||||||
int i = 2;
|
|
||||||
while (file.exists()) {
|
|
||||||
file = new File(directory, displayName + " (" + i + ")");
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create the file or directory
|
|
||||||
if (mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR) ? file.mkdir() : file.createNewFile()) {
|
|
||||||
// Return the document ID of the new entity
|
|
||||||
if (!parentDocumentId.endsWith("/")) {
|
|
||||||
parentDocumentId = parentDocumentId + "/";
|
|
||||||
}
|
|
||||||
return parentDocumentId + file.getName();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Do nothing. We are throwing a FileNotFoundException later if the file could not be created.
|
|
||||||
}
|
|
||||||
throw new FileNotFoundException("Failed to create document in " + parentDocumentId + " with name " + displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void deleteDocument(String documentId) throws FileNotFoundException {
|
|
||||||
File file = resolveDocumentId(documentId);
|
|
||||||
if (!deleteRecursively(file)) {
|
|
||||||
throw new FileNotFoundException("Failed to delete document " + documentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final String getDocumentType(String documentId) throws FileNotFoundException {
|
|
||||||
return resolveMimeType(resolveDocumentId(documentId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final boolean isChildDocument(String parentDocumentId, String documentId) {
|
|
||||||
return documentId.startsWith(parentDocumentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
|
|
||||||
File source = resolveDocumentId(sourceDocumentId);
|
|
||||||
File dest = resolveDocumentId(targetParentDocumentId);
|
|
||||||
|
|
||||||
File file = new File(dest, source.getName());
|
|
||||||
if (!file.exists() && source.renameTo(file)) {
|
|
||||||
// Return the new document ID
|
|
||||||
if (targetParentDocumentId.endsWith("/")) {
|
|
||||||
return targetParentDocumentId + file.getName();
|
|
||||||
}
|
|
||||||
return targetParentDocumentId + "/" + file.getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FileNotFoundException("Failed to move document from " + sourceDocumentId + " to " + targetParentDocumentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
|
|
||||||
File file = resolveDocumentId(documentId);
|
|
||||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
|
|
||||||
if (parentDocumentId.endsWith("/")) {
|
|
||||||
parentDocumentId = parentDocumentId.substring(0, parentDocumentId.length() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (projection == null) {
|
|
||||||
projection = directoryColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixCursor cursor = new MatrixCursor(projection);
|
|
||||||
File children = resolveDocumentId(parentDocumentId);
|
|
||||||
|
|
||||||
// Collect all children
|
|
||||||
File[] files = children.listFiles();
|
|
||||||
if (files != null) {
|
|
||||||
for (File file : files) {
|
|
||||||
addRowForDocument(cursor, parentDocumentId + "/" + file.getName(), file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
|
||||||
if (projection == null) {
|
|
||||||
projection = directoryColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixCursor cursor = new MatrixCursor(projection);
|
|
||||||
addRowForDocument(cursor, documentId, null);
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final Cursor queryRoots(String[] projection) {
|
|
||||||
ApplicationInfo info = Objects.requireNonNull(getContext()).getApplicationInfo();
|
|
||||||
String appName = info.loadLabel(getContext().getPackageManager()).toString();
|
|
||||||
|
|
||||||
if (projection == null) {
|
|
||||||
projection = rootColumns;
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixCursor cursor = new MatrixCursor(projection);
|
|
||||||
MatrixCursor.RowBuilder row = cursor.newRow();
|
|
||||||
row.add(DocumentsContract.Root.COLUMN_ROOT_ID, this.packageName);
|
|
||||||
row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, this.packageName);
|
|
||||||
row.add(DocumentsContract.Root.COLUMN_SUMMARY, this.packageName);
|
|
||||||
row.add(DocumentsContract.Root.COLUMN_FLAGS,
|
|
||||||
DocumentsContract.Root.FLAG_LOCAL_ONLY |
|
|
||||||
DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD);
|
|
||||||
row.add(DocumentsContract.Root.COLUMN_TITLE, appName);
|
|
||||||
row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*");
|
|
||||||
row.add(DocumentsContract.Root.COLUMN_ICON, info.icon);
|
|
||||||
return cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
|
|
||||||
deleteDocument(documentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final String renameDocument(String documentId, String displayName) throws FileNotFoundException {
|
|
||||||
File file = resolveDocumentId(documentId);
|
|
||||||
if (!file.renameTo(new File(file.getParentFile(), displayName))) {
|
|
||||||
throw new FileNotFoundException("Failed to rename document from " + documentId + " to " + displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the new document ID
|
|
||||||
return documentId.substring(0, documentId.lastIndexOf('/', documentId.length() - 2)) + "/" + displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a file instance for a given document ID.
|
|
||||||
*
|
|
||||||
* @param fullContentPath The document ID to resolve.
|
|
||||||
* @return File object for the given document ID.
|
|
||||||
* @throws FileNotFoundException If the document ID is invalid or the file does not exist.
|
|
||||||
*/
|
|
||||||
private File resolveDocumentId(String fullContentPath) throws FileNotFoundException {
|
|
||||||
if (!fullContentPath.startsWith(this.packageName)) {
|
|
||||||
throw new FileNotFoundException(fullContentPath + " not found");
|
|
||||||
}
|
|
||||||
String path = fullContentPath.substring(this.packageName.length());
|
|
||||||
|
|
||||||
// Resolve the relative path within /data/data/{PKG}
|
|
||||||
File file;
|
|
||||||
if (path.equals("/") || path.isEmpty()) {
|
|
||||||
file = this.dataDirectory;
|
|
||||||
} else {
|
|
||||||
// Remove leading slash
|
|
||||||
String relativePath = path.substring(1);
|
|
||||||
file = new File(this.dataDirectory, relativePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.exists()) {
|
|
||||||
throw new FileNotFoundException(fullContentPath + " not found");
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a row containing all file properties to a MatrixCursor for a given document ID.
|
|
||||||
*
|
|
||||||
* @param cursor The cursor to add the row to.
|
|
||||||
* @param documentId The document ID to add the row for.
|
|
||||||
* @param file The file to add the row for. If null, the file will be resolved from the document ID.
|
|
||||||
* @throws FileNotFoundException If the file does not exist.
|
|
||||||
*/
|
|
||||||
private void addRowForDocument(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException {
|
|
||||||
if (file == null) {
|
|
||||||
file = resolveDocumentId(documentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
int flags = 0;
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
// Prefer list view for directories
|
|
||||||
flags = flags | DocumentsContract.Document.FLAG_DIR_PREFERS_LAST_MODIFIED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.canWrite()) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
flags = flags | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
|
|
||||||
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
|
|
||||||
DocumentsContract.Document.FLAG_SUPPORTS_RENAME |
|
|
||||||
DocumentsContract.Document.FLAG_SUPPORTS_MOVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixCursor.RowBuilder row = cursor.newRow();
|
|
||||||
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId);
|
|
||||||
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.getName());
|
|
||||||
row.add(DocumentsContract.Document.COLUMN_SIZE, file.length());
|
|
||||||
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, resolveMimeType(file));
|
|
||||||
row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
|
||||||
row.add(DocumentsContract.Document.COLUMN_FLAGS, flags);
|
|
||||||
|
|
||||||
// Custom columns
|
|
||||||
row.add("full_path", file.getAbsolutePath());
|
|
||||||
|
|
||||||
// Add lstat column
|
|
||||||
String path = file.getPath();
|
|
||||||
try {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
StructStat lstat = Os.lstat(path);
|
|
||||||
sb.append(lstat.st_mode);
|
|
||||||
sb.append(";");
|
|
||||||
sb.append(lstat.st_uid);
|
|
||||||
sb.append(";");
|
|
||||||
sb.append(lstat.st_gid);
|
|
||||||
// Append symlink target if it is a symlink
|
|
||||||
if ((lstat.st_mode & S_IFLNK) == S_IFLNK) {
|
|
||||||
sb.append(";");
|
|
||||||
sb.append(Os.readlink(path));
|
|
||||||
}
|
|
||||||
row.add("lstat_info", sb.toString());
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Log.e("InternalDocumentsProvider", "Failed to get lstat info for " + path, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package app.revanced.extension.all.misc.screencapture.removerestriction;
|
|
||||||
|
|
||||||
import android.media.AudioAttributes;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class RemoveScreenCaptureRestrictionPatch {
|
|
||||||
// Member of AudioAttributes.Builder
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.Q)
|
|
||||||
public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) {
|
|
||||||
builder.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL);
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Member of AudioManager static class
|
|
||||||
public static void setAllowedCapturePolicy(final int capturePolicy) {
|
|
||||||
// Ignore request
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package app.revanced.extension.all.misc.screenshot.removerestriction;
|
|
||||||
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class RemoveScreenshotRestrictionPatch {
|
|
||||||
|
|
||||||
public static void addFlags(Window window, int flags) {
|
|
||||||
window.addFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setFlags(Window window, int flags, int mask) {
|
|
||||||
window.setFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE, mask & ~WindowManager.LayoutParams.FLAG_SECURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:shared:library"))
|
|
||||||
compileOnly(project(":extensions:boostforreddit:stub"))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,26 +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,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.rubenmayayo.reddit.ui.activities;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
public class WebViewActivity extends Activity {
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:shared:library"))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package app.revanced.extension.messenger.metaai;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class RemoveMetaAIPatch {
|
|
||||||
public static boolean overrideConfigBool(long id, boolean value) {
|
|
||||||
// It seems like all configs starting with 363219 are related to Meta AI.
|
|
||||||
// A list of specific ones that need disabling would probably be better,
|
|
||||||
// but these config numbers seem to change slightly with each update.
|
|
||||||
// These first 6 digits don't though.
|
|
||||||
if (Long.toString(id).startsWith("363219"))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
android {
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 26
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package app.revanced.extension.music.spoof;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @noinspection unused
|
|
||||||
*/
|
|
||||||
public class SpoofClientPatch {
|
|
||||||
private static final int CLIENT_TYPE_ID = 26;
|
|
||||||
private static final String CLIENT_VERSION = "6.21";
|
|
||||||
private static final String DEVICE_MODEL = "iPhone16,2";
|
|
||||||
private static final String OS_VERSION = "17.7.2.21H221";
|
|
||||||
|
|
||||||
public static int getClientId() {
|
|
||||||
return CLIENT_TYPE_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getClientVersion() {
|
|
||||||
return CLIENT_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getClientModel() {
|
|
||||||
return DEVICE_MODEL;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getOsVersion() {
|
|
||||||
return OS_VERSION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:shared:library"))
|
|
||||||
compileOnly(project(":extensions:nunl:stub"))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
package app.revanced.extension.nunl.ads;
|
|
||||||
|
|
||||||
import nl.nu.performance.api.client.interfaces.Block;
|
|
||||||
import nl.nu.performance.api.client.unions.SmallArticleLinkFlavor;
|
|
||||||
import nl.nu.performance.api.client.objects.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class HideAdsPatch {
|
|
||||||
private static final String[] blockedHeaderBlocks = {
|
|
||||||
"Aanbiedingen (Adverteerders)",
|
|
||||||
"Aangeboden door NUshop"
|
|
||||||
};
|
|
||||||
|
|
||||||
// "Rubrieken" menu links to ads.
|
|
||||||
private static final String[] blockedLinkBlocks = {
|
|
||||||
"Van onze adverteerders"
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void filterAds(List<Block> blocks) {
|
|
||||||
try {
|
|
||||||
ArrayList<Block> cleanedList = new ArrayList<>();
|
|
||||||
|
|
||||||
boolean skipFullHeader = false;
|
|
||||||
boolean skipUntilDivider = false;
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
while (index < blocks.size()) {
|
|
||||||
Block currentBlock = blocks.get(index);
|
|
||||||
|
|
||||||
// Because of pagination, we might not see the Divider in front of it.
|
|
||||||
// Just remove it as is and leave potential extra spacing visible on the screen.
|
|
||||||
if (currentBlock instanceof DpgBannerBlock) {
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index + 1 < blocks.size()) {
|
|
||||||
// Filter Divider -> DpgMediaBanner -> Divider.
|
|
||||||
if (currentBlock instanceof DividerBlock
|
|
||||||
&& blocks.get(index + 1) instanceof DpgBannerBlock) {
|
|
||||||
index += 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter Divider -> LinkBlock (... -> LinkBlock -> LinkBlock-> LinkBlock -> Divider).
|
|
||||||
if (currentBlock instanceof DividerBlock
|
|
||||||
&& blocks.get(index + 1) instanceof LinkBlock linkBlock) {
|
|
||||||
Link link = linkBlock.getLink();
|
|
||||||
if (link != null && link.getTitle() != null) {
|
|
||||||
for (String blockedLinkBlock : blockedLinkBlocks) {
|
|
||||||
if (blockedLinkBlock.equals(link.getTitle().getText())) {
|
|
||||||
skipUntilDivider = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (skipUntilDivider) {
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip LinkBlocks with a "flavor" claiming to be "isPartner" (sponsored inline ads).
|
|
||||||
if (currentBlock instanceof LinkBlock linkBlock
|
|
||||||
&& linkBlock.getLink() != null
|
|
||||||
&& linkBlock.getLink().getLinkFlavor() instanceof SmallArticleLinkFlavor smallArticleLinkFlavor
|
|
||||||
&& smallArticleLinkFlavor.isPartner() != null
|
|
||||||
&& smallArticleLinkFlavor.isPartner()) {
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentBlock instanceof DividerBlock) {
|
|
||||||
skipUntilDivider = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter HeaderBlock with known ads until next HeaderBlock.
|
|
||||||
if (currentBlock instanceof HeaderBlock headerBlock) {
|
|
||||||
StyledText headerText = headerBlock.getTitle();
|
|
||||||
if (headerText != null) {
|
|
||||||
skipFullHeader = false;
|
|
||||||
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
|
||||||
if (blockedHeaderBlock.equals(headerText.getText())) {
|
|
||||||
skipFullHeader = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (skipFullHeader) {
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipFullHeader && !skipUntilDivider) {
|
|
||||||
cleanedList.add(currentBlock);
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace list in-place to not deal with moving the result to the correct register in smali.
|
|
||||||
blocks.clear();
|
|
||||||
blocks.addAll(cleanedList);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "filterAds failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 26
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.interfaces;
|
|
||||||
|
|
||||||
public class Block {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.objects;
|
|
||||||
|
|
||||||
import nl.nu.performance.api.client.interfaces.Block;
|
|
||||||
|
|
||||||
public class DividerBlock extends Block {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.objects;
|
|
||||||
|
|
||||||
import nl.nu.performance.api.client.interfaces.Block;
|
|
||||||
|
|
||||||
public class DpgBannerBlock extends Block {
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.objects;
|
|
||||||
|
|
||||||
import nl.nu.performance.api.client.interfaces.Block;
|
|
||||||
|
|
||||||
public class HeaderBlock extends Block {
|
|
||||||
public final StyledText getTitle() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.objects;
|
|
||||||
|
|
||||||
import nl.nu.performance.api.client.unions.LinkFlavor;
|
|
||||||
|
|
||||||
public class Link {
|
|
||||||
public final StyledText getTitle() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
|
|
||||||
public final LinkFlavor getLinkFlavor() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.objects;
|
|
||||||
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import nl.nu.performance.api.client.interfaces.Block;
|
|
||||||
|
|
||||||
public abstract class LinkBlock extends Block implements Parcelable {
|
|
||||||
public final Link getLink() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.objects;
|
|
||||||
|
|
||||||
public class StyledText {
|
|
||||||
public final String getText() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.unions;
|
|
||||||
|
|
||||||
public interface LinkFlavor {
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package nl.nu.performance.api.client.unions;
|
|
||||||
|
|
||||||
public class SmallArticleLinkFlavor implements LinkFlavor {
|
|
||||||
public final Boolean isPartner() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:shared:library"))
|
|
||||||
compileOnly(project(":extensions:primevideo:stub"))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package app.revanced.extension.primevideo.ads;
|
|
||||||
|
|
||||||
import com.amazon.avod.fsm.SimpleTrigger;
|
|
||||||
import com.amazon.avod.media.ads.AdBreak;
|
|
||||||
import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
|
|
||||||
import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
|
|
||||||
import com.amazon.avod.media.playback.VideoPlayer;
|
|
||||||
import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class SkipAdsPatch {
|
|
||||||
public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
|
|
||||||
try {
|
|
||||||
AdBreak adBreak = trigger.getBreak();
|
|
||||||
|
|
||||||
// There are two scenarios when entering the original method:
|
|
||||||
// 1. Player naturally entered an ad break while watching a video.
|
|
||||||
// 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
|
|
||||||
// user is forced to watch an ad before continuing.
|
|
||||||
//
|
|
||||||
// Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
|
|
||||||
// target. Otherwise, just calculate when the ad break should end and skip to there.
|
|
||||||
if (trigger.getSeekStartPosition() != null)
|
|
||||||
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
|
|
||||||
else
|
|
||||||
player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
|
|
||||||
|
|
||||||
// Send "end of ads" trigger to state machine so everything doesn't get whacky.
|
|
||||||
state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "Failed skipping ads", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.amazon.avod.fsm;
|
|
||||||
|
|
||||||
public final class SimpleTrigger<T> implements Trigger<T> {
|
|
||||||
public SimpleTrigger(T triggerType) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.amazon.avod.fsm;
|
|
||||||
|
|
||||||
public abstract class StateBase<S, T> {
|
|
||||||
// This method orginally has protected access (modified in patch code).
|
|
||||||
public void doTrigger(Trigger<T> trigger) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.amazon.avod.fsm;
|
|
||||||
|
|
||||||
public interface Trigger<T> {
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.amazon.avod.media;
|
|
||||||
|
|
||||||
public final class TimeSpan {
|
|
||||||
public long getTotalMilliseconds() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.amazon.avod.media.ads;
|
|
||||||
|
|
||||||
import com.amazon.avod.media.TimeSpan;
|
|
||||||
|
|
||||||
public interface AdBreak {
|
|
||||||
TimeSpan getDurationExcludingAux();
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.amazon.avod.media.ads.internal.state;
|
|
||||||
|
|
||||||
public abstract class AdBreakState extends AdEnabledPlaybackState {
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.amazon.avod.media.ads.internal.state;
|
|
||||||
|
|
||||||
import com.amazon.avod.media.ads.AdBreak;
|
|
||||||
import com.amazon.avod.media.TimeSpan;
|
|
||||||
|
|
||||||
public class AdBreakTrigger {
|
|
||||||
public AdBreak getBreak() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimeSpan getSeekTarget() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimeSpan getSeekStartPosition() {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.amazon.avod.media.ads.internal.state;
|
|
||||||
|
|
||||||
import com.amazon.avod.fsm.StateBase;
|
|
||||||
import com.amazon.avod.media.playback.state.PlayerStateType;
|
|
||||||
import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
|
|
||||||
|
|
||||||
public class AdEnabledPlaybackState extends StateBase<PlayerStateType, PlayerTriggerType> {
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.amazon.avod.media.ads.internal.state;
|
|
||||||
|
|
||||||
public enum AdEnabledPlayerTriggerType {
|
|
||||||
NO_MORE_ADS_SKIP_TRANSITION
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.amazon.avod.media.ads.internal.state;
|
|
||||||
|
|
||||||
public class ServerInsertedAdBreakState extends AdBreakState {
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.amazon.avod.media.playback;
|
|
||||||
|
|
||||||
public interface VideoPlayer {
|
|
||||||
long getCurrentPosition();
|
|
||||||
|
|
||||||
void seekTo(long positionMs);
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.amazon.avod.media.playback.state;
|
|
||||||
|
|
||||||
public interface PlayerStateType {
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.amazon.avod.media.playback.state.trigger;
|
|
||||||
|
|
||||||
public interface PlayerTriggerType {
|
|
||||||
}
|
|
||||||
9
extensions/proguard-rules.pro
vendored
9
extensions/proguard-rules.pro
vendored
@@ -1,9 +0,0 @@
|
|||||||
-dontobfuscate
|
|
||||||
-dontoptimize
|
|
||||||
-keepattributes *
|
|
||||||
-keep class app.revanced.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
-keep class com.google.** {
|
|
||||||
*;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
compileOnly(project(":extensions:reddit:stub"))
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package app.revanced.extension.reddit.patches;
|
|
||||||
|
|
||||||
import com.reddit.domain.model.ILink;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public final class FilterPromotedLinksPatch {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*
|
|
||||||
* Filters list from promoted links.
|
|
||||||
**/
|
|
||||||
public static List<?> filterChildren(final Iterable<?> links) {
|
|
||||||
final List<Object> filteredList = new ArrayList<>();
|
|
||||||
|
|
||||||
for (Object item : links) {
|
|
||||||
if (item instanceof ILink && ((ILink) item).getPromoted()) continue;
|
|
||||||
|
|
||||||
filteredList.add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id(libs.plugins.android.library.get().pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<manifest/>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package com.reddit.domain.model;
|
|
||||||
|
|
||||||
public class ILink {
|
|
||||||
public boolean getPromoted() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
implementation(project(":extensions:shared:library"))
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.android.library")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "app.revanced.extension"
|
|
||||||
compileSdk = 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = 23
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
compileOnly(libs.annotation)
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
package app.revanced.extension.shared;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.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.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
|
||||||
import app.revanced.extension.shared.requests.Route;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class GmsCoreSupport {
|
|
||||||
private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube";
|
|
||||||
private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music";
|
|
||||||
|
|
||||||
private static final String GMS_CORE_PACKAGE_NAME
|
|
||||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
|
||||||
private static final Uri GMS_CORE_PROVIDER
|
|
||||||
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
|
||||||
private static final String DONT_KILL_MY_APP_URL
|
|
||||||
= "https://dontkillmyapp.com/";
|
|
||||||
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
|
|
||||||
= new Route(GET, "/api/v2/{manufacturer}.json");
|
|
||||||
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
|
|
||||||
= "?app=MicroG";
|
|
||||||
private static final String BUILD_MANUFACTURER
|
|
||||||
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a manufacturer specific page exists on DontKillMyApp.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
|
||||||
|
|
||||||
private static void open(String queryOrLink) {
|
|
||||||
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
|
||||||
|
|
||||||
Intent intent;
|
|
||||||
try {
|
|
||||||
// Check if queryOrLink is a valid URL.
|
|
||||||
new URL(queryOrLink);
|
|
||||||
|
|
||||||
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink));
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
intent = new Intent(Intent.ACTION_WEB_SEARCH);
|
|
||||||
intent.putExtra(SearchManager.QUERY, queryOrLink);
|
|
||||||
}
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
Utils.getContext().startActivity(intent);
|
|
||||||
|
|
||||||
// Gracefully exit, otherwise the broken app will continue to run.
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void showBatteryOptimizationDialog(Activity context,
|
|
||||||
String dialogMessageRef,
|
|
||||||
String positiveButtonTextRef,
|
|
||||||
DialogInterface.OnClickListener onPositiveClickListener) {
|
|
||||||
// Use a delay to allow the activity to finish initializing.
|
|
||||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
|
||||||
// Do not set cancelable to false, to allow using back button to skip the action,
|
|
||||||
// just in case the battery change can never be satisfied.
|
|
||||||
var dialog = new AlertDialog.Builder(context)
|
|
||||||
.setTitle(str("gms_core_dialog_title"))
|
|
||||||
.setMessage(str(dialogMessageRef))
|
|
||||||
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
|
|
||||||
.create();
|
|
||||||
Utils.showDialog(context, dialog);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
String packageName = context.getPackageName();
|
|
||||||
if (packageName.equals(PACKAGE_NAME_YOUTUBE) || packageName.equals(PACKAGE_NAME_YOUTUBE_MUSIC)) {
|
|
||||||
Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included");
|
|
||||||
// Cannot use localize text here, since the app will load
|
|
||||||
// resources from the unpatched app and all patch strings are missing.
|
|
||||||
Utils.showToastLong("The 'GmsCore support' patch breaks mount installations");
|
|
||||||
|
|
||||||
// Do not exit. If the app exits before launch completes (and without
|
|
||||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
|
||||||
// no toast will be shown and the app will continually relaunch
|
|
||||||
// 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 whitelisted from battery optimizations.
|
|
||||||
if (isAndroidAutomotive(context)) {
|
|
||||||
// Ignore Android Automotive devices (Google built-in),
|
|
||||||
// as there is no way to disable battery optimizations.
|
|
||||||
Logger.printDebug(() -> "Device is Android Automotive");
|
|
||||||
} else if (batteryOptimizationsEnabled(context)) {
|
|
||||||
Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations");
|
|
||||||
|
|
||||||
showBatteryOptimizationDialog(context,
|
|
||||||
"gms_core_dialog_not_whitelisted_using_battery_optimizations_message",
|
|
||||||
"gms_core_dialog_continue_text",
|
|
||||||
(dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if GmsCore is currently 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");
|
|
||||||
checkIfDontKillMyAppSupportsManufacturer();
|
|
||||||
|
|
||||||
showBatteryOptimizationDialog(context,
|
|
||||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
|
||||||
"gms_core_dialog_open_website_text",
|
|
||||||
(dialog, id) -> openDontKillMyApp());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkIfDontKillMyAppSupportsManufacturer() {
|
|
||||||
Utils.runOnBackgroundThread(() -> {
|
|
||||||
try {
|
|
||||||
final long start = System.currentTimeMillis();
|
|
||||||
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
|
||||||
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
|
|
||||||
connection.setConnectTimeout(5000);
|
|
||||||
connection.setReadTimeout(5000);
|
|
||||||
|
|
||||||
final boolean supported = connection.getResponseCode() == 200;
|
|
||||||
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
|
|
||||||
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
|
|
||||||
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
|
|
||||||
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
|
|
||||||
+ BUILD_MANUFACTURER, ex);
|
|
||||||
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void openDontKillMyApp() {
|
|
||||||
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
|
||||||
|
|
||||||
String manufacturerPageToOpen;
|
|
||||||
if (manufacturerSupported == null) {
|
|
||||||
// Fetch has not completed yet. Only happens on extremely slow internet connections
|
|
||||||
// and the user spends less than 1 second reading what's on screen.
|
|
||||||
// Instead of waiting for the fetch (which may timeout),
|
|
||||||
// open the website without a vendor.
|
|
||||||
manufacturerPageToOpen = "";
|
|
||||||
} else if (manufacturerSupported) {
|
|
||||||
manufacturerPageToOpen = BUILD_MANUFACTURER;
|
|
||||||
} else {
|
|
||||||
// No manufacturer specific page exists. Open the general page.
|
|
||||||
manufacturerPageToOpen = "general";
|
|
||||||
}
|
|
||||||
|
|
||||||
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
|
||||||
*/
|
|
||||||
private static boolean batteryOptimizationsEnabled(Context context) {
|
|
||||||
var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
|
||||||
return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isAndroidAutomotive(Context context) {
|
|
||||||
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getGmsCoreDownload() {
|
|
||||||
final var vendorGroupId = getGmsCoreVendorGroupId();
|
|
||||||
//noinspection SwitchStatementWithTooFewBranches
|
|
||||||
return switch (vendorGroupId) {
|
|
||||||
case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest";
|
|
||||||
default -> vendorGroupId + ".android.gms";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modified by a patch. Do not touch.
|
|
||||||
private static String getGmsCoreVendorGroupId() {
|
|
||||||
return "app.revanced";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
package app.revanced.extension.shared;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
|
|
||||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
|
|
||||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
|
||||||
* and additionally accessible thru {@link LogBufferManager}.
|
|
||||||
*
|
|
||||||
* All methods are thread safe.
|
|
||||||
*/
|
|
||||||
public class Logger {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log messages using lambdas.
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
|
||||||
public interface LogMessage {
|
|
||||||
/**
|
|
||||||
* @return Logger string message. This method is only called if logging is enabled.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
String buildMessageString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum LogLevel {
|
|
||||||
DEBUG,
|
|
||||||
INFO,
|
|
||||||
ERROR
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log tag prefix. Only used for system logging.
|
|
||||||
*/
|
|
||||||
private static final String REVANCED_LOG_TAG_PREFIX = "revanced: ";
|
|
||||||
|
|
||||||
private static final String LOGGER_CLASS_NAME = Logger.class.getName();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
|
||||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
|
||||||
* <br>
|
|
||||||
* For example, each of these classes returns 'SomethingView':
|
|
||||||
* <code>
|
|
||||||
* com.company.SomethingView
|
|
||||||
* com.company.SomethingView$StaticClass
|
|
||||||
* com.company.SomethingView$1
|
|
||||||
* </code>
|
|
||||||
*/
|
|
||||||
private static String getOuterClassSimpleName(Object obj) {
|
|
||||||
Class<?> logClass = obj.getClass();
|
|
||||||
String fullClassName = logClass.getName();
|
|
||||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
|
||||||
if (dollarSignIndex < 0) {
|
|
||||||
return logClass.getSimpleName(); // Already an outer class.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Class is inner, static, or anonymous.
|
|
||||||
// Parse the simple name full name.
|
|
||||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
|
||||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
|
||||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal method to handle logging to Android Log and {@link LogBufferManager}.
|
|
||||||
* Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
|
|
||||||
* with class name but without 'revanced:' prefix.
|
|
||||||
*
|
|
||||||
* @param logLevel The log level.
|
|
||||||
* @param message Log message object.
|
|
||||||
* @param ex Optional exception.
|
|
||||||
* @param includeStackTrace If the current stack should be included.
|
|
||||||
* @param showToast If a toast is to be shown.
|
|
||||||
*/
|
|
||||||
private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
|
|
||||||
boolean includeStackTrace, boolean showToast) {
|
|
||||||
// It's very important that no Settings are used in this method,
|
|
||||||
// as this code is used when a context is not set and thus referencing
|
|
||||||
// a setting will crash the app.
|
|
||||||
String messageString = message.buildMessageString();
|
|
||||||
String className = getOuterClassSimpleName(message);
|
|
||||||
|
|
||||||
String logText = messageString;
|
|
||||||
|
|
||||||
// Append exception message if present.
|
|
||||||
if (ex != null) {
|
|
||||||
var exceptionMessage = ex.getMessage();
|
|
||||||
if (exceptionMessage != null) {
|
|
||||||
logText += "\nException: " + exceptionMessage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeStackTrace) {
|
|
||||||
var sw = new StringWriter();
|
|
||||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
|
||||||
String stackTrace = sw.toString();
|
|
||||||
// Remove the stacktrace elements of this class.
|
|
||||||
final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
|
|
||||||
final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
|
|
||||||
logText += stackTrace.substring(loggerBegins);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not include "revanced:" prefix in clipboard logs.
|
|
||||||
String managerToastString = className + ": " + logText;
|
|
||||||
LogBufferManager.appendToLogBuffer(managerToastString);
|
|
||||||
|
|
||||||
String logTag = REVANCED_LOG_TAG_PREFIX + className;
|
|
||||||
switch (logLevel) {
|
|
||||||
case DEBUG:
|
|
||||||
if (ex == null) Log.d(logTag, logText);
|
|
||||||
else Log.d(logTag, logText, ex);
|
|
||||||
break;
|
|
||||||
case INFO:
|
|
||||||
if (ex == null) Log.i(logTag, logText);
|
|
||||||
else Log.i(logTag, logText, ex);
|
|
||||||
break;
|
|
||||||
case ERROR:
|
|
||||||
if (ex == null) Log.e(logTag, logText);
|
|
||||||
else Log.e(logTag, logText, ex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showToast) {
|
|
||||||
Utils.showToastLong(managerToastString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs debug messages under the outer class name of the code calling this method.
|
|
||||||
* <p>
|
|
||||||
* Whenever possible, the log string should be constructed entirely inside
|
|
||||||
* {@link LogMessage#buildMessageString()} so the performance cost of
|
|
||||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
|
||||||
*/
|
|
||||||
public static void printDebug(LogMessage message) {
|
|
||||||
printDebug(message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs debug messages under the outer class name of the code calling this method.
|
|
||||||
* <p>
|
|
||||||
* Whenever possible, the log string should be constructed entirely inside
|
|
||||||
* {@link LogMessage#buildMessageString()} so the performance cost of
|
|
||||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
|
||||||
*/
|
|
||||||
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
|
||||||
if (DEBUG.get()) {
|
|
||||||
logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs information messages using the outer class name of the code calling this method.
|
|
||||||
*/
|
|
||||||
public static void printInfo(LogMessage message) {
|
|
||||||
printInfo(message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs information messages using the outer class name of the code calling this method.
|
|
||||||
*/
|
|
||||||
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
|
||||||
logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs exceptions under the outer class name of the code calling this method.
|
|
||||||
* Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
|
|
||||||
*/
|
|
||||||
public static void printException(LogMessage message) {
|
|
||||||
printException(message, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs exceptions under the outer class name of the code calling this method.
|
|
||||||
* <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(LogMessage message, @Nullable Throwable ex) {
|
|
||||||
logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(LogMessage message) {
|
|
||||||
logInternal(LogLevel.INFO, message, null, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(LogMessage message, @Nullable Exception ex) {
|
|
||||||
logInternal(LogLevel.ERROR, message, ex, false, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,892 +0,0 @@
|
|||||||
package app.revanced.extension.shared;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.app.DialogFragment;
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.net.ConnectivityManager;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceGroup;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
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.AppLanguage;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
|
||||||
|
|
||||||
public class Utils {
|
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
private static volatile Context context;
|
|
||||||
|
|
||||||
private static String versionName;
|
|
||||||
private static String applicationLabel;
|
|
||||||
|
|
||||||
private Utils() {
|
|
||||||
} // utility class
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
*
|
|
||||||
* @return The manifest 'Version' entry of the patches.jar used during patching.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("SameReturnValue")
|
|
||||||
public static String getPatchesReleaseVersion() {
|
|
||||||
return ""; // Value is replaced during patching.
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PackageInfo getPackageInfo() throws PackageManager.NameNotFoundException {
|
|
||||||
final var packageName = Objects.requireNonNull(getContext()).getPackageName();
|
|
||||||
|
|
||||||
PackageManager packageManager = context.getPackageManager();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
return packageManager.getPackageInfo(
|
|
||||||
packageName,
|
|
||||||
PackageManager.PackageInfoFlags.of(0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return packageManager.getPackageInfo(
|
|
||||||
packageName,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The version name of the app, such as 19.11.43
|
|
||||||
*/
|
|
||||||
public static String getAppVersionName() {
|
|
||||||
if (versionName == null) {
|
|
||||||
try {
|
|
||||||
versionName = getPackageInfo().versionName;
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "Failed to get package info", ex);
|
|
||||||
versionName = "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return versionName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getApplicationName() {
|
|
||||||
if (applicationLabel == null) {
|
|
||||||
try {
|
|
||||||
ApplicationInfo applicationInfo = getPackageInfo().applicationInfo;
|
|
||||||
applicationLabel = (String) applicationInfo.loadLabel(context.getPackageManager());
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "Failed to get application name", ex);
|
|
||||||
applicationLabel = "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return applicationLabel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide a view by setting its layout height and width to 1dp.
|
|
||||||
*
|
|
||||||
* @param 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 = Objects.requireNonNull(context.getPackageManager().getLaunchIntentForPackage(packageName));
|
|
||||||
Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent());
|
|
||||||
// Required for API 34 and later
|
|
||||||
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
|
|
||||||
mainIntent.setPackage(packageName);
|
|
||||||
context.startActivity(mainIntent);
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Context getContext() {
|
|
||||||
if (context == null) {
|
|
||||||
Logger.initializationException(() -> "Context is not set by extension hook, returning null", null);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setContext(Context appContext) {
|
|
||||||
// Intentionally use logger before context is set,
|
|
||||||
// to expose any bugs in the 'no context available' logger method.
|
|
||||||
Logger.initializationInfo(() -> "Set context: " + appContext);
|
|
||||||
// Must initially set context to check the app language.
|
|
||||||
context = appContext;
|
|
||||||
|
|
||||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
|
||||||
if (language != AppLanguage.DEFAULT) {
|
|
||||||
// Create a new context with the desired language.
|
|
||||||
Logger.printDebug(() -> "Using app language: " + language);
|
|
||||||
Configuration config = new Configuration(appContext.getResources().getConfiguration());
|
|
||||||
config.setLocale(language.getLocale());
|
|
||||||
context = appContext.createConfigurationContext(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setClipboard(CharSequence 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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc).
|
|
||||||
* If this should match any ReVanced language override then instead use
|
|
||||||
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
|
|
||||||
* This is the default locale of the device, which may differ if
|
|
||||||
* {@link BaseSettings#REVANCED_LANGUAGE} is set to a different language.
|
|
||||||
*/
|
|
||||||
public static boolean isRightToLeftLocale() {
|
|
||||||
if (isRightToLeftTextLayout == null) {
|
|
||||||
isRightToLeftTextLayout = isRightToLeftLocale(Locale.getDefault());
|
|
||||||
}
|
|
||||||
return isRightToLeftTextLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc).
|
|
||||||
*/
|
|
||||||
public static boolean isRightToLeftLocale(Locale locale) {
|
|
||||||
String displayLanguage = locale.getDisplayLanguage();
|
|
||||||
return new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return A UTF8 string containing a left-to-right or right-to-left
|
|
||||||
* character of the device locale. If this should match any ReVanced language
|
|
||||||
* override then instead use {@link #getTextDirectionString(Locale)} with
|
|
||||||
* {@link BaseSettings#REVANCED_LANGUAGE}.
|
|
||||||
*/
|
|
||||||
public static String getTextDirectionString() {
|
|
||||||
return getTextDirectionString(isRightToLeftLocale());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getTextDirectionString(Locale locale) {
|
|
||||||
return getTextDirectionString(isRightToLeftLocale(locale));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getTextDirectionString(boolean isRightToLeft) {
|
|
||||||
return isRightToLeft
|
|
||||||
? "\u200F" // u200F = right to left character.
|
|
||||||
: "\u200E"; // u200E = left to right character.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return if the text contains at least 1 number character,
|
|
||||||
* including any unicode numbers such as Arabic.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
||||||
public static boolean containsNumber(@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(() -> {
|
|
||||||
Context currentContext = context;
|
|
||||||
|
|
||||||
if (currentContext == null) {
|
|
||||||
Logger.initializationException(() -> "Cannot show toast (context is null): " + messageToToast, null);
|
|
||||||
} else {
|
|
||||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
|
||||||
Toast.makeText(currentContext, messageToToast, toastDuration).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isDarkModeEnabled() {
|
|
||||||
Configuration config = Resources.getSystem().getConfiguration();
|
|
||||||
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
|
||||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isLandscapeOrientation() {
|
|
||||||
final int orientation = Resources.getSystem().getConfiguration().orientation;
|
|
||||||
return orientation == Configuration.ORIENTATION_LANDSCAPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically logs any exceptions the runnable throws.
|
|
||||||
*
|
|
||||||
* @see #runOnMainThreadNowOrLater(Runnable)
|
|
||||||
*/
|
|
||||||
public static void runOnMainThread(@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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calling extension code must ensure the un-patched app has the permission
|
|
||||||
* <code>android.permission.ACCESS_NETWORK_STATE</code>, otherwise the app will crash
|
|
||||||
* if this method is used.
|
|
||||||
*/
|
|
||||||
public static boolean isNetworkConnected() {
|
|
||||||
NetworkType networkType = getNetworkType();
|
|
||||||
return networkType == NetworkType.MOBILE
|
|
||||||
|| networkType == NetworkType.OTHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calling extension code must ensure the un-patched app has the permission
|
|
||||||
* <code>android.permission.ACCESS_NETWORK_STATE</code>, otherwise the app will crash
|
|
||||||
* if this method is used.
|
|
||||||
*/
|
|
||||||
@SuppressLint({"MissingPermission", "deprecation"})
|
|
||||||
public static NetworkType getNetworkType() {
|
|
||||||
Context networkContext = getContext();
|
|
||||||
if (networkContext == null) {
|
|
||||||
return NetworkType.NONE;
|
|
||||||
}
|
|
||||||
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
|
||||||
var networkInfo = cm.getActiveNetworkInfo();
|
|
||||||
|
|
||||||
if (networkInfo == null || !networkInfo.isConnected()) {
|
|
||||||
return NetworkType.NONE;
|
|
||||||
}
|
|
||||||
var type = networkInfo.getType();
|
|
||||||
return (type == ConnectivityManager.TYPE_MOBILE)
|
|
||||||
|| (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 removePunctuationToLowercase(@Nullable CharSequence original) {
|
|
||||||
if (original == null) return "";
|
|
||||||
return punctuationPattern.matcher(original).replaceAll("")
|
|
||||||
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(PreferenceGroup group) {
|
|
||||||
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
|
||||||
List<Pair<String, Preference>> preferences = new ArrayList<>();
|
|
||||||
|
|
||||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
|
||||||
Preference preference = group.getPreference(i);
|
|
||||||
|
|
||||||
final Sort preferenceSort;
|
|
||||||
if (preference instanceof PreferenceGroup subGroup) {
|
|
||||||
sortPreferenceGroups(subGroup);
|
|
||||||
preferenceSort = groupSort; // Sort value for groups is for it's content, not itself.
|
|
||||||
} else {
|
|
||||||
// Allow individual preferences to set a key sorting.
|
|
||||||
// Used to force a preference to the top or bottom of a group.
|
|
||||||
preferenceSort = Sort.fromKey(preference.getKey(), groupSort);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String sortValue;
|
|
||||||
switch (preferenceSort) {
|
|
||||||
case BY_TITLE:
|
|
||||||
sortValue = removePunctuationToLowercase(preference.getTitle());
|
|
||||||
break;
|
|
||||||
case BY_KEY:
|
|
||||||
sortValue = preference.getKey();
|
|
||||||
break;
|
|
||||||
case UNSORTED:
|
|
||||||
continue; // Keep original sorting.
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
preferences.add(new Pair<>(sortValue, preference));
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection ComparatorCombinators
|
|
||||||
Collections.sort(preferences, (pair1, pair2)
|
|
||||||
-> pair1.first.compareTo(pair2.first));
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
for (Pair<String, Preference> pair : preferences) {
|
|
||||||
int order = index++;
|
|
||||||
Preference pref = pair.second;
|
|
||||||
|
|
||||||
// Move any screens, intents, and the one off About preference to the top.
|
|
||||||
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|
|
||||||
|| pref.getIntent() != null) {
|
|
||||||
// Any arbitrary large number.
|
|
||||||
order -= 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
pref.setOrder(order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set all preferences to multiline titles if the device is not using an English variant.
|
|
||||||
* The English strings are heavily scrutinized and all titles fit on screen
|
|
||||||
* except 2 or 3 preference strings and those do not affect readability.
|
|
||||||
*
|
|
||||||
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
|
|
||||||
* and visually it looks better to clip the text and keep all titles 1 line.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage();
|
|
||||||
if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
|
||||||
Preference pref = group.getPreference(i);
|
|
||||||
pref.setSingleLineTitle(false);
|
|
||||||
|
|
||||||
if (pref instanceof PreferenceGroup subGroup) {
|
|
||||||
setPreferenceTitlesToMultiLineIfNeeded(subGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a color resource or hex code to an int representation of the color.
|
|
||||||
*/
|
|
||||||
public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException {
|
|
||||||
if (colorString.startsWith("#")) {
|
|
||||||
return Color.parseColor(colorString);
|
|
||||||
}
|
|
||||||
return getResourceColor(colorString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts dip value to actual device pixels.
|
|
||||||
*
|
|
||||||
* @param dip The density-independent pixels value
|
|
||||||
* @return The device pixel value
|
|
||||||
*/
|
|
||||||
public static int dipToPixels(float dip) {
|
|
||||||
return (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
dip,
|
|
||||||
Resources.getSystem().getDisplayMetrics()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int clamp(int value, int lower, int upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float clamp(float value, float lower, float upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.shared.settings.BaseSettings;
|
|
||||||
|
|
||||||
abstract class Check {
|
|
||||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
|
||||||
|
|
||||||
private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15;
|
|
||||||
private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10;
|
|
||||||
|
|
||||||
private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
protected abstract Boolean check();
|
|
||||||
|
|
||||||
protected abstract String failureReason();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies a sorting order for displaying the checks that failed.
|
|
||||||
* A lower value indicates to show first before other checks.
|
|
||||||
*/
|
|
||||||
public abstract int uiSortingValue();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For debugging and development only.
|
|
||||||
* Forces all checks to be performed and the check failed dialog to be shown.
|
|
||||||
* Can be enabled by importing settings text with {@link BaseSettings#CHECK_ENVIRONMENT_WARNINGS_ISSUED}
|
|
||||||
* set to -1.
|
|
||||||
*/
|
|
||||||
static boolean debugAlwaysShowWarning() {
|
|
||||||
final boolean alwaysShowWarning = BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0;
|
|
||||||
if (alwaysShowWarning) {
|
|
||||||
Logger.printInfo(() -> "Debug forcing environment check warning to show");
|
|
||||||
}
|
|
||||||
|
|
||||||
return alwaysShowWarning;
|
|
||||||
}
|
|
||||||
|
|
||||||
static boolean shouldRun() {
|
|
||||||
return BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()
|
|
||||||
< NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void disableForever() {
|
|
||||||
Logger.printInfo(() -> "Environment checks disabled forever");
|
|
||||||
|
|
||||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
|
||||||
BaseSettings.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,32 +0,0 @@
|
|||||||
package app.revanced.extension.shared.checks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fields are set by the patch. Do not modify.
|
|
||||||
* Fields are not final, because the compiler is inlining them.
|
|
||||||
*
|
|
||||||
* @noinspection CanBeFinal
|
|
||||||
*/
|
|
||||||
final class PatchInfo {
|
|
||||||
static long PATCH_TIME = 0L;
|
|
||||||
|
|
||||||
final static class Build {
|
|
||||||
static String PATCH_BOARD = "";
|
|
||||||
static String PATCH_BOOTLOADER = "";
|
|
||||||
static String PATCH_BRAND = "";
|
|
||||||
static String PATCH_CPU_ABI = "";
|
|
||||||
static String PATCH_CPU_ABI2 = "";
|
|
||||||
static String PATCH_DEVICE = "";
|
|
||||||
static String PATCH_DISPLAY = "";
|
|
||||||
static String PATCH_FINGERPRINT = "";
|
|
||||||
static String PATCH_HARDWARE = "";
|
|
||||||
static String PATCH_HOST = "";
|
|
||||||
static String PATCH_ID = "";
|
|
||||||
static String PATCH_MANUFACTURER = "";
|
|
||||||
static String PATCH_MODEL = "";
|
|
||||||
static String PATCH_PRODUCT = "";
|
|
||||||
static String PATCH_RADIO = "";
|
|
||||||
static String PATCH_TAGS = "";
|
|
||||||
static String PATCH_TYPE = "";
|
|
||||||
static String PATCH_USER = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,145 +0,0 @@
|
|||||||
package app.revanced.extension.shared.requests;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
public class Requester {
|
|
||||||
private Requester() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
|
|
||||||
return getConnectionFromCompiledRoute(apiUrl, route.compile(params));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
|
|
||||||
String url = apiUrl + route.getCompiledRoute();
|
|
||||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
|
||||||
// Request data is in the URL parameters and no body is sent.
|
|
||||||
// The calling code must set a length if using a request body.
|
|
||||||
connection.setFixedLengthStreamingMode(0);
|
|
||||||
connection.setRequestMethod(route.getMethod().name());
|
|
||||||
String agentString = System.getProperty("http.agent")
|
|
||||||
+ "; ReVanced/" + Utils.getAppVersionName()
|
|
||||||
+ " (" + Utils.getPatchesReleaseVersion() + ")";
|
|
||||||
connection.setRequestProperty("User-Agent", agentString);
|
|
||||||
|
|
||||||
return connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
|
|
||||||
*/
|
|
||||||
private static String parseInputStreamAndClose(InputStream inputStream) throws IOException {
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
|
||||||
StringBuilder jsonBuilder = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
jsonBuilder.append(line);
|
|
||||||
jsonBuilder.append('\n');
|
|
||||||
}
|
|
||||||
return jsonBuilder.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection} response as a String.
|
|
||||||
* This does not close the url connection. If further requests to this host are unlikely
|
|
||||||
* in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}.
|
|
||||||
*/
|
|
||||||
public static String parseString(HttpURLConnection connection) throws IOException {
|
|
||||||
return parseInputStreamAndClose(connection.getInputStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection} response as a String, and disconnect.
|
|
||||||
*
|
|
||||||
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
|
|
||||||
*
|
|
||||||
* @see #parseString(HttpURLConnection)
|
|
||||||
*/
|
|
||||||
public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException {
|
|
||||||
String result = parseString(connection);
|
|
||||||
connection.disconnect();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection} error stream as a String.
|
|
||||||
* If the server sent no error response data, this returns an empty string.
|
|
||||||
*/
|
|
||||||
public static String parseErrorString(HttpURLConnection connection) throws IOException {
|
|
||||||
InputStream errorStream = connection.getErrorStream();
|
|
||||||
if (errorStream == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return parseInputStreamAndClose(errorStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection} error stream as a String, and disconnect.
|
|
||||||
* If the server sent no error response data, this returns an empty string.
|
|
||||||
*
|
|
||||||
* Should only be used if other requests to the server are unlikely in the near future.
|
|
||||||
*
|
|
||||||
* @see #parseErrorString(HttpURLConnection)
|
|
||||||
*/
|
|
||||||
public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException {
|
|
||||||
String result = parseErrorString(connection);
|
|
||||||
connection.disconnect();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection} response into a JSONObject.
|
|
||||||
* This does not close the url connection. If further requests to this host are unlikely
|
|
||||||
* in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}.
|
|
||||||
*/
|
|
||||||
public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException {
|
|
||||||
return new JSONObject(parseString(connection));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
|
|
||||||
*
|
|
||||||
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
|
|
||||||
*
|
|
||||||
* @see #parseJSONObject(HttpURLConnection)
|
|
||||||
*/
|
|
||||||
public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
|
|
||||||
JSONObject object = parseJSONObject(connection);
|
|
||||||
connection.disconnect();
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection}, and closes the underlying InputStream.
|
|
||||||
* This does not close the url connection. If further requests to this host are unlikely
|
|
||||||
* in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}.
|
|
||||||
*/
|
|
||||||
public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException {
|
|
||||||
return new JSONArray(parseString(connection));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect.
|
|
||||||
*
|
|
||||||
* <b>Should only be used if other requests to the server in the near future are unlikely</b>
|
|
||||||
*
|
|
||||||
* @see #parseJSONArray(HttpURLConnection)
|
|
||||||
*/
|
|
||||||
public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException {
|
|
||||||
JSONArray array = parseJSONArray(connection);
|
|
||||||
connection.disconnect();
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package app.revanced.extension.shared.requests;
|
|
||||||
|
|
||||||
public class Route {
|
|
||||||
private final String route;
|
|
||||||
private final Method method;
|
|
||||||
private final int paramCount;
|
|
||||||
|
|
||||||
public Route(Method method, String route) {
|
|
||||||
this.method = method;
|
|
||||||
this.route = route;
|
|
||||||
this.paramCount = countMatches(route, '{');
|
|
||||||
|
|
||||||
if (paramCount != countMatches(route, '}'))
|
|
||||||
throw new IllegalArgumentException("Not enough parameters");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Method getMethod() {
|
|
||||||
return method;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompiledRoute compile(String... params) {
|
|
||||||
if (params.length != paramCount)
|
|
||||||
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
|
|
||||||
"Expected: " + paramCount + ", provided: " + params.length);
|
|
||||||
|
|
||||||
StringBuilder compiledRoute = new StringBuilder(route);
|
|
||||||
for (int i = 0; i < paramCount; i++) {
|
|
||||||
int paramStart = compiledRoute.indexOf("{");
|
|
||||||
int paramEnd = compiledRoute.indexOf("}");
|
|
||||||
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
|
|
||||||
}
|
|
||||||
return new CompiledRoute(this, compiledRoute.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CompiledRoute {
|
|
||||||
private final Route baseRoute;
|
|
||||||
private final String compiledRoute;
|
|
||||||
|
|
||||||
private CompiledRoute(Route baseRoute, String compiledRoute) {
|
|
||||||
this.baseRoute = baseRoute;
|
|
||||||
this.compiledRoute = compiledRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCompiledRoute() {
|
|
||||||
return compiledRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Method getMethod() {
|
|
||||||
return baseRoute.method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int countMatches(CharSequence seq, char c) {
|
|
||||||
int count = 0;
|
|
||||||
for (int i = 0; i < seq.length(); i++) {
|
|
||||||
if (seq.charAt(i) == c)
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Method {
|
|
||||||
GET,
|
|
||||||
POST
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package app.revanced.extension.shared.settings;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public enum AppLanguage {
|
|
||||||
/**
|
|
||||||
* The current app language.
|
|
||||||
*/
|
|
||||||
DEFAULT,
|
|
||||||
|
|
||||||
// Languages codes not included with YouTube, but are translated on Crowdin
|
|
||||||
GA,
|
|
||||||
|
|
||||||
// Language codes found in locale_config.xml
|
|
||||||
// All region specific variants have been removed.
|
|
||||||
AF,
|
|
||||||
AM,
|
|
||||||
AR,
|
|
||||||
AS,
|
|
||||||
AZ,
|
|
||||||
BE,
|
|
||||||
BG,
|
|
||||||
BN,
|
|
||||||
BS,
|
|
||||||
CA,
|
|
||||||
CS,
|
|
||||||
DA,
|
|
||||||
DE,
|
|
||||||
EL,
|
|
||||||
EN,
|
|
||||||
ES,
|
|
||||||
ET,
|
|
||||||
EU,
|
|
||||||
FA,
|
|
||||||
FI,
|
|
||||||
FR,
|
|
||||||
GL,
|
|
||||||
GU,
|
|
||||||
HI,
|
|
||||||
HE, // App uses obsolete 'IW' and not the modern 'HE' ISO code.
|
|
||||||
HR,
|
|
||||||
HU,
|
|
||||||
HY,
|
|
||||||
ID,
|
|
||||||
IS,
|
|
||||||
IT,
|
|
||||||
JA,
|
|
||||||
KA,
|
|
||||||
KK,
|
|
||||||
KM,
|
|
||||||
KN,
|
|
||||||
KO,
|
|
||||||
KY,
|
|
||||||
LO,
|
|
||||||
LT,
|
|
||||||
LV,
|
|
||||||
MK,
|
|
||||||
ML,
|
|
||||||
MN,
|
|
||||||
MR,
|
|
||||||
MS,
|
|
||||||
MY,
|
|
||||||
NE,
|
|
||||||
NL,
|
|
||||||
NB,
|
|
||||||
OR,
|
|
||||||
PA,
|
|
||||||
PL,
|
|
||||||
PT,
|
|
||||||
RO,
|
|
||||||
RU,
|
|
||||||
SI,
|
|
||||||
SK,
|
|
||||||
SL,
|
|
||||||
SQ,
|
|
||||||
SR,
|
|
||||||
SV,
|
|
||||||
SW,
|
|
||||||
TA,
|
|
||||||
TE,
|
|
||||||
TH,
|
|
||||||
TL,
|
|
||||||
TR,
|
|
||||||
UK,
|
|
||||||
UR,
|
|
||||||
UZ,
|
|
||||||
VI,
|
|
||||||
ZH,
|
|
||||||
ZU;
|
|
||||||
|
|
||||||
private final String language;
|
|
||||||
private final Locale locale;
|
|
||||||
|
|
||||||
AppLanguage() {
|
|
||||||
language = name().toLowerCase(Locale.US);
|
|
||||||
locale = Locale.forLanguageTag(language);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The 2 letter ISO 639_1 language code.
|
|
||||||
*/
|
|
||||||
public String getLanguage() {
|
|
||||||
// Changing the app language does not force the app to completely restart,
|
|
||||||
// so the default needs to be the current language and not a static field.
|
|
||||||
if (this == DEFAULT) {
|
|
||||||
return Locale.getDefault().getLanguage();
|
|
||||||
}
|
|
||||||
|
|
||||||
return language;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Locale getLocale() {
|
|
||||||
if (this == DEFAULT) {
|
|
||||||
return Locale.getDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
return locale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user