Compare commits

..

2 Commits

Author SHA1 Message Date
semantic-release-bot
c38ffb45fb chore(release): 2.196.0-dev.14 [skip ci]
# [2.196.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v2.196.0-dev.13...v2.196.0-dev.14) (2023-11-04)

### Features

* **Digitales Amt:** Bump compatibility to `3.0.2` ([#3217](https://github.com/ReVanced/revanced-patches/issues/3217)) ([f933d2c](f933d2c537))
2023-11-04 18:16:25 +00:00
fe
f933d2c537 feat(Digitales Amt): Bump compatibility to 3.0.2 (#3217)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-11-04 19:13:08 +01:00
1763 changed files with 24105 additions and 152850 deletions

View File

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

49
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

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

View File

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

View File

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

View File

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

2
.github/config.yml vendored
View File

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

View File

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

View File

@@ -1,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

View File

@@ -16,16 +16,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: main
destination_branch: 'main'
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: |
This pull request will ${{ env.MESSAGE }}.
## Dependencies before merge
## Before merging this PR
- [ ] Pull translations from Crowdin
- [ ] https://github.com/revanced/revanced-integrations
pr_draft: true

View File

@@ -1,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)"

View File

@@ -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 }}

View File

@@ -6,61 +6,39 @@ on:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
release:
name: Release
permissions:
contents: write
packages: write
id-token: write
attestations: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
- name: Cache
uses: actions/cache@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v3
- name: Build
path: |
${{ runner.home }}/.gradle/caches
${{ runner.home }}/.gradle/wrapper
.gradle
node_modules
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
- name: Build with Gradle
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew :patches:buildAndroid clean
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: ./gradlew generateMeta clean
- name: Setup semantic-release
run: npm install
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
uses: cycjimmy/semantic-release-action@v4
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npm exec semantic-release

View File

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

5
.gitignore vendored
View File

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

2
.idea/misc.xml generated
View File

@@ -4,5 +4,5 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</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>

View File

@@ -21,10 +21,11 @@
"@semantic-release/git",
{
"assets": [
"README.md",
"CHANGELOG.md",
"gradle.properties"
],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
"gradle.properties",
"patches.json"
]
}
],
[
@@ -32,17 +33,20 @@
{
"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",
{
"backmergeBranches": [{"from": "main", "to": "dev"}],
"clearWorkspace": true
backmergeBranches: [{"from": "main", "to": "dev"}],
clearWorkspace: true
}
]
]

File diff suppressed because it is too large Load Diff

View File

@@ -6,52 +6,36 @@
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<a href="https://github.com/revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
<picture/>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</a>
<br>
<br>
@@ -64,15 +48,15 @@ This document describes how to contribute to ReVanced Patches.
## 📖 Resources to help you get started
* The [documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs) contains the fundamentals
of ReVanced Patcher and how to use ReVanced Patcher to create patches
* The [documentation](https://github.com/ReVanced/revanced-patches/tree/docs/docs) provides the fundamentals of 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
* [Issues](https://github.com/ReVanced/revanced-patches/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[Feature request issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
[Feature request issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+).
> **Note**
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patches.
@@ -81,11 +65,7 @@ Features can be requested by opening an issue using the
## 🐞 Submitting a bug report
If you encounter a bug while using ReVanced Patches, open an issue using the
[Bug report issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
## 🌐 Submitting translations
You can contribute translations at [translate.revanced.app](https://translate.revanced.app).
[Bug report issue template](https://github.com/ReVanced/revanced-patches/issues/new?assignees=&labels=Bug+report&projects=&template=bug-report.yml&title=bug%3A+).
## 🧑‍⚖️ Guidelines for requesting or contributing patches
@@ -111,18 +91,19 @@ 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
* Malicious patches: Patches that are malicious in nature are not allowed
## 📝 How to contribute
1. Before contributing, it is recommended to open an issue to discuss your change
1. Before contributing, it is recommended to open an issue to discuss your change
with the maintainers of ReVanced Patches. This will help you determine whether your change is acceptable
and whether it is worth your time to implement it
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
3. Commit your changes. In case you are contributing a new patch, make sure to follow the conventions for patches
described in the [ReVanced Patcher documentation](https://github.com/ReVanced/revanced-patcher/tree/main/docs)
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
that your pull request closes in the description of your pull request
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
it will be merged into the `dev` branch and will be included in the next release of ReVanced Patches
❤️ Thank you for considering contributing to ReVanced Patches,
ReVanced

View File

@@ -6,52 +6,36 @@
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
<img height="24px" src="assets/revanced-logo/revanced-logo-round.svg" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<a href="https://github.com/revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
<picture/>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</a>
<br>
<br>
@@ -67,7 +51,7 @@ This repository contains a collection of ReVanced Patches.
## ❓ 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.
## 💪 Features
@@ -77,11 +61,11 @@ Some of the features the patches provide are:
* 🚫 **Block ads**: Say goodbye to ads
***Customize your app**: Personalize the appearance of apps with various layouts and themes
* 🪄 **Add new features**: Extend the functionality of apps with lots of new features
* ⚙️ **Miscellaneous and general purpose**: Rename packages, enable debugging, disable screen capture restrictions,
export activities, etc.
* ⚙️ **Miscellaneous and general purpose**: Rename packages, enable debugging, disable screen capture restrictions,
export activities, etc.
***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
@@ -93,13 +77,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).
### 📃 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
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
ReVanced Patches is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patches as long as you track changes/dates in source files.
Any modifications to ReVanced Patches must also be made available under the GPL,
along with build & install instructions.
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.
Any modifications to ReVanced Patches must also be made available under the GPL along with build & install instructions.

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,3 +1,113 @@
import org.gradle.kotlin.dsl.support.listFilesOrdered
plugins {
alias(libs.plugins.android.library) apply false
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"
}
}
}
}
}

View File

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

9
dummy/build.gradle.kts Normal file
View File

@@ -0,0 +1,9 @@
plugins {
id("java")
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}

View File

@@ -0,0 +1,9 @@
package android.os;
import java.io.File;
public final class Environment {
public static File getExternalStorageDirectory() {
throw new UnsupportedOperationException("Stub");
}
}

View File

@@ -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)
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -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);
}
}

View File

@@ -1,12 +0,0 @@
android {
namespace = "app.revanced.extension"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
compileOnly(libs.annotation)
}

View File

@@ -1,3 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>

View File

@@ -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));
}
}
}

View File

@@ -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)
}

View File

@@ -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);
}
}
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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);
}
}

View File

@@ -1,4 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:boostforreddit:stub"))
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -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);
}
}

View File

@@ -1,17 +0,0 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,6 +0,0 @@
package com.rubenmayayo.reddit.ui.activities;
import android.app.Activity;
public class WebViewActivity extends Activity {
}

View File

@@ -1,3 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,25 +0,0 @@
package app.revanced.extension.messenger.metaai;
import java.util.*;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public class RemoveMetaAIPatch {
private static final Set<Long> loggedIDs = Collections.synchronizedSet(new HashSet<>());
public static boolean overrideBooleanFlag(long id, boolean value) {
try {
if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) {
if (loggedIDs.add(id))
Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false");
return false;
}
} catch (Exception ex) {
Logger.printException(() -> "overrideBooleanFlag failure", ex);
}
return value;
}
}

View File

@@ -1,5 +0,0 @@
android {
defaultConfig {
minSdk = 26
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -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;
}
}

View File

@@ -1,4 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:nunl:stub"))
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -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);
}
}
}

View File

@@ -1,17 +0,0 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,5 +0,0 @@
package nl.nu.performance.api.client.interfaces;
public class Block {
}

View File

@@ -1,7 +0,0 @@
package nl.nu.performance.api.client.objects;
import nl.nu.performance.api.client.interfaces.Block;
public class DividerBlock extends Block {
}

View File

@@ -1,7 +0,0 @@
package nl.nu.performance.api.client.objects;
import nl.nu.performance.api.client.interfaces.Block;
public class DpgBannerBlock extends Block {
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -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");
}
}

View File

@@ -1,7 +0,0 @@
package nl.nu.performance.api.client.objects;
public class StyledText {
public final String getText() {
throw new UnsupportedOperationException("Stub");
}
}

View File

@@ -1,4 +0,0 @@
package nl.nu.performance.api.client.unions;
public interface LinkFlavor {
}

View File

@@ -1,7 +0,0 @@
package nl.nu.performance.api.client.unions;
public class SmallArticleLinkFlavor implements LinkFlavor {
public final Boolean isPartner() {
throw new UnsupportedOperationException("Stub");
}
}

View File

@@ -1,4 +0,0 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:primevideo:stub"))
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -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);
}
}
}

View File

@@ -1,17 +0,0 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,6 +0,0 @@
package com.amazon.avod.fsm;
public final class SimpleTrigger<T> implements Trigger<T> {
public SimpleTrigger(T triggerType) {
}
}

View File

@@ -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) {
}
}

View File

@@ -1,4 +0,0 @@
package com.amazon.avod.fsm;
public interface Trigger<T> {
}

View File

@@ -1,7 +0,0 @@
package com.amazon.avod.media;
public final class TimeSpan {
public long getTotalMilliseconds() {
throw new UnsupportedOperationException();
}
}

View File

@@ -1,7 +0,0 @@
package com.amazon.avod.media.ads;
import com.amazon.avod.media.TimeSpan;
public interface AdBreak {
TimeSpan getDurationExcludingAux();
}

View File

@@ -1,4 +0,0 @@
package com.amazon.avod.media.ads.internal.state;
public abstract class AdBreakState extends AdEnabledPlaybackState {
}

View File

@@ -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();
}
}

View File

@@ -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> {
}

View File

@@ -1,5 +0,0 @@
package com.amazon.avod.media.ads.internal.state;
public enum AdEnabledPlayerTriggerType {
NO_MORE_ADS_SKIP_TRANSITION
}

View File

@@ -1,4 +0,0 @@
package com.amazon.avod.media.ads.internal.state;
public class ServerInsertedAdBreakState extends AdBreakState {
}

View File

@@ -1,7 +0,0 @@
package com.amazon.avod.media.playback;
public interface VideoPlayer {
long getCurrentPosition();
void seekTo(long positionMs);
}

View File

@@ -1,4 +0,0 @@
package com.amazon.avod.media.playback.state;
public interface PlayerStateType {
}

View File

@@ -1,4 +0,0 @@
package com.amazon.avod.media.playback.state.trigger;
public interface PlayerTriggerType {
}

View File

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

View File

@@ -1,3 +0,0 @@
dependencies {
compileOnly(project(":extensions:reddit:stub"))
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -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;
}
}

View File

@@ -1,17 +0,0 @@
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -1 +0,0 @@
<manifest/>

View File

@@ -1,7 +0,0 @@
package com.reddit.domain.model;
public class ILink {
public boolean getPromoted() {
throw new UnsupportedOperationException("Stub");
}
}

View File

@@ -1,3 +0,0 @@
dependencies {
implementation(project(":extensions:shared:library"))
}

View File

@@ -1,21 +0,0 @@
plugins {
alias(libs.plugins.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)
}

View File

@@ -1,256 +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.Dialog;
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 android.util.Pair;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
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(() -> {
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
context,
str("gms_core_dialog_title"), // Title.
str(dialogMessageRef), // Message.
null, // No EditText.
str(positiveButtonTextRef), // OK button text.
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
null, // No Cancel button action.
null, // No Neutral button text.
null, // No Neutral button action.
true // Dismiss dialog when onNeutralClick.
);
Dialog dialog = dialogPair.first;
// 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.
dialog.setCancelable(true);
// Show the dialog
Utils.showDialog(context, dialog);
}, 100);
}
/**
* Injection point.
*/
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.
var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER);
//noinspection TryFinallyCanBeTryWithResources
try {
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());
}
} finally {
if (client != null) client.close();
}
} 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) {
//noinspection ObsoleteSdkInt
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// Android 5.0 does not have battery optimization settings.
return false;
}
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";
}
}

View File

@@ -1,214 +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, and are safe to call even
* if {@link Utils#getContext()} is not available.
*/
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);
}
}
private static boolean shouldLogDebug() {
// If the app is still starting up and the context is not yet set,
// then allow debug logging regardless what the debug setting actually is.
return Utils.context == null || DEBUG.get();
}
private static boolean shouldShowErrorToast() {
return Utils.context != null && DEBUG_TOAST_ON_ERROR.get();
}
private static boolean includeStackTrace() {
return Utils.context != null && DEBUG_STACKTRACE.get();
}
/**
* 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 (shouldLogDebug()) {
logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), 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, includeStackTrace(), 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, includeStackTrace(), shouldShowErrorToast());
}
}

View File

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

View File

@@ -1,212 +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 static app.revanced.extension.shared.Utils.dipToPixels;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.text.Html;
import android.util.Pair;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
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>&nbsp;").append(check.failureReason());
}
reasons.append("</ul>");
var message = Html.fromHtml(
str("revanced_check_environment_failed_message", reasons.toString()),
FROM_HTML_MODE_COMPACT
);
Utils.runOnMainThreadDelayed(() -> {
// Create the custom dialog.
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
activity,
str("revanced_check_environment_failed_title"), // Title.
message, // Message.
null, // No EditText.
str("revanced_check_environment_dialog_open_official_source_button"), // OK button text.
() -> {
// Action for the OK (website) button.
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);
},
null, // No cancel button.
str("revanced_check_environment_dialog_ignore_button"), // Neutral button text.
() -> {
// Neutral button action.
// 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);
},
true // Dismiss dialog when onNeutralClick.
);
// Get the dialog and main layout.
Dialog dialog = dialogPair.first;
LinearLayout mainLayout = dialogPair.second;
// Add icon to the dialog.
ImageView iconView = new ImageView(activity);
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
iconView.setPadding(0, 0, 0, 0);
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
iconParams.gravity = Gravity.CENTER;
mainLayout.addView(iconView, 0); // Add icon at the top.
dialog.setCancelable(false);
// Show the dialog.
Utils.showDialog(activity, dialog, false, new DialogFragmentOnStartAction() {
boolean hasRun;
@Override
public void onStart(Dialog 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;
// Get the button container to access buttons.
LinearLayout buttonContainer = (LinearLayout) mainLayout.getChildAt(mainLayout.getChildCount() - 1);
Button openWebsiteButton;
Button ignoreButton;
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
// Neutral button is the first child (index 0).
ignoreButton = (Button) rowContainer.getChildAt(0);
// OK button is the last child.
openWebsiteButton = (Button) rowContainer.getChildAt(rowContainer.getChildCount() - 1);
} else {
// Multi-row layout: buttons are in separate containers, ordered OK, Cancel, Neutral.
LinearLayout okContainer =
(LinearLayout) buttonContainer.getChildAt(0); // OK is first.
openWebsiteButton = (Button) okContainer.getChildAt(0);
LinearLayout neutralContainer =
(LinearLayout)buttonContainer.getChildAt(buttonContainer.getChildCount() - 1); // Neutral is last.
ignoreButton = (Button) neutralContainer.getChildAt(0);
}
// Initially set buttons to INVISIBLE and disabled.
openWebsiteButton.setVisibility(View.INVISIBLE);
openWebsiteButton.setEnabled(false);
ignoreButton.setVisibility(View.INVISIBLE);
ignoreButton.setEnabled(false);
// Start the countdown for showing and enabling buttons.
getCountdownRunnable(ignoreButton, openWebsiteButton).run();
}
});
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
}
private static Runnable getCountdownRunnable(Button ignoreButton, 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.setVisibility(View.VISIBLE);
openWebsiteButton.setEnabled(true);
}
secondsRemaining--;
Utils.runOnMainThreadDelayed(this, 1000);
} else {
ignoreButton.setVisibility(View.VISIBLE);
ignoreButton.setEnabled(true);
}
}
};
}
}

View File

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

View File

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

View File

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

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