mirror of
https://github.com/ReVanced/revanced-manager.git
synced 2025-05-21 16:36:47 +08:00
Compare commits
345 Commits
v1.21.0-de
...
compose/re
Author | SHA1 | Date | |
---|---|---|---|
4ea8411f3f | |||
9e2a7917ba | |||
88fc639751 | |||
2fc099568e | |||
e641b91e57 | |||
d09e9b79cb | |||
3f5d04a513 | |||
3ad027007f | |||
813ec4dbe2 | |||
9fac5126df | |||
5e03cf2307 | |||
4d522664b6 | |||
8fdd5074a0 | |||
5555373deb | |||
6e57a6e977 | |||
10001b492b | |||
d37ed050bc | |||
ee07621e37 | |||
fc05f95837 | |||
d5c63ead26 | |||
1956982060 | |||
e10e5e4e3f | |||
ede1ab5ed4 | |||
1092188ab0 | |||
f348eba115 | |||
3d234820a3 | |||
cd06d36f68 | |||
242c4570ce | |||
71b73a3b42 | |||
067020f38f | |||
2aef67872d | |||
818dc09aa4 | |||
a762969966 | |||
74338931b8 | |||
0ab424bfdb | |||
fff1a41fee | |||
7644a74648 | |||
9db3bd5b3f | |||
b81bd17fbc | |||
cf3866f892 | |||
5d3a81f4b9 | |||
f9831d4da5 | |||
8a20d8cf9b | |||
49f75f9edd | |||
9916e4da4d | |||
2ec1c0238d | |||
9dc716b1c8 | |||
31fb8b1404 | |||
0685479d53 | |||
20c13ee71c | |||
cf322147d5 | |||
b4c37e6ddc | |||
697386c36c | |||
f6f72387b9 | |||
d201bdc422 | |||
2055400565 | |||
10e7e4b39f | |||
edb4e421e2 | |||
747017a5f9 | |||
e992a99783 | |||
e869db0555 | |||
936a9efd0b | |||
edc11b6e1d | |||
32f2710763 | |||
4257c32bf5 | |||
c18901c35b | |||
f126fe9fa8 | |||
4cbd480e84 | |||
afc72ffd85 | |||
cdc69ea8ff | |||
52a89b1638 | |||
641d518b6e | |||
2eca45d397 | |||
de6fddf405 | |||
45f32040f8 | |||
42ef2ca99d | |||
563f2f37f7 | |||
881b13740a | |||
d39804f7ed | |||
b8a85c4891 | |||
37e9630b9c | |||
40492b67d1 | |||
f655a6e03a | |||
36f864efbb | |||
a995f43b7b | |||
1ee1330e47 | |||
3f4a234915 | |||
032ca39cf6 | |||
6f9a984541 | |||
e6e043f168 | |||
7c7fb7b343 | |||
b26fe30861 | |||
f99cdfe926 | |||
ec0a077539 | |||
a22158d070 | |||
48fe3a707e | |||
8d3d500b7b | |||
d8248cc915 | |||
397a1f8f9c | |||
a0f187354f | |||
1bf004ddee | |||
495100dea9 | |||
e3bd8a8b22 | |||
d987ac6c7a | |||
9883dcd0a7 | |||
d63133189d | |||
39fbb87010 | |||
f197760efd | |||
fa13d4a538 | |||
71b5f539c1 | |||
603a827e45 | |||
47bdc69a43 | |||
1ce56af3b1 | |||
a12c5c583b | |||
5455cf20ab | |||
75fcdb139c | |||
269fa79136 | |||
16fea59605 | |||
c7d183ee8d | |||
413083c58d | |||
1f671aba33 | |||
6bfd9098d6 | |||
4e7d96e91d | |||
ac0a036035 | |||
2a1445d61f | |||
0df39a1136 | |||
634d793839 | |||
afd6c5d6b7 | |||
ab0682cc5c | |||
60ca901ac7 | |||
91ca5be57a | |||
ac47a7eaa4 | |||
ce134224a8 | |||
ca49d3a465 | |||
5d7f9d1387 | |||
8d5d86fea8 | |||
6709505e9e | |||
088de60c91 | |||
ef041869e5 | |||
16cdc7aca4 | |||
39536c0e18 | |||
607d8b67c9 | |||
0b9889ea44 | |||
4acef776b2 | |||
e186dfdaa9 | |||
c0f3d02e6f | |||
36c8f59d6f | |||
f38b31a591 | |||
3232bb10e6 | |||
b7cb6b94f5 | |||
aa6e612fba | |||
d9d7b98409 | |||
4fdd6bbe5f | |||
439f6250f3 | |||
d55abf5dda | |||
a8b9d9316f | |||
55c7800f39 | |||
d9eb1c42bc | |||
8cd617e32d | |||
a17a05995a | |||
de4e616dcc | |||
12b00e5c8d | |||
9cab91959e | |||
bd9778a3d1 | |||
62a5fce66c | |||
2bd84636d6 | |||
ac561e7aca | |||
59daceef99 | |||
1dc41badd9 | |||
1a83315424 | |||
3c5776214f | |||
5fff0a2923 | |||
8df7f2992d | |||
7741394c9c | |||
25bd91debc | |||
7fe4724e10 | |||
123ae37524 | |||
172604fcdb | |||
7d887c73e8 | |||
6abaac25d9 | |||
cc897840e2 | |||
757840b76f | |||
50e8d1f8f4 | |||
65f8d38c59 | |||
e70c10adbd | |||
64ec73d821 | |||
32e8a37f33 | |||
5290713504 | |||
18cfb56b45 | |||
4b12ae1531 | |||
9df98edca5 | |||
c3af6acb2c | |||
7ba00cafd9 | |||
5aefb3bc59 | |||
212e55ffd8 | |||
bf54d38c91 | |||
cee2240cdc | |||
4c1ad868a9 | |||
f5b3b29d6d | |||
8f5449527d | |||
8f6d720454 | |||
56a4a7043d | |||
5762859906 | |||
608bac6854 | |||
723f9cd98c | |||
abf4d91703 | |||
d8392ad3eb | |||
39caad18a5 | |||
6437f7bb65 | |||
e232044157 | |||
f78b56ef0a | |||
ca3c9af3b8 | |||
b8b2e74151 | |||
1f8341ac42 | |||
0964f15475 | |||
63fd7957c6 | |||
65377ffd9e | |||
f79320c013 | |||
cf71ea26ec | |||
ee96c37c20 | |||
a86923aee1 | |||
e0f8d06152 | |||
4cb4ce298a | |||
36de61a57f | |||
6f2ca5bb89 | |||
940885768d | |||
fc577b4c3e | |||
bf10af2ae2 | |||
b4dfcf1bb4 | |||
0b0ba21852 | |||
42e0346e25 | |||
212db84d0b | |||
e6eb8accf2 | |||
8bd73c3afa | |||
5369a25fa2 | |||
eeae46a415 | |||
c0badbe96b | |||
2bb51c136a | |||
3cfa4ea6d6 | |||
f01adf5eb0 | |||
a0b92554e9 | |||
ac4c7e06e7 | |||
0f9a6f4340 | |||
9586a9c0dd | |||
f6563b265b | |||
7aea9473de | |||
3f059d7748 | |||
7e3c31c4b2 | |||
1707a9690a | |||
379ce917a9 | |||
299aaa2b68 | |||
5cf5e87fa8 | |||
55f22562eb | |||
272d911464 | |||
6beb34baa8 | |||
61de0b67fa | |||
aec8cec9b8 | |||
83b9573b52 | |||
21d99a1f24 | |||
1331479072 | |||
b472a36a9a | |||
3238fcdae7 | |||
cd2587b1fd | |||
879884a9fa | |||
5d3b963682 | |||
955e7a4f1c | |||
d2dcd4209d | |||
6299ff5b48 | |||
94a4dbaba1 | |||
c36deea045 | |||
7030d43aa5 | |||
aa02e9f8cf | |||
37e177b56e | |||
453f4da8ec | |||
400163b820 | |||
4ae9904c8a | |||
fe5e191cb5 | |||
d9d83df9de | |||
8dd8f88d2b | |||
01fd4c8ffa | |||
7ac3bb74e0 | |||
3b65cd0edc | |||
a9606728bf | |||
4d4f1a242c | |||
6b7143dd8f | |||
7e4ee00cb2 | |||
4868c45b43 | |||
81f485da6b | |||
18cbe51e6b | |||
149c8cc8b2 | |||
0dccb8c27b | |||
4302ea8832 | |||
1eac42dab8 | |||
9dd74f1f22 | |||
923ce74735 | |||
2d9f9adfee | |||
9a55e51a3a | |||
5681c917c5 | |||
6309e8bdf5 | |||
535efa3d73 | |||
b8a51d32f5 | |||
919b6b7014 | |||
971277ed39 | |||
7ce4de7a8b | |||
9591f4e14f | |||
27426b1390 | |||
fcb75dd780 | |||
1be9c9c1bd | |||
e088d053ab | |||
ffa8d9c063 | |||
7a5596a281 | |||
9f46f74357 | |||
36c4e2dfe0 | |||
5cb31dbe9d | |||
399fc98dec | |||
c22371e0c5 | |||
a4842c078b | |||
c332760786 | |||
ea4247c688 | |||
fec8c0cc14 | |||
9b585c73fb | |||
c695fa525f | |||
93f3e27d48 | |||
52ab7937bd | |||
762bfa8514 | |||
ca20996b62 | |||
ad14818de8 | |||
32839656f8 | |||
a48faad17a | |||
40487923f9 | |||
f1656c6d1e | |||
4c3dbbd8d5 | |||
4088ed747e | |||
bca8df8efd | |||
54f0a69596 | |||
9065c0d260 | |||
cb0150a0f9 | |||
ec0f7e3f7a | |||
e5d898f025 | |||
52bdb1cd6a | |||
49f9dfcf95 | |||
9536cdcae1 | |||
57e2632f38 | |||
b372f7ee84 | |||
70e8253b63 |
41
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
41
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: 🐞 Bug report
|
name: 🐞 Bug report
|
||||||
description: Report a bug or an issue.
|
description: Report a bug or an issue.
|
||||||
title: "bug: "
|
title: 'bug: '
|
||||||
labels: ["Bug report"]
|
labels: ['Bug report']
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@ -70,7 +70,7 @@ body:
|
|||||||
|
|
||||||
Before creating a new bug report, please keep the following in mind:
|
Before creating a new bug report, please keep the following in mind:
|
||||||
|
|
||||||
- **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-manager/labels/Bug%20report).
|
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-manager/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-manager/blob/main/CONTRIBUTING.md).
|
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@ -80,47 +80,30 @@ body:
|
|||||||
- Describe your bug in detail
|
- Describe your bug in detail
|
||||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||||
- Add images and videos if possible
|
- Add images and videos if possible
|
||||||
- List used patches if applicable
|
- List used patches, downloader and settings if applicable
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Version of ReVanced Manager and version & name of app you are patching
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: Installation method
|
|
||||||
options:
|
|
||||||
- Regular
|
|
||||||
- Mount
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: ReVanced Manager logs
|
|
||||||
description: Export logs in ReVanced Manager settings.
|
|
||||||
render: shell
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Patch logs
|
label: Patch logs
|
||||||
description: Export logs in "Patcher" screen.
|
description: Patch logs can be exported by clicking on the "Logs" button in the "Patcher" screen, when patching finishes.
|
||||||
render: shell
|
render: shell
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Debug logs
|
||||||
|
description: Debug logs can be exported by clicking on "Export debug logs" in "Settings" > "Advanced".
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: true
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: acknowledgements
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your bug report will be closed if you don't follow the checklist below.
|
description: Your bug report will be closed if you don't follow the checklist below.
|
||||||
options:
|
options:
|
||||||
- label: This issue is not a duplicate of an existing bug report.
|
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||||
required: true
|
required: true
|
||||||
- label: I have chosen an appropriate title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
- label: All requested information has been provided properly.
|
- label: All requested information has been provided properly.
|
||||||
required: true
|
required: true
|
||||||
- label: The bug is only related to ReVanced Manager
|
- label: The bug is only related to ReVanced Manager.
|
||||||
required: true
|
required: true
|
||||||
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -2,4 +2,4 @@ blank_issues_enabled: false
|
|||||||
contact_links:
|
contact_links:
|
||||||
- name: 🗨 Discussions
|
- name: 🗨 Discussions
|
||||||
url: https://github.com/revanced/revanced-suggestions/discussions
|
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||||
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
|
about: Have something unspecific to ReVanced Manager in mind? Search for or start a new discussion!
|
15
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
15
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -1,7 +1,3 @@
|
|||||||
name: ⭐ Feature request
|
|
||||||
description: Create a detailed request for a new feature.
|
|
||||||
title: "feat: "
|
|
||||||
labels: ["Feature request"]
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@ -70,7 +66,7 @@ body:
|
|||||||
|
|
||||||
Before creating a new feature request, please keep the following in mind:
|
Before creating a new feature request, please keep the following in mind:
|
||||||
|
|
||||||
- **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-manager//labels/Feature%20request).
|
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-manager/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-manager/blob/main/CONTRIBUTING.md).
|
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-manager/blob/main/CONTRIBUTING.md).
|
||||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@ -79,13 +75,12 @@ body:
|
|||||||
description: |
|
description: |
|
||||||
- Describe your feature in detail
|
- Describe your feature in detail
|
||||||
- Add images, videos, links, examples, references, etc. if possible
|
- Add images, videos, links, examples, references, etc. if possible
|
||||||
- Add the target application name in case you request a new patch
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Motivation
|
label: Motivation
|
||||||
description: |
|
description: |
|
||||||
A strong motivation is necessary for a feature request to be considered.
|
A strong motivation is necessary for a feature request to be considered.
|
||||||
|
|
||||||
- Why should this feature be implemented?
|
- Why should this feature be implemented?
|
||||||
- What is the explicit use case?
|
- What is the explicit use case?
|
||||||
- What are the benefits?
|
- What are the benefits?
|
||||||
@ -98,9 +93,11 @@ body:
|
|||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your feature request will be closed if you don't follow the checklist below.
|
description: Your feature request will be closed if you don't follow the checklist below.
|
||||||
options:
|
options:
|
||||||
- label: This issue is not a duplicate of an existing feature request.
|
- label: I have checked all open and closed feature requests and this is not a duplicate
|
||||||
required: true
|
required: true
|
||||||
- label: I have chosen an appropriate title.
|
- label: I have chosen an appropriate title.
|
||||||
required: true
|
required: true
|
||||||
- label: The feature request is only related to ReVanced Manager
|
- label: All requested information has been provided properly.
|
||||||
|
required: true
|
||||||
|
- label: The feature request is only related to ReVanced Manager.
|
||||||
required: true
|
required: true
|
||||||
|
38
.github/dependabot.yml
vendored
38
.github/dependabot.yml
vendored
@ -1,38 +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
|
|
||||||
|
|
||||||
# ReVanced Manager Flutter
|
|
||||||
- package-ecosystem: pub
|
|
||||||
labels: []
|
|
||||||
directory: /
|
|
||||||
target-branch: dev
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
||||||
|
|
||||||
- package-ecosystem: gradle
|
|
||||||
labels: []
|
|
||||||
directory: /
|
|
||||||
target-branch: dev
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
||||||
|
|
||||||
# ReVanced Manager Compose
|
|
||||||
- package-ecosystem: gradle
|
|
||||||
labels: [ "ReVanced Manager Compose" ]
|
|
||||||
directory: /
|
|
||||||
target-branch: compose-dev
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
103
.github/workflows/build_pull_request.yml
vendored
103
.github/workflows/build_pull_request.yml
vendored
@ -2,115 +2,24 @@ name: Build pull request
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
pull_request:
|
||||||
# Enable or disable cache
|
branches:
|
||||||
flutter-cache:
|
- dev
|
||||||
description: Cache
|
|
||||||
type: boolean
|
|
||||||
default: true
|
|
||||||
|
|
||||||
# Select app flavour
|
|
||||||
app-flavour:
|
|
||||||
description: App flavour
|
|
||||||
default: 'release'
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- release
|
|
||||||
- debug
|
|
||||||
- profile
|
|
||||||
|
|
||||||
# Select pull request
|
|
||||||
pr-number:
|
|
||||||
description: PR number (Without hashtag)
|
|
||||||
required: true
|
|
||||||
|
|
||||||
run-name: "Build pull request ${{ inputs.pr-number }}"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
release:
|
||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Setup pull request
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
gh repo clone "${{ github.repository }}"
|
|
||||||
cd revanced-manager
|
|
||||||
gh repo set-default "${{ github.repository }}"
|
|
||||||
gh pr checkout "${{ inputs.pr-number }}"
|
|
||||||
|
|
||||||
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
- name: Setup Java
|
|
||||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
cache: ${{ inputs.flutter-cache }}
|
|
||||||
|
|
||||||
- name: Get dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Generate translations
|
|
||||||
run: dart run slang
|
|
||||||
|
|
||||||
- name: Generate code files
|
|
||||||
run: dart run build_runner build --delete-conflicting-outputs
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
continue-on-error: true
|
env:
|
||||||
id: flutter-build
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: ./gradlew build --no-daemon
|
||||||
flutter build apk --"${{ inputs.app-flavour }}";
|
|
||||||
|
|
||||||
- name: Prepare comment
|
|
||||||
run: |
|
|
||||||
if [[ "${{ steps.flutter-build.outcome }}" == "success" ]]; then
|
|
||||||
echo "MESSAGE=✅ Succeeded build on ${{ env.COMMIT_HASH }}." >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "MESSAGE=🚫 Failed build on ${{ env.COMMIT_HASH }}." >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Comment on pull request #${{ inputs.pr-number }}"
|
|
||||||
uses: thollander/actions-comment-pull-request@v2
|
|
||||||
with:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
pr_number: ${{ inputs.pr-number }}
|
|
||||||
mode: recreate
|
|
||||||
message: |
|
|
||||||
## ⚒️ Build status
|
|
||||||
|
|
||||||
${{ env.MESSAGE }}
|
|
||||||
|
|
||||||
Details: [${{ github.run_id }}](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})!
|
|
||||||
|
|
||||||
### ⚙️ Workflow run configuration
|
|
||||||
|
|
||||||
- Flutter cache: ${{ inputs.flutter-cache }}
|
|
||||||
- App flavor: ${{ inputs.app-flavour }}
|
|
||||||
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
if-no-files-found: error
|
|
||||||
name: revanced-manager-(${{ env.COMMIT_HASH }}-${{ inputs.pr-number }}-${{ inputs.app-flavour }})
|
|
||||||
path: |
|
|
||||||
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk
|
|
||||||
build/app/outputs/flutter-apk/app-${{ inputs.app-flavour }}.apk.sha1
|
|
||||||
|
4
.github/workflows/open_pull_request.yml
vendored
4
.github/workflows/open_pull_request.yml
vendored
@ -22,7 +22,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
destination_branch: 'main'
|
destination_branch: 'main'
|
||||||
pr_title: 'chore: ${{ env.MESSAGE }}'
|
pr_title: 'chore: ${{ env.MESSAGE }}'
|
||||||
pr_body: |
|
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
|
||||||
This pull request will ${{ env.MESSAGE }}.
|
|
||||||
pr_draft: true
|
pr_draft: true
|
||||||
github_token: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
|
||||||
|
65
.github/workflows/release.yml
vendored
65
.github/workflows/release.yml
vendored
@ -6,16 +6,14 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- dev
|
- dev
|
||||||
paths:
|
|
||||||
- ".github/workflows/release.yml"
|
|
||||||
- "android/**"
|
|
||||||
- "assets/**"
|
|
||||||
- "lib/**"
|
|
||||||
- "pubspec.yaml"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Release
|
name: Release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -24,7 +22,18 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
run: echo "JAVA_HOME=$JAVA_HOME_17_X64" >> $GITHUB_ENV
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'temurin'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: ./gradlew assembleRelease
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@ -32,30 +41,24 @@ jobs:
|
|||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Get dependencies
|
- name: Setup keystore
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Generate translations
|
|
||||||
run: dart run slang
|
|
||||||
|
|
||||||
- name: Generate code files
|
|
||||||
run: dart run build_runner build --delete-conflicting-outputs
|
|
||||||
|
|
||||||
- name: Release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
|
||||||
signingKey: "keystore.jks"
|
|
||||||
keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
|
||||||
keyAlias: ${{ secrets.SIGNING_KEY_ALIAS }}
|
|
||||||
keyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.SIGNING_KEYSTORE }}" | base64 --decode > android/app/keystore.jks
|
echo "${{ secrets.KEYSTORE }}" | base64 --decode > "app/keystore.jks"
|
||||||
npx semantic-release
|
|
||||||
|
- name: Semantic Release
|
||||||
|
uses: cycjimmy/semantic-release-action@v4
|
||||||
|
id: semantic
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||||
|
KEYSTORE_ENTRY_ALIAS: ${{ secrets.KEYSTORE_ENTRY_ALIAS }}
|
||||||
|
KEYSTORE_ENTRY_PASSWORD: ${{ secrets.KEYSTORE_ENTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Attest
|
||||||
|
if: steps.semantic.outputs.new_release_published == 'true'
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: build/app/outputs/apk/release/revanced-manager-*.apk
|
63
.github/workflows/sync_crowdin.yml
vendored
63
.github/workflows/sync_crowdin.yml
vendored
@ -1,63 +0,0 @@
|
|||||||
name: Sync Crowdin
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: 00 12 * * 1
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- assets/i18n/*.json
|
|
||||||
- assets/i18n/*.dart
|
|
||||||
- .github/workflows/sync_crowdin.yml
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
name: Sync
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Dart
|
|
||||||
uses: dart-lang/setup-dart@v1
|
|
||||||
|
|
||||||
- name: Sync translations from Crowdin
|
|
||||||
uses: crowdin/github-action@v1
|
|
||||||
with:
|
|
||||||
config: crowdin.yml
|
|
||||||
upload_sources: true
|
|
||||||
upload_translations: false
|
|
||||||
download_translations: true
|
|
||||||
localization_branch_name: feat/translations
|
|
||||||
create_pull_request: true
|
|
||||||
pull_request_title: "chore: Sync translations"
|
|
||||||
pull_request_body: "Sync translations from [crowdin.com/project/revanced](https://crowdin.com/project/revanced)"
|
|
||||||
pull_request_base_branch_name: "dev"
|
|
||||||
commit_message: "chore: Sync translations"
|
|
||||||
github_user_name: revanced-bot
|
|
||||||
github_user_email: github@revanced.app
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
||||||
|
|
||||||
- name: Remove empty values from JSON
|
|
||||||
run: |
|
|
||||||
cd assets/i18n
|
|
||||||
sudo chmod 777 *.json
|
|
||||||
dart nuke.dart >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Validation of Translation Strings
|
|
||||||
run: |
|
|
||||||
dart analyze lib/gen/strings.g.dart
|
|
||||||
|
|
||||||
- name: Commit translations
|
|
||||||
run: |
|
|
||||||
git config user.name revanced-bot
|
|
||||||
git config user.email github@revanced.app
|
|
||||||
sudo chown -R $USER:$USER .git
|
|
||||||
git add assets/i18n/*.json
|
|
||||||
git commit -m "chore: Remove empty values from JSON" assets/i18n/*.json
|
|
||||||
git push origin HEAD:feat/translations
|
|
2
.github/workflows/update_documentation.yml
vendored
2
.github/workflows/update_documentation.yml
vendored
@ -16,4 +16,4 @@ jobs:
|
|||||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||||
repository: revanced/revanced-documentation
|
repository: revanced/revanced-documentation
|
||||||
event-type: update-documentation
|
event-type: update-documentation
|
||||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
58
.gitignore
vendored
58
.gitignore
vendored
@ -1,50 +1,12 @@
|
|||||||
# See https://www.dartlang.org/guides/libraries/private-files
|
*.iml
|
||||||
|
.gradle
|
||||||
# Files and directories created by pub
|
/local.properties
|
||||||
.dart_tool/
|
/.idea
|
||||||
.packages
|
.DS_Store
|
||||||
build/
|
/build
|
||||||
# If you're building an application, you may want to check-in your pubspec.lock
|
/captures
|
||||||
# pubspec.lock
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
# Directory created by dartdoc
|
|
||||||
# If you don't generate documentation locally you can remove this line.
|
|
||||||
doc/api/
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env*
|
|
||||||
|
|
||||||
# Avoid committing generated Javascript files:
|
|
||||||
*.dart.js
|
|
||||||
*.info.json # Produced by the --dump-info flag.
|
|
||||||
*.js # When generated by dart2js. Don't specify *.js if your
|
|
||||||
# project includes source files written in JavaScript.
|
|
||||||
*.js_
|
|
||||||
*.js.deps
|
|
||||||
*.js.map
|
|
||||||
|
|
||||||
.flutter-plugins
|
|
||||||
.flutter-plugins-dependencies
|
|
||||||
|
|
||||||
# Generated Builder file
|
|
||||||
**/*.g.dart
|
|
||||||
**/*.locator.dart
|
|
||||||
**/*.router.dart
|
|
||||||
|
|
||||||
flutter_*.png
|
|
||||||
|
|
||||||
#### Custom
|
|
||||||
|
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
# IntelliJ related
|
.kotlin/
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Node Dependency directories
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# FVM
|
|
||||||
.fvm
|
|
||||||
|
45
.metadata
45
.metadata
@ -1,45 +0,0 @@
|
|||||||
# This file tracks properties of this Flutter project.
|
|
||||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
|
||||||
#
|
|
||||||
# This file should be version controlled.
|
|
||||||
|
|
||||||
version:
|
|
||||||
revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
channel: stable
|
|
||||||
|
|
||||||
project_type: app
|
|
||||||
|
|
||||||
# Tracks metadata for the flutter migrate command
|
|
||||||
migration:
|
|
||||||
platforms:
|
|
||||||
- platform: root
|
|
||||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
- platform: android
|
|
||||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
- platform: ios
|
|
||||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
- platform: linux
|
|
||||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
- platform: macos
|
|
||||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
- platform: web
|
|
||||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
- platform: windows
|
|
||||||
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
|
|
||||||
|
|
||||||
# User provided section
|
|
||||||
|
|
||||||
# List of Local paths (relative to this file) that should be
|
|
||||||
# ignored by the migrate tool.
|
|
||||||
#
|
|
||||||
# Files that are not part of the templates will be ignored by default.
|
|
||||||
unmanaged_files:
|
|
||||||
- 'lib/main.dart'
|
|
||||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
|
64
.releaserc
64
.releaserc
@ -1,64 +0,0 @@
|
|||||||
{
|
|
||||||
"branches": [
|
|
||||||
"main",
|
|
||||||
{
|
|
||||||
"name": "dev",
|
|
||||||
"prerelease": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
[
|
|
||||||
"@semantic-release/commit-analyzer", {
|
|
||||||
"releaseRules": [
|
|
||||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@semantic-release/changelog",
|
|
||||||
"@semantic-release/release-notes-generator",
|
|
||||||
[
|
|
||||||
"@droidsolutions-oss/semantic-release-update-file",
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"path": ["pubspec.yaml"],
|
|
||||||
"type": "flutter",
|
|
||||||
"branches": ["main", "dev"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/exec",
|
|
||||||
{
|
|
||||||
"prepareCmd": "flutter build apk"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/git",
|
|
||||||
{
|
|
||||||
"assets": [
|
|
||||||
"pubspec.yaml"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/github",
|
|
||||||
{
|
|
||||||
"assets": [
|
|
||||||
{
|
|
||||||
"path": "build/app/outputs/apk/release/revanced-manager*.apk"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"successComment": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@saithodev/semantic-release-backmerge",
|
|
||||||
{
|
|
||||||
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
|
||||||
"clearWorkspace": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
91
.vscode/tasks.json
vendored
91
.vscode/tasks.json
vendored
@ -1,91 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "Generate (Builder)",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "flutter packages pub run build_runner build --delete-conflicting-outputs",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Build (Android)",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "flutter build apk",
|
|
||||||
"problemMatcher": [],
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Install (Android)",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "adb install build\\app\\outputs\\flutter-apk\\app-release.apk",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Clean (Flutter)",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "flutter clean && flutter pub get",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Clean (Builder)",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "flutter packages pub run build_runner clean",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Build all (Android)",
|
|
||||||
"dependsOrder": "sequence",
|
|
||||||
"dependsOn": [
|
|
||||||
"Generate (Builder)",
|
|
||||||
"Build (Android)"
|
|
||||||
],
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Clean all",
|
|
||||||
"dependsOrder": "sequence",
|
|
||||||
"dependsOn": [
|
|
||||||
"Clean (Flutter)",
|
|
||||||
"Clean (Builder)"
|
|
||||||
],
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Clean all & Build all (Android)",
|
|
||||||
"dependsOrder": "sequence",
|
|
||||||
"dependsOn": [
|
|
||||||
"Clean all",
|
|
||||||
"Build all (Android)"
|
|
||||||
],
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Clean all & Install (Android)",
|
|
||||||
"dependsOrder": "sequence",
|
|
||||||
"dependsOn": [
|
|
||||||
"Clean all",
|
|
||||||
"Build all (Android)",
|
|
||||||
"Install (Android)",
|
|
||||||
],
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Build & Install (Android)",
|
|
||||||
"dependsOrder": "sequence",
|
|
||||||
"dependsOn": [
|
|
||||||
"Build (Android)",
|
|
||||||
"Install (Android)"
|
|
||||||
],
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Validate translations",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "flutter pub run flutter_i18n diff en.json pt.json",
|
|
||||||
"problemMatcher": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -96,7 +96,7 @@ If you encounter a bug while using ReVanced Manager, open an issue using the
|
|||||||
|
|
||||||
## 🤚 I want to contribute but don't know how to code
|
## 🤚 I want to contribute but don't know how to code
|
||||||
|
|
||||||
Even if you don't know how to code, you can still contribute by
|
Even if you don't know how to code, you can still contribute by
|
||||||
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
|
translating ReVanced Manager on [Crowdin](https://translate.revanced.app/).
|
||||||
|
|
||||||
❤️ Thank you for considering contributing to ReVanced Manager,
|
❤️ Thank you for considering contributing to ReVanced Manager,
|
||||||
|
@ -73,14 +73,13 @@ ReVanced Manager is an application that uses [ReVanced Patcher](https://github.c
|
|||||||
|
|
||||||
Some of the features ReVanced Manager provides are:
|
Some of the features ReVanced Manager provides are:
|
||||||
|
|
||||||
- 💉 **Patch apps**: Apply any patch of your choice to Android apps
|
- ⬇️ **Download**: Automatically download apps using the ReVanced Manager downloader plugin system
|
||||||
- 📱 **Portable**: ReVanced Patcher that fits in your pocket
|
- 💉 **Patch**: Select and apply patches to any Android app
|
||||||
- 🤗 **Simple UI**: Quickly understand the ins and outs of ReVanced Manager
|
- 🛠️ **Customize**: Manage patches, apps, signing, themes, updates, and many more settings
|
||||||
- 🛠️ **Customization**: Configurable API, custom sources, language, signing keystore, theme and more
|
|
||||||
|
|
||||||
## 🔽 Download
|
## 🔽 Download
|
||||||
|
|
||||||
You can download the most recent version of ReVanced Manager at [revanced.app/download](https://revanced.app/download) or from [GitHub releases](https://github.com/ReVanced/revanced-manager/releases).
|
You can download the most recent version of ReVanced Manager at [revanced.app/download](https://revanced.app/download) or from [GitHub releases](https://github.com/ReVanced/revanced-manager/releases/latest).
|
||||||
Learn how to use ReVanced Manager by following the [documentation](/docs).
|
Learn how to use ReVanced Manager by following the [documentation](/docs).
|
||||||
|
|
||||||
## 📚 Everything else
|
## 📚 Everything else
|
||||||
|
77
SECURITY.md
77
SECURITY.md
@ -1,77 +0,0 @@
|
|||||||
<p align="center">
|
|
||||||
<picture>
|
|
||||||
<source
|
|
||||||
width="256px"
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
width="256px"
|
|
||||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
|
||||||
>
|
|
||||||
</picture>
|
|
||||||
<br>
|
|
||||||
<a href="https://revanced.app/">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
|
||||||
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
|
||||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="http://revanced.app/discord">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://reddit.com/r/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://t.me/app_revanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://x.com/revancedapp">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<a href="https://www.youtube.com/@ReVanced">
|
|
||||||
<picture>
|
|
||||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
Continuing the legacy of Vanced
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# 🔒 Security Policy
|
|
||||||
|
|
||||||
This document describes how to report security vulnerabilities for ReVanced Manager.
|
|
||||||
|
|
||||||
## 🚨 Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please open an issue in our [advisory tracker](https://github.com/ReVanced/revanced-manager/security/advisories/new) or reach out privately to us on [Discord](https://discord.gg/revanced).
|
|
||||||
|
|
||||||
If a vulnerability is confirmed and accepted, you can join our [Discord](https://discord.gg/revanced) server to receive a special contributor role.
|
|
||||||
|
|
||||||
### ⏳ Supported Versions
|
|
||||||
|
|
||||||
| Version | Branch | Supported |
|
|
||||||
| --------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------ |
|
|
||||||
|  | main | :white_check_mark: |
|
|
||||||
|  | dev | :white_check_mark: |
|
|
||||||
|  | compose-dev | :white_check_mark: |
|
|
@ -1,161 +0,0 @@
|
|||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
|
||||||
# check for errors, warnings, and lints.
|
|
||||||
#
|
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
|
|
||||||
# The following line activates a set of recommended lints for Flutter apps,
|
|
||||||
# packages, and plugins designed to encourage good coding practices.
|
|
||||||
include: package:flutter_lints/flutter.yaml
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
exclude:
|
|
||||||
- lib/app/app.locator.dart
|
|
||||||
- lib/app/app.router.dart
|
|
||||||
- lib/models/patch.g.dart
|
|
||||||
- lib/models/patched_application.g.dart
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
- always_declare_return_types
|
|
||||||
- require_trailing_commas
|
|
||||||
- always_put_control_body_on_new_line
|
|
||||||
- always_use_package_imports # we do this commonly
|
|
||||||
- annotate_overrides
|
|
||||||
- avoid_bool_literals_in_conditional_expressions
|
|
||||||
- avoid_double_and_int_checks
|
|
||||||
- avoid_empty_else
|
|
||||||
- avoid_equals_and_hash_code_on_mutable_classes
|
|
||||||
- avoid_escaping_inner_quotes
|
|
||||||
- avoid_field_initializers_in_const_classes
|
|
||||||
- avoid_function_literals_in_foreach_calls
|
|
||||||
- avoid_implementing_value_types
|
|
||||||
- avoid_init_to_null
|
|
||||||
- avoid_js_rounded_ints
|
|
||||||
- avoid_null_checks_in_equality_operators
|
|
||||||
- avoid_print
|
|
||||||
- avoid_redundant_argument_values
|
|
||||||
- avoid_relative_lib_imports
|
|
||||||
- avoid_renaming_method_parameters
|
|
||||||
- avoid_return_types_on_setters
|
|
||||||
- avoid_returning_null_for_void
|
|
||||||
- avoid_setters_without_getters
|
|
||||||
- avoid_shadowing_type_parameters
|
|
||||||
- avoid_single_cascade_in_expression_statements
|
|
||||||
- avoid_type_to_string
|
|
||||||
- avoid_types_as_parameter_names
|
|
||||||
- avoid_unnecessary_containers
|
|
||||||
- avoid_void_async
|
|
||||||
- avoid_web_libraries_in_flutter # we use web libraries in web-specific code, and our tests prevent us from using them elsewhere
|
|
||||||
- await_only_futures
|
|
||||||
- camel_case_extensions
|
|
||||||
- camel_case_types
|
|
||||||
- cancel_subscriptions
|
|
||||||
- cast_nullable_to_non_nullable
|
|
||||||
- close_sinks # not reliable enough
|
|
||||||
- control_flow_in_finally
|
|
||||||
- curly_braces_in_flow_control_structures
|
|
||||||
- depend_on_referenced_packages
|
|
||||||
- deprecated_consistency
|
|
||||||
- directives_ordering
|
|
||||||
- empty_catches
|
|
||||||
- empty_constructor_bodies
|
|
||||||
- empty_statements
|
|
||||||
- eol_at_end_of_file
|
|
||||||
- exhaustive_cases
|
|
||||||
- file_names
|
|
||||||
- flutter_style_todos
|
|
||||||
- hash_and_equals
|
|
||||||
- implementation_imports
|
|
||||||
- collection_methods_unrelated_type
|
|
||||||
- leading_newlines_in_multiline_strings
|
|
||||||
- library_names
|
|
||||||
- library_prefixes
|
|
||||||
- library_private_types_in_public_api
|
|
||||||
- missing_whitespace_between_adjacent_strings
|
|
||||||
- no_adjacent_strings_in_list
|
|
||||||
- no_duplicate_case_values
|
|
||||||
- no_logic_in_create_state
|
|
||||||
- non_constant_identifier_names
|
|
||||||
- noop_primitive_operations
|
|
||||||
- null_check_on_nullable_type_parameter
|
|
||||||
- null_closures
|
|
||||||
- overridden_fields
|
|
||||||
- package_api_docs
|
|
||||||
- package_names
|
|
||||||
- package_prefixed_library_names
|
|
||||||
- prefer_adjacent_string_concatenation
|
|
||||||
- prefer_asserts_in_initializer_lists
|
|
||||||
- prefer_collection_literals
|
|
||||||
- prefer_conditional_assignment
|
|
||||||
- prefer_const_constructors
|
|
||||||
- prefer_const_constructors_in_immutables
|
|
||||||
- prefer_const_declarations
|
|
||||||
- prefer_const_literals_to_create_immutables
|
|
||||||
- prefer_contains
|
|
||||||
- prefer_final_fields
|
|
||||||
- prefer_final_in_for_each
|
|
||||||
- prefer_final_locals
|
|
||||||
- prefer_for_elements_to_map_fromIterable
|
|
||||||
- prefer_foreach
|
|
||||||
- prefer_function_declarations_over_variables
|
|
||||||
- prefer_generic_function_type_aliases
|
|
||||||
- prefer_if_elements_to_conditional_expressions
|
|
||||||
- prefer_if_null_operators
|
|
||||||
- prefer_initializing_formals
|
|
||||||
- prefer_inlined_adds
|
|
||||||
- prefer_interpolation_to_compose_strings
|
|
||||||
- prefer_is_empty
|
|
||||||
- prefer_is_not_empty
|
|
||||||
- prefer_is_not_operator
|
|
||||||
- prefer_iterable_whereType
|
|
||||||
- prefer_mixin # Has false positives, see https://github.com/dart-lang/linter/issues/3018
|
|
||||||
- prefer_null_aware_method_calls # "call()" is confusing to people new to the language since it's not documented anywhere
|
|
||||||
- prefer_null_aware_operators
|
|
||||||
- prefer_single_quotes
|
|
||||||
- prefer_spread_collections
|
|
||||||
- prefer_typing_uninitialized_variables
|
|
||||||
- prefer_void_to_null
|
|
||||||
- provide_deprecation_message
|
|
||||||
- recursive_getters
|
|
||||||
- sized_box_for_whitespace
|
|
||||||
- slash_for_doc_comments
|
|
||||||
- sort_child_properties_last
|
|
||||||
- sort_constructors_first
|
|
||||||
- sort_pub_dependencies
|
|
||||||
- sort_unnamed_constructors_first
|
|
||||||
- test_types_in_equals
|
|
||||||
- throw_in_finally
|
|
||||||
- tighten_type_of_initializing_formals
|
|
||||||
- type_init_formals
|
|
||||||
- unnecessary_brace_in_string_interps
|
|
||||||
- unnecessary_const
|
|
||||||
- unnecessary_getters_setters
|
|
||||||
- unnecessary_new
|
|
||||||
- unnecessary_null_aware_assignments
|
|
||||||
- unnecessary_null_checks
|
|
||||||
- unnecessary_null_in_if_null_operators
|
|
||||||
- unnecessary_nullable_for_final_variable_declarations
|
|
||||||
- unnecessary_overrides
|
|
||||||
- unnecessary_parenthesis
|
|
||||||
- unnecessary_statements
|
|
||||||
- unnecessary_string_escapes
|
|
||||||
- unnecessary_string_interpolations
|
|
||||||
- unnecessary_this
|
|
||||||
- unrelated_type_equality_checks
|
|
||||||
- unsafe_html
|
|
||||||
- use_build_context_synchronously
|
|
||||||
- use_full_hex_values_for_flutter_colors
|
|
||||||
- use_function_type_syntax_for_parameters
|
|
||||||
- use_if_null_to_convert_nulls_to_bools
|
|
||||||
- use_is_even_rather_than_modulo
|
|
||||||
- use_key_in_widget_constructors
|
|
||||||
- use_late_for_private_fields_and_variables
|
|
||||||
- use_named_constants
|
|
||||||
- use_raw_strings
|
|
||||||
- use_rethrow_when_possible
|
|
||||||
- use_setters_to_change_properties
|
|
||||||
- use_test_throws_matchers
|
|
||||||
- valid_regexps
|
|
||||||
- void_checks
|
|
13
android/.gitignore
vendored
13
android/.gitignore
vendored
@ -1,13 +0,0 @@
|
|||||||
gradle-wrapper.jar
|
|
||||||
/.gradle
|
|
||||||
/captures/
|
|
||||||
/gradlew
|
|
||||||
/gradlew.bat
|
|
||||||
/local.properties
|
|
||||||
GeneratedPluginRegistrant.java
|
|
||||||
|
|
||||||
# Remember to never publicly share your keystore.
|
|
||||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
|
||||||
key.properties
|
|
||||||
**/*.keystore
|
|
||||||
**/*.jks
|
|
@ -1,3 +0,0 @@
|
|||||||
source "https://rubygems.org"
|
|
||||||
|
|
||||||
gem "fastlane"
|
|
@ -1,118 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id "com.android.application"
|
|
||||||
id "kotlin-android"
|
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
|
||||||
}
|
|
||||||
|
|
||||||
def localProperties = new Properties()
|
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
|
||||||
if (localPropertiesFile.exists()) {
|
|
||||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
|
||||||
localProperties.load(reader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
|
||||||
if (flutterVersionCode == null) {
|
|
||||||
flutterVersionCode = '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
|
||||||
if (flutterVersionName == null) {
|
|
||||||
flutterVersionName = '1.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdk flutter.compileSdkVersion
|
|
||||||
ndkVersion flutter.ndkVersion
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '17'
|
|
||||||
}
|
|
||||||
sourceSets {
|
|
||||||
main.java.srcDirs += 'src/main/kotlin'
|
|
||||||
}
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "app.revanced.manager.flutter"
|
|
||||||
minSdk 26
|
|
||||||
targetSdk 34
|
|
||||||
versionCode flutterVersionCode.toInteger()
|
|
||||||
versionName flutterVersionName
|
|
||||||
}
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
ndk {
|
|
||||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
|
||||||
}
|
|
||||||
if (System.getenv("signingKey") != null) {
|
|
||||||
signingConfigs {
|
|
||||||
create("release") {
|
|
||||||
storeFile = file(System.getenv("signingKey"))
|
|
||||||
storePassword = System.getenv("keyStorePassword")
|
|
||||||
keyAlias = System.getenv("keyAlias")
|
|
||||||
keyPassword = System.getenv("keyPassword")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
signingConfig = signingConfigs.release
|
|
||||||
resValue "string", "app_name", "ReVanced Manager"
|
|
||||||
applicationVariants.all { variant ->
|
|
||||||
variant.outputs.all {
|
|
||||||
outputFileName = "revanced-manager-v${flutterVersionName}.apk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
signingConfig = signingConfigs.debug
|
|
||||||
resValue "string", "app_name", "ReVanced Manager Debug"
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
shrinkResources false
|
|
||||||
minifyEnabled false
|
|
||||||
resValue "string", "app_name", "ReVanced Manager Debug"
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
ndk {
|
|
||||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
profile {
|
|
||||||
shrinkResources false
|
|
||||||
minifyEnabled false
|
|
||||||
resValue "string", "app_name", "ReVanced Manager Profile"
|
|
||||||
applicationIdSuffix ".profile"
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
ndk {
|
|
||||||
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
jniLibs {
|
|
||||||
useLegacyPackaging true
|
|
||||||
excludes += ['/prebuilt/**']
|
|
||||||
}
|
|
||||||
resources {
|
|
||||||
excludes += ['/prebuilt/**']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace 'app.revanced.manager.flutter'
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin {
|
|
||||||
jvmToolchain(17)
|
|
||||||
}
|
|
||||||
|
|
||||||
flutter {
|
|
||||||
source '../..'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("app.revanced:revanced-patcher:19.3.1")
|
|
||||||
implementation("app.revanced:revanced-library:2.2.1")
|
|
||||||
}
|
|
21
android/app/proguard-rules.pro
vendored
21
android/app/proguard-rules.pro
vendored
@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.kts.kts.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
-dontobfuscate
|
|
||||||
|
|
||||||
-keep class app.revanced.** { *; }
|
|
||||||
-keep class com.android.tools.smali.** { *; }
|
|
||||||
-keep class kotlin.** { *; }
|
|
||||||
-keep class com.google.auto.value.** { *; }
|
|
||||||
-keep class com.android.apksig.internal.** { *; }
|
|
||||||
-keepnames class com.google.common.collect.**
|
|
||||||
-keepnames class org.xmlpull.** { *; }
|
|
||||||
|
|
||||||
-dontwarn com.google.auto.value.**
|
|
||||||
-dontwarn com.google.j2objc.annotations.*
|
|
||||||
-dontwarn java.awt.**
|
|
||||||
-dontwarn javax.**
|
|
@ -1,3 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
@ -1,81 +0,0 @@
|
|||||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="32" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="32" />
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:name="${applicationName}"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:largeHeap="true"
|
|
||||||
android:requestLegacyExternalStorage="true"
|
|
||||||
android:enableOnBackInvokedCallback="true">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<meta-data
|
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
|
||||||
android:resource="@style/NormalTheme" />
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".ExportSettingsActivity"
|
|
||||||
android:exported="true">
|
|
||||||
</activity>
|
|
||||||
<meta-data
|
|
||||||
android:name="flutterEmbedding"
|
|
||||||
android:value="2" />
|
|
||||||
<provider
|
|
||||||
android:name="androidx.core.content.FileProvider"
|
|
||||||
android:authorities="${applicationId}.fileProvider"
|
|
||||||
android:exported="false"
|
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
||||||
android:resource="@xml/file_paths" />
|
|
||||||
</provider>
|
|
||||||
|
|
||||||
<receiver
|
|
||||||
android:name=".utils.packageInstaller.InstallerReceiver"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="APP_INSTALL_ACTION" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
<receiver
|
|
||||||
android:name=".utils.packageInstaller.UninstallerReceiver"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="APP_UNINSTALL_ACTION" />
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
@ -1,83 +0,0 @@
|
|||||||
package app.revanced.manager.flutter
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Base64
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.security.cert.CertificateFactory
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
class ExportSettingsActivity : Activity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val callingPackageName = getCallingPackage()!!
|
|
||||||
|
|
||||||
if (getFingerprint(callingPackageName) == getFingerprint(getPackageName())) {
|
|
||||||
// Create JSON Object
|
|
||||||
val json = JSONObject()
|
|
||||||
|
|
||||||
// Default Data
|
|
||||||
json.put("keystorePassword", "s3cur3p@ssw0rd")
|
|
||||||
|
|
||||||
// Load Shared Preferences
|
|
||||||
val sharedPreferences = getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
|
|
||||||
val allEntries: Map<String, *> = sharedPreferences.getAll()
|
|
||||||
for ((key, value) in allEntries.entries) {
|
|
||||||
json.put(key.replace("flutter.", ""), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load keystore
|
|
||||||
val keystoreFile = File(getExternalFilesDir(null), "/revanced-manager.keystore")
|
|
||||||
if (keystoreFile.exists()) {
|
|
||||||
val keystoreBytes = keystoreFile.readBytes()
|
|
||||||
val keystoreBase64 = Base64.encodeToString(keystoreBytes, Base64.DEFAULT)
|
|
||||||
json.put("keystore", keystoreBase64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load saved patches
|
|
||||||
val storedPatchesFile = File(filesDir.parentFile.absolutePath, "/app_flutter/selected-patches.json")
|
|
||||||
if (storedPatchesFile.exists()) {
|
|
||||||
val patchesBytes = storedPatchesFile.readBytes()
|
|
||||||
val patches = String(patchesBytes, Charsets.UTF_8)
|
|
||||||
json.put("patches", JSONObject(patches))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send data back
|
|
||||||
val resultIntent = Intent()
|
|
||||||
resultIntent.putExtra("data", json.toString())
|
|
||||||
setResult(Activity.RESULT_OK, resultIntent)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
val resultIntent = Intent()
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFingerprint(packageName: String): String {
|
|
||||||
// Get the signature of the app that matches the package name
|
|
||||||
val packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
|
|
||||||
val signature = packageInfo.signatures[0]
|
|
||||||
|
|
||||||
// Get the raw certificate data
|
|
||||||
val rawCert = signature.toByteArray()
|
|
||||||
|
|
||||||
// Generate an X509Certificate from the data
|
|
||||||
val certFactory = CertificateFactory.getInstance("X509")
|
|
||||||
val x509Cert = certFactory.generateCertificate(ByteArrayInputStream(rawCert)) as X509Certificate
|
|
||||||
|
|
||||||
// Get the SHA256 fingerprint
|
|
||||||
val fingerprint = MessageDigest.getInstance("SHA256").digest(x509Cert.encoded).joinToString("") {
|
|
||||||
"%02x".format(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fingerprint
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,420 +0,0 @@
|
|||||||
package app.revanced.manager.flutter
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.SearchManager
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import app.revanced.library.ApkUtils
|
|
||||||
import app.revanced.library.ApkUtils.applyTo
|
|
||||||
import app.revanced.manager.flutter.utils.Aapt
|
|
||||||
import app.revanced.manager.flutter.utils.packageInstaller.InstallerReceiver
|
|
||||||
import app.revanced.manager.flutter.utils.packageInstaller.UninstallerReceiver
|
|
||||||
import app.revanced.patcher.PatchBundleLoader
|
|
||||||
import app.revanced.patcher.PatchSet
|
|
||||||
import app.revanced.patcher.Patcher
|
|
||||||
import app.revanced.patcher.PatcherConfig
|
|
||||||
import app.revanced.patcher.patch.PatchResult
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
import kotlinx.coroutines.flow.FlowCollector
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.File
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
import java.util.logging.LogRecord
|
|
||||||
import java.util.logging.Logger
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
|
||||||
private lateinit var installerChannel: MethodChannel
|
|
||||||
private var cancel: Boolean = false
|
|
||||||
private var stopResult: MethodChannel.Result? = null
|
|
||||||
|
|
||||||
private lateinit var patches: PatchSet
|
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
|
||||||
super.configureFlutterEngine(flutterEngine)
|
|
||||||
|
|
||||||
val patcherChannel = "app.revanced.manager.flutter/patcher"
|
|
||||||
val installerChannel = "app.revanced.manager.flutter/installer"
|
|
||||||
val openBrowserChannel = "app.revanced.manager.flutter/browser"
|
|
||||||
|
|
||||||
MethodChannel(
|
|
||||||
flutterEngine.dartExecutor.binaryMessenger,
|
|
||||||
openBrowserChannel
|
|
||||||
).setMethodCallHandler { call, result ->
|
|
||||||
if (call.method == "openBrowser") {
|
|
||||||
val searchQuery = call.argument<String>("query")
|
|
||||||
openBrowser(searchQuery)
|
|
||||||
result.success(null)
|
|
||||||
} else {
|
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mainChannel =
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, patcherChannel)
|
|
||||||
|
|
||||||
this.installerChannel =
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, installerChannel)
|
|
||||||
|
|
||||||
mainChannel.setMethodCallHandler { call, result ->
|
|
||||||
when (call.method) {
|
|
||||||
"runPatcher" -> {
|
|
||||||
val inFilePath = call.argument<String>("inFilePath")
|
|
||||||
val outFilePath = call.argument<String>("outFilePath")
|
|
||||||
val integrationsPath = call.argument<String>("integrationsPath")
|
|
||||||
val selectedPatches = call.argument<List<String>>("selectedPatches")
|
|
||||||
val options = call.argument<Map<String, Map<String, Any>>>("options")
|
|
||||||
val tmpDirPath = call.argument<String>("tmpDirPath")
|
|
||||||
val keyStoreFilePath = call.argument<String>("keyStoreFilePath")
|
|
||||||
val keystorePassword = call.argument<String>("keystorePassword")
|
|
||||||
|
|
||||||
if (
|
|
||||||
inFilePath != null &&
|
|
||||||
outFilePath != null &&
|
|
||||||
integrationsPath != null &&
|
|
||||||
selectedPatches != null &&
|
|
||||||
options != null &&
|
|
||||||
tmpDirPath != null &&
|
|
||||||
keyStoreFilePath != null &&
|
|
||||||
keystorePassword != null
|
|
||||||
) {
|
|
||||||
cancel = false
|
|
||||||
runPatcher(
|
|
||||||
result,
|
|
||||||
inFilePath,
|
|
||||||
outFilePath,
|
|
||||||
integrationsPath,
|
|
||||||
selectedPatches,
|
|
||||||
options,
|
|
||||||
tmpDirPath,
|
|
||||||
keyStoreFilePath,
|
|
||||||
keystorePassword
|
|
||||||
)
|
|
||||||
} else result.notImplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
"stopPatcher" -> {
|
|
||||||
cancel = true
|
|
||||||
stopResult = result
|
|
||||||
}
|
|
||||||
|
|
||||||
"getPatches" -> {
|
|
||||||
val patchBundleFilePath = call.argument<String>("patchBundleFilePath")!!
|
|
||||||
val cacheDirPath = call.argument<String>("cacheDirPath")!!
|
|
||||||
|
|
||||||
try {
|
|
||||||
val patchBundleFile = File(patchBundleFilePath)
|
|
||||||
patchBundleFile.setWritable(false)
|
|
||||||
patches = PatchBundleLoader.Dex(
|
|
||||||
patchBundleFile,
|
|
||||||
optimizedDexDirectory = File(cacheDirPath)
|
|
||||||
)
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
return@setMethodCallHandler result.notImplemented()
|
|
||||||
} catch (err: Error) {
|
|
||||||
return@setMethodCallHandler result.notImplemented()
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONArray().apply {
|
|
||||||
patches.forEach {
|
|
||||||
JSONObject().apply {
|
|
||||||
put("name", it.name)
|
|
||||||
put("description", it.description)
|
|
||||||
put("excluded", !it.use)
|
|
||||||
put("compatiblePackages", JSONArray().apply {
|
|
||||||
it.compatiblePackages?.forEach { compatiblePackage ->
|
|
||||||
val compatiblePackageJson = JSONObject().apply {
|
|
||||||
put("name", compatiblePackage.name)
|
|
||||||
put(
|
|
||||||
"versions",
|
|
||||||
JSONArray().apply {
|
|
||||||
compatiblePackage.versions?.forEach { version ->
|
|
||||||
put(version)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
put(compatiblePackageJson)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
put("options", JSONArray().apply {
|
|
||||||
it.options.values.forEach { option ->
|
|
||||||
JSONObject().apply {
|
|
||||||
put("key", option.key)
|
|
||||||
put("title", option.title)
|
|
||||||
put("description", option.description)
|
|
||||||
put("required", option.required)
|
|
||||||
|
|
||||||
fun JSONObject.putValue(
|
|
||||||
value: Any?,
|
|
||||||
key: String = "value"
|
|
||||||
) = if (value is Array<*>) put(
|
|
||||||
key,
|
|
||||||
JSONArray().apply {
|
|
||||||
value.forEach { put(it) }
|
|
||||||
})
|
|
||||||
else put(key, value)
|
|
||||||
|
|
||||||
putValue(option.default)
|
|
||||||
|
|
||||||
option.values?.let { values ->
|
|
||||||
put("values",
|
|
||||||
JSONObject().apply {
|
|
||||||
values.forEach { (key, value) ->
|
|
||||||
putValue(value, key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} ?: put("values", null)
|
|
||||||
put("valueType", option.valueType)
|
|
||||||
}.let(::put)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}.let(::put)
|
|
||||||
}
|
|
||||||
}.toString().let(result::success)
|
|
||||||
}
|
|
||||||
|
|
||||||
"installApk" -> {
|
|
||||||
val apkPath = call.argument<String>("apkPath")!!
|
|
||||||
PackageInstallerManager.result = result
|
|
||||||
installApk(apkPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
"uninstallApp" -> {
|
|
||||||
val packageName = call.argument<String>("packageName")!!
|
|
||||||
uninstallApp(packageName)
|
|
||||||
PackageInstallerManager.result = result
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openBrowser(query: String?) {
|
|
||||||
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
|
||||||
putExtra(SearchManager.QUERY, query)
|
|
||||||
}
|
|
||||||
if (intent.resolveActivity(packageManager) != null) {
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runPatcher(
|
|
||||||
result: MethodChannel.Result,
|
|
||||||
inFilePath: String,
|
|
||||||
outFilePath: String,
|
|
||||||
integrationsPath: String,
|
|
||||||
selectedPatches: List<String>,
|
|
||||||
options: Map<String, Map<String, Any>>,
|
|
||||||
tmpDirPath: String,
|
|
||||||
keyStoreFilePath: String,
|
|
||||||
keystorePassword: String
|
|
||||||
) {
|
|
||||||
val inFile = File(inFilePath)
|
|
||||||
val outFile = File(outFilePath)
|
|
||||||
val integrations = File(integrationsPath)
|
|
||||||
val keyStoreFile = File(keyStoreFilePath)
|
|
||||||
val tmpDir = File(tmpDirPath)
|
|
||||||
|
|
||||||
Thread {
|
|
||||||
fun updateProgress(progress: Double, header: String, log: String) {
|
|
||||||
handler.post {
|
|
||||||
installerChannel.invokeMethod(
|
|
||||||
"update",
|
|
||||||
mapOf(
|
|
||||||
"progress" to progress,
|
|
||||||
"header" to header,
|
|
||||||
"log" to log
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun postStop() = handler.post { stopResult!!.success(null) }
|
|
||||||
|
|
||||||
fun cancel(block: () -> Unit = {}): Boolean {
|
|
||||||
if (cancel) {
|
|
||||||
block()
|
|
||||||
postStop()
|
|
||||||
}
|
|
||||||
|
|
||||||
return cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Setup logger
|
|
||||||
Logger.getLogger("").apply {
|
|
||||||
handlers.forEach {
|
|
||||||
it.close()
|
|
||||||
removeHandler(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
object : java.util.logging.Handler() {
|
|
||||||
override fun publish(record: LogRecord) {
|
|
||||||
if (record.loggerName?.startsWith("app.revanced") != true || cancel) return
|
|
||||||
|
|
||||||
updateProgress(-1.0, "", record.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun flush() = Unit
|
|
||||||
override fun close() = flush()
|
|
||||||
}.let(::addHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
updateProgress(0.0, "Reading APK...", "Reading APK")
|
|
||||||
|
|
||||||
val patcher = Patcher(
|
|
||||||
PatcherConfig(
|
|
||||||
inFile,
|
|
||||||
tmpDir,
|
|
||||||
Aapt.binary(applicationContext).absolutePath,
|
|
||||||
tmpDir.path,
|
|
||||||
true // TODO: Add option to disable this
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cancel(patcher::close)) return@Thread
|
|
||||||
updateProgress(0.02, "Loading patches...", "Loading patches")
|
|
||||||
|
|
||||||
val patches = patches.filter { patch ->
|
|
||||||
val isCompatible = patch.compatiblePackages?.any {
|
|
||||||
it.name == patcher.context.packageMetadata.packageName
|
|
||||||
} ?: false
|
|
||||||
|
|
||||||
val compatibleOrUniversal =
|
|
||||||
isCompatible || patch.compatiblePackages.isNullOrEmpty()
|
|
||||||
|
|
||||||
compatibleOrUniversal && selectedPatches.any { it == patch.name }
|
|
||||||
}.onEach { patch ->
|
|
||||||
options[patch.name]?.forEach { (key, value) ->
|
|
||||||
patch.options[key] = value
|
|
||||||
}
|
|
||||||
}.toSet()
|
|
||||||
|
|
||||||
if (cancel(patcher::close)) return@Thread
|
|
||||||
updateProgress(0.05, "Executing...", "")
|
|
||||||
|
|
||||||
val patcherResult = patcher.use {
|
|
||||||
patcher.apply {
|
|
||||||
acceptIntegrations(setOf(integrations))
|
|
||||||
acceptPatches(patches)
|
|
||||||
}
|
|
||||||
|
|
||||||
runBlocking {
|
|
||||||
// Update the progress bar every time a patch is executed from 0.15 to 0.7
|
|
||||||
val totalPatchesCount = patches.size
|
|
||||||
val progressStep = 0.55 / totalPatchesCount
|
|
||||||
var progress = 0.05
|
|
||||||
|
|
||||||
patcher.apply(false).collect(FlowCollector { patchResult: PatchResult ->
|
|
||||||
if (cancel(patcher::close)) return@FlowCollector
|
|
||||||
|
|
||||||
val msg = patchResult.exception?.let {
|
|
||||||
val writer = StringWriter()
|
|
||||||
it.printStackTrace(PrintWriter(writer))
|
|
||||||
"${patchResult.patch.name} failed: $writer"
|
|
||||||
} ?: run {
|
|
||||||
"${patchResult.patch.name} succeeded"
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgress(progress, "", msg)
|
|
||||||
progress += progressStep
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancel(patcher::close)) return@Thread
|
|
||||||
updateProgress(0.75, "Building...", "")
|
|
||||||
|
|
||||||
patcher.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancel(patcher::close)) return@Thread
|
|
||||||
|
|
||||||
patcherResult.applyTo(inFile)
|
|
||||||
|
|
||||||
if (cancel(patcher::close)) return@Thread
|
|
||||||
|
|
||||||
ApkUtils.sign(
|
|
||||||
inFile,
|
|
||||||
outFile,
|
|
||||||
ApkUtils.SigningOptions(
|
|
||||||
keyStoreFile,
|
|
||||||
keystorePassword,
|
|
||||||
"alias",
|
|
||||||
keystorePassword
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
updateProgress(.85, "Patched", "Patched APK")
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
if (!cancel) {
|
|
||||||
val stack = ex.stackTraceToString()
|
|
||||||
updateProgress(
|
|
||||||
-100.0,
|
|
||||||
"Failed",
|
|
||||||
"An error occurred:\n$stack"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.post { result.success(null) }
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun installApk(apkPath: String) {
|
|
||||||
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
|
||||||
val sessionParams =
|
|
||||||
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
|
||||||
val sessionId: Int = packageInstaller.createSession(sessionParams)
|
|
||||||
val session: PackageInstaller.Session = packageInstaller.openSession(sessionId)
|
|
||||||
session.use { activeSession ->
|
|
||||||
val sessionOutputStream = activeSession.openWrite(applicationContext.packageName, 0, -1)
|
|
||||||
sessionOutputStream.use { outputStream ->
|
|
||||||
val apkFile = File(apkPath)
|
|
||||||
apkFile.inputStream().use { inputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val receiverIntent = Intent(applicationContext, InstallerReceiver::class.java).apply {
|
|
||||||
action = "APP_INSTALL_ACTION"
|
|
||||||
}
|
|
||||||
val receiverPendingIntent = PendingIntent.getBroadcast(
|
|
||||||
context,
|
|
||||||
sessionId,
|
|
||||||
receiverIntent,
|
|
||||||
PackageInstallerManager.flags
|
|
||||||
)
|
|
||||||
session.commit(receiverPendingIntent.intentSender)
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun uninstallApp(packageName: String) {
|
|
||||||
val packageInstaller: PackageInstaller = applicationContext.packageManager.packageInstaller
|
|
||||||
val receiverIntent = Intent(applicationContext, UninstallerReceiver::class.java).apply {
|
|
||||||
action = "APP_UNINSTALL_ACTION"
|
|
||||||
}
|
|
||||||
val receiverPendingIntent =
|
|
||||||
PendingIntent.getBroadcast(context, 0, receiverIntent, PackageInstallerManager.flags)
|
|
||||||
packageInstaller.uninstall(packageName, receiverPendingIntent.intentSender)
|
|
||||||
}
|
|
||||||
|
|
||||||
object PackageInstallerManager {
|
|
||||||
var result: MethodChannel.Result? = null
|
|
||||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
} else {
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object Aapt {
|
|
||||||
fun binary(context: Context): File {
|
|
||||||
return File(context.applicationInfo.nativeLibraryDir).resolveAapt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun File.resolveAapt() = resolve(list { _, f -> !File(f).isDirectory && f.contains("aapt") }!!.first())
|
|
@ -1,32 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils.packageInstaller
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import app.revanced.manager.flutter.MainActivity
|
|
||||||
|
|
||||||
class InstallerReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
|
||||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
|
||||||
if (confirmationIntent != null) {
|
|
||||||
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
|
||||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
|
||||||
val otherPackageName = intent.getStringExtra(PackageInstaller.EXTRA_OTHER_PACKAGE_NAME)
|
|
||||||
MainActivity.PackageInstallerManager.result!!.success(mapOf(
|
|
||||||
"status" to status,
|
|
||||||
"packageName" to packageName,
|
|
||||||
"message" to message,
|
|
||||||
"otherPackageName" to otherPackageName
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
package app.revanced.manager.flutter.utils.packageInstaller
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import app.revanced.manager.flutter.MainActivity
|
|
||||||
|
|
||||||
class UninstallerReceiver : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
|
|
||||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
|
||||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
|
||||||
if (confirmationIntent != null) {
|
|
||||||
context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
MainActivity.PackageInstallerManager.result!!.success(status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="?android:colorBackground" />
|
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
|
@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Modify this file to customize your launch splash screen -->
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:drawable="@android:color/white" />
|
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
|
||||||
<!-- <item>
|
|
||||||
<bitmap
|
|
||||||
android:gravity="center"
|
|
||||||
android:src="@mipmap/launch_image" />
|
|
||||||
</item> -->
|
|
||||||
</layer-list>
|
|
@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_launcher_round</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#1B1B1B</color>
|
|
||||||
</resources>
|
|
@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
|
||||||
the Flutter engine draws its first frame -->
|
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
|
||||||
</style>
|
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
|
||||||
This theme determines the color of the Android Window while your
|
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
|
||||||
running.
|
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<paths>
|
|
||||||
<cache-path name="cache" path="." />
|
|
||||||
</paths>
|
|
@ -1,3 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
|
||||||
</manifest>
|
|
@ -1,27 +0,0 @@
|
|||||||
allprojects {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
maven {
|
|
||||||
// A repository must be speficied for some reason. "registry" is a dummy.
|
|
||||||
url = uri("https://maven.pkg.github.com/revanced/registry")
|
|
||||||
credentials {
|
|
||||||
username = project.findProperty("gpr.user") as String ?: System.getenv("GITHUB_ACTOR")
|
|
||||||
password = project.findProperty("gpr.key") as String ?: System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mavenLocal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.buildDir = '../build'
|
|
||||||
subprojects {
|
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
|
||||||
}
|
|
||||||
subprojects {
|
|
||||||
project.evaluationDependsOn(':app')
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("clean", Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
org.gradle.jvmargs=-Xmx1536M -XX:+UseParallelGC
|
|
||||||
org.gradle.parallel=true
|
|
||||||
org.gradle.daemon=true
|
|
||||||
org.gradle.caching=true
|
|
||||||
android.useAndroidX=true
|
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
|
||||||
android.nonTransitiveRClass=false
|
|
||||||
android.nonFinalResIds=false
|
|
@ -1,25 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
def flutterSdkPath = {
|
|
||||||
def properties = new Properties()
|
|
||||||
file("local.properties").withInputStream { properties.load(it) }
|
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
|
||||||
return flutterSdkPath
|
|
||||||
}
|
|
||||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
|
||||||
|
|
||||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
|
||||||
id "com.android.application" version "8.1.2" apply false
|
|
||||||
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
include ":app"
|
|
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
222
app/build.gradle.kts
Normal file
222
app/build.gradle.kts
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
|
alias(libs.plugins.devtools)
|
||||||
|
alias(libs.plugins.about.libraries)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "app.revanced.manager"
|
||||||
|
compileSdk = 35
|
||||||
|
buildToolsVersion = "35.0.1"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "app.revanced.manager"
|
||||||
|
minSdk = 26
|
||||||
|
targetSdk = 35
|
||||||
|
versionCode = 1
|
||||||
|
versionName = "0.0.1"
|
||||||
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
resValue("string", "app_name", "ReVanced Manager (Debug)")
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "${Random.nextLong()}L")
|
||||||
|
}
|
||||||
|
|
||||||
|
release {
|
||||||
|
if (!project.hasProperty("noProguard")) {
|
||||||
|
isMinifyEnabled = true
|
||||||
|
isShrinkResources = true
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
|
||||||
|
val keystoreFile = file("keystore.jks")
|
||||||
|
|
||||||
|
if (project.hasProperty("signAsDebug") || !keystoreFile.exists()) {
|
||||||
|
applicationIdSuffix = ".debug_signed"
|
||||||
|
resValue("string", "app_name", "ReVanced Manager (Debug signed)")
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
|
||||||
|
isPseudoLocalesEnabled = true
|
||||||
|
} else {
|
||||||
|
signingConfig = signingConfigs.create("release") {
|
||||||
|
storeFile = keystoreFile
|
||||||
|
storePassword = System.getenv("KEYSTORE_PASSWORD")
|
||||||
|
keyAlias = System.getenv("KEYSTORE_ENTRY_ALIAS")
|
||||||
|
keyPassword = System.getenv("KEYSTORE_ENTRY_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConfigField("long", "BUILD_ID", "0L")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesInfo {
|
||||||
|
includeInApk = false
|
||||||
|
includeInBundle = false
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources.excludes.addAll(
|
||||||
|
listOf(
|
||||||
|
"/prebuilt/**",
|
||||||
|
"META-INF/DEPENDENCIES",
|
||||||
|
"META-INF/**.version",
|
||||||
|
"DebugProbesKt.bin",
|
||||||
|
"kotlin-tooling-metadata.json",
|
||||||
|
"org/bouncycastle/pqc/**.properties",
|
||||||
|
"org/bouncycastle/x509/**.properties",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
compose = true
|
||||||
|
aidl = true
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
cmake {
|
||||||
|
path = file("src/main/cpp/CMakeLists.txt")
|
||||||
|
version = "3.22.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
// AndroidX Core
|
||||||
|
implementation(libs.androidx.ktx)
|
||||||
|
implementation(libs.runtime.ktx)
|
||||||
|
implementation(libs.runtime.compose)
|
||||||
|
implementation(libs.splash.screen)
|
||||||
|
implementation(libs.activity.compose)
|
||||||
|
implementation(libs.work.runtime.ktx)
|
||||||
|
implementation(libs.preferences.datastore)
|
||||||
|
implementation(libs.appcompat)
|
||||||
|
|
||||||
|
// Compose
|
||||||
|
implementation(platform(libs.compose.bom))
|
||||||
|
implementation(libs.compose.ui)
|
||||||
|
implementation(libs.compose.ui.preview)
|
||||||
|
implementation(libs.compose.ui.tooling)
|
||||||
|
implementation(libs.compose.livedata)
|
||||||
|
implementation(libs.compose.material.icons.extended)
|
||||||
|
implementation(libs.compose.material3)
|
||||||
|
implementation(libs.navigation.compose)
|
||||||
|
|
||||||
|
// Accompanist
|
||||||
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|
||||||
|
// Placeholder
|
||||||
|
implementation(libs.placeholder.material3)
|
||||||
|
|
||||||
|
// HTML Scraper
|
||||||
|
implementation(libs.skrapeit.dsl)
|
||||||
|
implementation(libs.skrapeit.parser)
|
||||||
|
|
||||||
|
// Coil (async image loading, network image)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.appiconloader)
|
||||||
|
|
||||||
|
// KotlinX
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.kotlinx.collection.immutable)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
|
// Room
|
||||||
|
implementation(libs.room.runtime)
|
||||||
|
implementation(libs.room.ktx)
|
||||||
|
annotationProcessor(libs.room.compiler)
|
||||||
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
|
// ReVanced
|
||||||
|
implementation(libs.revanced.patcher)
|
||||||
|
implementation(libs.revanced.library)
|
||||||
|
|
||||||
|
// Downloader plugins
|
||||||
|
implementation(libs.plugin.api)
|
||||||
|
|
||||||
|
// Native processes
|
||||||
|
implementation(libs.kotlin.process)
|
||||||
|
|
||||||
|
// HiddenAPI
|
||||||
|
compileOnly(libs.hidden.api.stub)
|
||||||
|
|
||||||
|
// LibSU
|
||||||
|
implementation(libs.libsu.core)
|
||||||
|
implementation(libs.libsu.service)
|
||||||
|
implementation(libs.libsu.nio)
|
||||||
|
|
||||||
|
// Koin
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.koin.compose)
|
||||||
|
implementation(libs.koin.compose.navigation)
|
||||||
|
implementation(libs.koin.workmanager)
|
||||||
|
|
||||||
|
// Licenses
|
||||||
|
implementation(libs.about.libraries)
|
||||||
|
|
||||||
|
// Ktor
|
||||||
|
implementation(libs.ktor.core)
|
||||||
|
implementation(libs.ktor.logging)
|
||||||
|
implementation(libs.ktor.okhttp)
|
||||||
|
implementation(libs.ktor.content.negotiation)
|
||||||
|
implementation(libs.ktor.serialization)
|
||||||
|
|
||||||
|
// Markdown
|
||||||
|
implementation(libs.markdown.renderer)
|
||||||
|
|
||||||
|
// Fading Edges
|
||||||
|
implementation(libs.fading.edges)
|
||||||
|
|
||||||
|
// Scrollbars
|
||||||
|
implementation(libs.scrollbars)
|
||||||
|
|
||||||
|
// EnumUtil
|
||||||
|
implementation(libs.enumutil)
|
||||||
|
ksp(libs.enumutil.ksp)
|
||||||
|
|
||||||
|
// Reorderable lists
|
||||||
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
|
// Compose Icons
|
||||||
|
implementation(libs.compose.icons.fontawesome)
|
||||||
|
}
|
63
app/proguard-rules.pro
vendored
Normal file
63
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.kts.kts.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
|
# Required for serialization to work properly
|
||||||
|
-if @kotlinx.serialization.Serializable class **
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
static <1>$Companion Companion;
|
||||||
|
}
|
||||||
|
-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
static **$* *;
|
||||||
|
}
|
||||||
|
-keepclassmembers class <2>$<3> {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
-if @kotlinx.serialization.Serializable class ** {
|
||||||
|
public static ** INSTANCE;
|
||||||
|
}
|
||||||
|
-keepclassmembers class <1> {
|
||||||
|
public static <1> INSTANCE;
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# This required for the process runtime.
|
||||||
|
-keep class app.revanced.manager.patcher.runtime.process.* {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
# Required for the patcher to function correctly
|
||||||
|
-keep class app.revanced.patcher.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class brut.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class org.xmlpull.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class kotlin.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class org.jf.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class com.android.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
-keep class app.revanced.manager.plugin.** {
|
||||||
|
*;
|
||||||
|
}
|
||||||
|
|
||||||
|
-dontwarn com.google.auto.value.**
|
||||||
|
-dontwarn java.awt.**
|
||||||
|
-dontwarn javax.**
|
||||||
|
-dontwarn org.slf4j.**
|
||||||
|
-dontwarn it.skrape.fetcher.*
|
||||||
|
-dontwarn com.google.j2objc.annotations.*
|
||||||
|
|
||||||
|
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
429
app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
Normal file
429
app/schemas/app.revanced.manager.data.room.AppDatabase/1.json
Normal file
@ -0,0 +1,429 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "d0119047505da435972c5247181de675",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "patch_bundles",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `name` TEXT NOT NULL, `version` TEXT, `source` TEXT NOT NULL, `auto_update` INTEGER NOT NULL, PRIMARY KEY(`uid`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "autoUpdate",
|
||||||
|
"columnName": "auto_update",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "patch_selections",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchBundle",
|
||||||
|
"columnName": "patch_bundle",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_patch_selections_patch_bundle_package_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"patch_bundle",
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_patch_selections_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "patch_bundles",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"patch_bundle"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "selected_patches",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`selection` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`selection`, `patch_name`), FOREIGN KEY(`selection`) REFERENCES `patch_selections`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "selection",
|
||||||
|
"columnName": "selection",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchName",
|
||||||
|
"columnName": "patch_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"selection",
|
||||||
|
"patch_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "patch_selections",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"selection"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "downloaded_app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `directory` TEXT NOT NULL, `last_used` INTEGER NOT NULL, PRIMARY KEY(`package_name`, `version`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "directory",
|
||||||
|
"columnName": "directory",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsed",
|
||||||
|
"columnName": "last_used",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name",
|
||||||
|
"version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "installed_app",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`current_package_name` TEXT NOT NULL, `original_package_name` TEXT NOT NULL, `version` TEXT NOT NULL, `install_type` TEXT NOT NULL, PRIMARY KEY(`current_package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "currentPackageName",
|
||||||
|
"columnName": "current_package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "originalPackageName",
|
||||||
|
"columnName": "original_package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "installType",
|
||||||
|
"columnName": "install_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"current_package_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "applied_patch",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `bundle` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, PRIMARY KEY(`package_name`, `bundle`, `patch_name`), FOREIGN KEY(`package_name`) REFERENCES `installed_app`(`current_package_name`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bundle",
|
||||||
|
"columnName": "bundle",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchName",
|
||||||
|
"columnName": "patch_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name",
|
||||||
|
"bundle",
|
||||||
|
"patch_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_applied_patch_bundle",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"bundle"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_applied_patch_bundle` ON `${TABLE_NAME}` (`bundle`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "installed_app",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"current_package_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "patch_bundles",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"bundle"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "option_groups",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER NOT NULL, `patch_bundle` INTEGER NOT NULL, `package_name` TEXT NOT NULL, PRIMARY KEY(`uid`), FOREIGN KEY(`patch_bundle`) REFERENCES `patch_bundles`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchBundle",
|
||||||
|
"columnName": "patch_bundle",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_option_groups_patch_bundle_package_name",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"patch_bundle",
|
||||||
|
"package_name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_option_groups_patch_bundle_package_name` ON `${TABLE_NAME}` (`patch_bundle`, `package_name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "patch_bundles",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"patch_bundle"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "options",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group` INTEGER NOT NULL, `patch_name` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`group`, `patch_name`, `key`), FOREIGN KEY(`group`) REFERENCES `option_groups`(`uid`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "group",
|
||||||
|
"columnName": "group",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "patchName",
|
||||||
|
"columnName": "patch_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "value",
|
||||||
|
"columnName": "value",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group",
|
||||||
|
"patch_name",
|
||||||
|
"key"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "option_groups",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"group"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "trusted_downloader_plugins",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package_name` TEXT NOT NULL, `signature` BLOB NOT NULL, PRIMARY KEY(`package_name`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "packageName",
|
||||||
|
"columnName": "package_name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "signature",
|
||||||
|
"columnName": "signature",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"package_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd0119047505da435972c5247181de675')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
78
app/src/main/AndroidManifest.xml
Normal file
78
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<permission
|
||||||
|
android:name="app.revanced.manager.permission.PLUGIN_HOST"
|
||||||
|
android:protectionLevel="signature"
|
||||||
|
android:label="@string/plugin_host_permission_label"
|
||||||
|
android:description="@string/plugin_host_permission_description"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<uses-permission android:name="app.revanced.manager.permission.PLUGIN_HOST" />
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".ManagerApplication"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:largeHeap="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.ReVancedManager"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
tools:targetApi="34">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.ReVancedManager">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".plugin.downloader.webview.WebViewActivity" android:exported="false" android:theme="@style/Theme.WebViewActivity" />
|
||||||
|
|
||||||
|
<service android:name=".service.InstallService" />
|
||||||
|
<service android:name=".service.UninstallService" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="patching"
|
||||||
|
/>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
@ -0,0 +1,8 @@
|
|||||||
|
// IRootService.aidl
|
||||||
|
package app.revanced.manager;
|
||||||
|
|
||||||
|
// Declare any non-default types here with import statements
|
||||||
|
|
||||||
|
interface IRootSystemService {
|
||||||
|
IBinder getFileSystemService();
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
// IPatcherEvents.aidl
|
||||||
|
package app.revanced.manager.patcher.runtime.process;
|
||||||
|
|
||||||
|
// Interface for sending events back to the main app process.
|
||||||
|
oneway interface IPatcherEvents {
|
||||||
|
void log(String level, String msg);
|
||||||
|
void patchSucceeded();
|
||||||
|
void progress(String name, String state, String msg);
|
||||||
|
// The patching process has ended. The exceptionStackTrace is null if it finished successfully.
|
||||||
|
void finished(String exceptionStackTrace);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
// IPatcherProcess.aidl
|
||||||
|
package app.revanced.manager.patcher.runtime.process;
|
||||||
|
|
||||||
|
import app.revanced.manager.patcher.runtime.process.Parameters;
|
||||||
|
import app.revanced.manager.patcher.runtime.process.IPatcherEvents;
|
||||||
|
|
||||||
|
interface IPatcherProcess {
|
||||||
|
// Returns BuildConfig.BUILD_ID, which is used to ensure the main app and runner process are running the same code.
|
||||||
|
long buildId();
|
||||||
|
// Makes the patcher process exit with code 0
|
||||||
|
oneway void exit();
|
||||||
|
// Starts patching.
|
||||||
|
oneway void start(in Parameters parameters, IPatcherEvents events);
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
// Parameters.aidl
|
||||||
|
package app.revanced.manager.patcher.runtime.process;
|
||||||
|
|
||||||
|
parcelable Parameters;
|
6
app/src/main/assets/root/module.prop
Normal file
6
app/src/main/assets/root/module.prop
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
id=__PKG_NAME__-ReVanced
|
||||||
|
name=__LABEL__ ReVanced
|
||||||
|
version=__VERSION__
|
||||||
|
versionCode=0
|
||||||
|
author=ReVanced
|
||||||
|
description=Mounts the patched APK on top of the original one
|
40
app/src/main/assets/root/service.sh
Normal file
40
app/src/main/assets/root/service.sh
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
DIR=${0%/*}
|
||||||
|
|
||||||
|
package_name="__PKG_NAME__"
|
||||||
|
version="__VERSION__"
|
||||||
|
|
||||||
|
rm "$DIR/log"
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
until [ "$(getprop sys.boot_completed)" = 1 ]; do sleep 5; done
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
base_path="$DIR/$package_name.apk"
|
||||||
|
stock_path="$(pm path "$package_name" | grep base | sed 's/package://g')"
|
||||||
|
stock_version="$(dumpsys package "$package_name" | grep versionName | cut -d "=" -f2)"
|
||||||
|
|
||||||
|
echo "base_path: $base_path"
|
||||||
|
echo "stock_path: $stock_path"
|
||||||
|
echo "base_version: $version"
|
||||||
|
echo "stock_version: $stock_version"
|
||||||
|
|
||||||
|
if mount | grep -q "$stock_path" ; then
|
||||||
|
echo "Not mounting as stock path is already mounted"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$version" != "$stock_version" ]; then
|
||||||
|
echo "Not mounting as versions don't match"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$stock_path" ]; then
|
||||||
|
echo "Not mounting as app info could not be loaded"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mount -o bind "$base_path" "$stock_path"
|
||||||
|
|
||||||
|
} >> "$DIR/log"
|
38
app/src/main/cpp/CMakeLists.txt
Normal file
38
app/src/main/cpp/CMakeLists.txt
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
# For more information about using CMake with Android Studio, read the
|
||||||
|
# documentation: https://d.android.com/studio/projects/add-native-code.html.
|
||||||
|
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
|
||||||
|
|
||||||
|
# Sets the minimum CMake version required for this project.
|
||||||
|
cmake_minimum_required(VERSION 3.22.1)
|
||||||
|
|
||||||
|
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
|
||||||
|
# Since this is the top level CMakeLists.txt, the project name is also accessible
|
||||||
|
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
|
||||||
|
# build script scope).
|
||||||
|
project("prop_override")
|
||||||
|
|
||||||
|
# Creates and names a library, sets it as either STATIC
|
||||||
|
# or SHARED, and provides the relative paths to its source code.
|
||||||
|
# You can define multiple libraries, and CMake builds them for you.
|
||||||
|
# Gradle automatically packages shared libraries with your APK.
|
||||||
|
#
|
||||||
|
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
|
||||||
|
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
|
||||||
|
# is preferred for the same purpose.
|
||||||
|
#
|
||||||
|
# In order to load a library into your app from Java/Kotlin, you must call
|
||||||
|
# System.loadLibrary() and pass the name of the library defined here;
|
||||||
|
# for GameActivity/NativeActivity derived applications, the same library name must be
|
||||||
|
# used in the AndroidManifest.xml file.
|
||||||
|
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||||
|
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||||
|
prop_override.cpp)
|
||||||
|
|
||||||
|
# Specifies libraries CMake should link to your target library. You
|
||||||
|
# can link libraries from various origins, such as libraries defined in this
|
||||||
|
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||||
|
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||||
|
# List libraries link to the target library
|
||||||
|
android
|
||||||
|
log)
|
62
app/src/main/cpp/prop_override.cpp
Normal file
62
app/src/main/cpp/prop_override.cpp
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// Library for overriding Android system properties via environment variables.
|
||||||
|
//
|
||||||
|
// Usage: LD_PRELOAD=prop_override.so PROP_dalvik.vm.heapsize=123M getprop dalvik.vm.heapsize
|
||||||
|
// Output: 123M
|
||||||
|
#include <string>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
|
||||||
|
// Source: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/include/cutils/properties.h
|
||||||
|
#define PROP_VALUE_MAX 92
|
||||||
|
// This is the mangled name of "android::base::GetProperty".
|
||||||
|
#define GET_PROPERTY_MANGLED_NAME "_ZN7android4base11GetPropertyERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_"
|
||||||
|
|
||||||
|
extern "C" typedef int (*property_get_ptr)(const char *, char *, const char *);
|
||||||
|
typedef std::string (*GetProperty_ptr)(const std::string &, const std::string &);
|
||||||
|
|
||||||
|
char *GetPropOverride(const std::string &key) {
|
||||||
|
auto envKey = "PROP_" + key;
|
||||||
|
|
||||||
|
return getenv(envKey.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// See: https://android.googlesource.com/platform/system/core/+/100b08a848d018eeb1caa5d5e7c7c2aaac65da15/libcutils/properties.cpp
|
||||||
|
extern "C" int property_get(const char *key, char *value, const char *default_value) {
|
||||||
|
auto replacement = GetPropOverride(std::string(key));
|
||||||
|
if (replacement) {
|
||||||
|
int len = strnlen(replacement, PROP_VALUE_MAX);
|
||||||
|
|
||||||
|
strncpy(value, replacement, len);
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static property_get_ptr original = NULL;
|
||||||
|
if (!original) {
|
||||||
|
// Get the address of the original function.
|
||||||
|
original = reinterpret_cast<property_get_ptr>(dlsym(RTLD_NEXT, "property_get"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return original(key, value, default_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defining android::base::GetProperty ourselves won't work because std::string has a slightly different "path" in the NDK version of the C++ standard library.
|
||||||
|
// We can get around this by forcing the function to adopt a specific name using the asm keyword.
|
||||||
|
std::string GetProperty(const std::string &, const std::string &) asm(GET_PROPERTY_MANGLED_NAME);
|
||||||
|
|
||||||
|
|
||||||
|
// See: https://android.googlesource.com/platform/system/libbase/+/1a34bb67c4f3ba0a1ea6f4f20ac9fe117ba4fe64/properties.cpp
|
||||||
|
// This isn't used for the properties we want to override, but property_get is deprecated so that could change in the future.
|
||||||
|
std::string GetProperty(const std::string &key, const std::string &default_value) {
|
||||||
|
auto replacement = GetPropOverride(key);
|
||||||
|
if (replacement) {
|
||||||
|
return std::string(replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GetProperty_ptr original = NULL;
|
||||||
|
if (!original) {
|
||||||
|
original = reinterpret_cast<GetProperty_ptr>(dlsym(RTLD_NEXT, GET_PROPERTY_MANGLED_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
return original(key, default_value);
|
||||||
|
}
|
336
app/src/main/java/app/revanced/manager/MainActivity.kt
Normal file
336
app/src/main/java/app/revanced/manager/MainActivity.kt
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
package app.revanced.manager
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.navigation
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import androidx.navigation.toRoute
|
||||||
|
import app.revanced.manager.ui.model.navigation.AppSelector
|
||||||
|
import app.revanced.manager.ui.model.navigation.ComplexParameter
|
||||||
|
import app.revanced.manager.ui.model.navigation.Dashboard
|
||||||
|
import app.revanced.manager.ui.model.navigation.InstalledApplicationInfo
|
||||||
|
import app.revanced.manager.ui.model.navigation.Patcher
|
||||||
|
import app.revanced.manager.ui.model.navigation.SelectedApplicationInfo
|
||||||
|
import app.revanced.manager.ui.model.navigation.Settings
|
||||||
|
import app.revanced.manager.ui.model.navigation.Update
|
||||||
|
import app.revanced.manager.ui.screen.AppSelectorScreen
|
||||||
|
import app.revanced.manager.ui.screen.DashboardScreen
|
||||||
|
import app.revanced.manager.ui.screen.InstalledAppInfoScreen
|
||||||
|
import app.revanced.manager.ui.screen.PatcherScreen
|
||||||
|
import app.revanced.manager.ui.screen.PatchesSelectorScreen
|
||||||
|
import app.revanced.manager.ui.screen.RequiredOptionsScreen
|
||||||
|
import app.revanced.manager.ui.screen.SelectedAppInfoScreen
|
||||||
|
import app.revanced.manager.ui.screen.SettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.UpdateScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.AboutSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.AdvancedSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.ContributorSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.DeveloperSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.DownloadsSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.GeneralSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.ImportExportSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.LicensesSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.ChangelogsSettingsScreen
|
||||||
|
import app.revanced.manager.ui.screen.settings.update.UpdatesSettingsScreen
|
||||||
|
import app.revanced.manager.ui.theme.ReVancedManagerTheme
|
||||||
|
import app.revanced.manager.ui.theme.Theme
|
||||||
|
import app.revanced.manager.ui.viewmodel.MainViewModel
|
||||||
|
import app.revanced.manager.ui.viewmodel.SelectedAppInfoViewModel
|
||||||
|
import app.revanced.manager.util.EventEffect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.androidx.compose.navigation.koinNavViewModel
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.getViewModel as getActivityViewModel
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
installSplashScreen()
|
||||||
|
|
||||||
|
val vm: MainViewModel = getActivityViewModel()
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult(),
|
||||||
|
onResult = vm::applyLegacySettings
|
||||||
|
)
|
||||||
|
val theme by vm.prefs.theme.getAsState()
|
||||||
|
val dynamicColor by vm.prefs.dynamicColor.getAsState()
|
||||||
|
|
||||||
|
EventEffect(vm.legacyImportActivityFlow) {
|
||||||
|
try {
|
||||||
|
launcher.launch(it)
|
||||||
|
} catch (_: ActivityNotFoundException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReVancedManagerTheme(
|
||||||
|
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||||
|
dynamicColor = dynamicColor
|
||||||
|
) {
|
||||||
|
ReVancedManager(vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReVancedManager(vm: MainViewModel) {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
|
||||||
|
EventEffect(vm.appSelectFlow) { app ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo,
|
||||||
|
SelectedApplicationInfo.ViewModelParams(app)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavHost(
|
||||||
|
navController = navController,
|
||||||
|
startDestination = Dashboard,
|
||||||
|
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
|
||||||
|
exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) },
|
||||||
|
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) },
|
||||||
|
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) },
|
||||||
|
) {
|
||||||
|
composable<Dashboard> {
|
||||||
|
DashboardScreen(
|
||||||
|
onSettingsClick = { navController.navigate(Settings) },
|
||||||
|
onAppSelectorClick = {
|
||||||
|
navController.navigate(AppSelector)
|
||||||
|
},
|
||||||
|
onUpdateClick = {
|
||||||
|
navController.navigate(Update())
|
||||||
|
},
|
||||||
|
onDownloaderPluginClick = {
|
||||||
|
navController.navigate(Settings.Downloads)
|
||||||
|
},
|
||||||
|
onAppClick = { packageName ->
|
||||||
|
navController.navigate(InstalledApplicationInfo(packageName))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<InstalledApplicationInfo> {
|
||||||
|
val data = it.toRoute<InstalledApplicationInfo>()
|
||||||
|
|
||||||
|
InstalledAppInfoScreen(
|
||||||
|
onPatchClick = vm::selectApp,
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
viewModel = koinViewModel { parametersOf(data.packageName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<AppSelector> {
|
||||||
|
AppSelectorScreen(
|
||||||
|
onSelect = vm::selectApp,
|
||||||
|
onStorageSelect = vm::selectApp,
|
||||||
|
onBackClick = navController::popBackStack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Patcher> {
|
||||||
|
PatcherScreen(
|
||||||
|
onBackClick = {
|
||||||
|
navController.navigate(route = Dashboard) {
|
||||||
|
launchSingleTop = true
|
||||||
|
popUpTo<Dashboard> {
|
||||||
|
inclusive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(it.getComplexArg<Patcher.ViewModelParams>()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Update> {
|
||||||
|
val data = it.toRoute<Update>()
|
||||||
|
|
||||||
|
UpdateScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
vm = koinViewModel { parametersOf(data.downloadOnScreenEntry) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation<SelectedApplicationInfo>(startDestination = SelectedApplicationInfo.Main) {
|
||||||
|
composable<SelectedApplicationInfo.Main> {
|
||||||
|
val parentBackStackEntry = navController.navGraphEntry(it)
|
||||||
|
val data =
|
||||||
|
parentBackStackEntry.getComplexArg<SelectedApplicationInfo.ViewModelParams>()
|
||||||
|
val viewModel =
|
||||||
|
koinNavViewModel<SelectedAppInfoViewModel>(viewModelStoreOwner = parentBackStackEntry) {
|
||||||
|
parametersOf(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedAppInfoScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onPatchClick = {
|
||||||
|
it.lifecycleScope.launch {
|
||||||
|
navController.navigateComplex(
|
||||||
|
Patcher,
|
||||||
|
viewModel.getPatcherParams()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPatchSelectorClick = { app, patches, options ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo.PatchesSelector,
|
||||||
|
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||||
|
app,
|
||||||
|
patches,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onRequiredOptions = { app, patches, options ->
|
||||||
|
navController.navigateComplex(
|
||||||
|
SelectedApplicationInfo.RequiredOptions,
|
||||||
|
SelectedApplicationInfo.PatchesSelector.ViewModelParams(
|
||||||
|
app,
|
||||||
|
patches,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
vm = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedApplicationInfo.PatchesSelector> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
PatchesSelectorScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onSave = { patches, options ->
|
||||||
|
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||||
|
navController.popBackStack()
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<SelectedApplicationInfo.RequiredOptions> {
|
||||||
|
val data =
|
||||||
|
it.getComplexArg<SelectedApplicationInfo.PatchesSelector.ViewModelParams>()
|
||||||
|
val selectedAppInfoVm = koinNavViewModel<SelectedAppInfoViewModel>(
|
||||||
|
viewModelStoreOwner = navController.navGraphEntry(it)
|
||||||
|
)
|
||||||
|
|
||||||
|
RequiredOptionsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onContinue = { patches, options ->
|
||||||
|
selectedAppInfoVm.updateConfiguration(patches, options)
|
||||||
|
it.lifecycleScope.launch {
|
||||||
|
navController.navigateComplex(
|
||||||
|
Patcher,
|
||||||
|
selectedAppInfoVm.getPatcherParams()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
vm = koinViewModel { parametersOf(data) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navigation<Settings>(startDestination = Settings.Main) {
|
||||||
|
composable<Settings.Main> {
|
||||||
|
SettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
navigate = navController::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.General> {
|
||||||
|
GeneralSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Advanced> {
|
||||||
|
AdvancedSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Developer> {
|
||||||
|
DeveloperSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Updates> {
|
||||||
|
UpdatesSettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onChangelogClick = { navController.navigate(Settings.Changelogs) },
|
||||||
|
onUpdateClick = { navController.navigate(Update()) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Downloads> {
|
||||||
|
DownloadsSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.ImportExport> {
|
||||||
|
ImportExportSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.About> {
|
||||||
|
AboutSettingsScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
navigate = navController::navigate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Changelogs> {
|
||||||
|
ChangelogsSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Contributors> {
|
||||||
|
ContributorSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<Settings.Licenses> {
|
||||||
|
LicensesSettingsScreen(onBackClick = navController::popBackStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavController.navGraphEntry(entry: NavBackStackEntry) =
|
||||||
|
remember(entry) { getBackStackEntry(entry.destination.parent!!.id) }
|
||||||
|
|
||||||
|
// Androidx Navigation does not support storing complex types in route objects, so we have to store them inside the saved state handle of the back stack entry instead.
|
||||||
|
private fun <T : Parcelable, R : ComplexParameter<T>> NavController.navigateComplex(
|
||||||
|
route: R,
|
||||||
|
data: T
|
||||||
|
) {
|
||||||
|
navigate(route)
|
||||||
|
getBackStackEntry(route).savedStateHandle["args"] = data
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T : Parcelable> NavBackStackEntry.getComplexArg() = savedStateHandle.get<T>("args")!!
|
110
app/src/main/java/app/revanced/manager/ManagerApplication.kt
Normal file
110
app/src/main/java/app/revanced/manager/ManagerApplication.kt
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package app.revanced.manager
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
|
import app.revanced.manager.di.*
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.domain.repository.DownloaderPluginRepository
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundleRepository
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import coil.Coil
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.internal.BuilderImpl
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||||
|
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.android.ext.koin.androidLogger
|
||||||
|
import org.koin.androidx.workmanager.koin.workManagerFactory
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
|
||||||
|
class ManagerApplication : Application() {
|
||||||
|
private val scope = MainScope()
|
||||||
|
private val prefs: PreferencesManager by inject()
|
||||||
|
private val patchBundleRepository: PatchBundleRepository by inject()
|
||||||
|
private val downloaderPluginRepository: DownloaderPluginRepository by inject()
|
||||||
|
private val fs: Filesystem by inject()
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidContext(this@ManagerApplication)
|
||||||
|
androidLogger()
|
||||||
|
workManagerFactory()
|
||||||
|
modules(
|
||||||
|
httpModule,
|
||||||
|
preferencesModule,
|
||||||
|
repositoryModule,
|
||||||
|
serviceModule,
|
||||||
|
managerModule,
|
||||||
|
workerModule,
|
||||||
|
viewModelModule,
|
||||||
|
databaseModule,
|
||||||
|
rootModule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pixels = 512
|
||||||
|
Coil.setImageLoader(
|
||||||
|
ImageLoader.Builder(this)
|
||||||
|
.components {
|
||||||
|
add(AppIconKeyer())
|
||||||
|
add(AppIconFetcher.Factory(pixels, true, this@ManagerApplication))
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val shellBuilder = BuilderImpl.create().setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
|
Shell.setDefaultBuilder(shellBuilder)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
prefs.preload()
|
||||||
|
}
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
downloaderPluginRepository.reload()
|
||||||
|
}
|
||||||
|
scope.launch(Dispatchers.Default) {
|
||||||
|
with(patchBundleRepository) {
|
||||||
|
reload()
|
||||||
|
updateCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
|
||||||
|
private var firstActivityCreated = false
|
||||||
|
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
if (firstActivityCreated) return
|
||||||
|
firstActivityCreated = true
|
||||||
|
|
||||||
|
// We do not want to call onFreshProcessStart() if there is state to restore.
|
||||||
|
// This can happen on system-initiated process death.
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
Log.d(tag, "Fresh process created")
|
||||||
|
onFreshProcessStart()
|
||||||
|
} else Log.d(tag, "System-initiated process death detected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityStarted(activity: Activity) {}
|
||||||
|
override fun onActivityResumed(activity: Activity) {}
|
||||||
|
override fun onActivityPaused(activity: Activity) {}
|
||||||
|
override fun onActivityStopped(activity: Activity) {}
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onFreshProcessStart() {
|
||||||
|
fs.uiTempDir.apply {
|
||||||
|
deleteRecursively()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package app.revanced.manager.data.platform
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import app.revanced.manager.util.RequestManageStorageContract
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class Filesystem(private val app: Application) {
|
||||||
|
val contentResolver = app.contentResolver // TODO: move Content Resolver operations to here.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directory that gets cleared when the app restarts.
|
||||||
|
* Do not store paths to this directory in a parcel.
|
||||||
|
*/
|
||||||
|
val tempDir: File = app.getDir("ephemeral", Context.MODE_PRIVATE).apply {
|
||||||
|
deleteRecursively()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directory for storing temporary files related to UI.
|
||||||
|
* This is the same as [tempDir], but does not get cleared on system-initiated process death.
|
||||||
|
* Paths to this directory can be safely stored in parcels.
|
||||||
|
*/
|
||||||
|
val uiTempDir: File = app.getDir("ui_ephemeral", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun externalFilesDir(): Path = Environment.getExternalStorageDirectory().toPath()
|
||||||
|
|
||||||
|
private fun usesManagePermission() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
|
||||||
|
|
||||||
|
private val storagePermissionName =
|
||||||
|
if (usesManagePermission()) Manifest.permission.MANAGE_EXTERNAL_STORAGE else Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
|
|
||||||
|
fun permissionContract(): Pair<ActivityResultContract<String, Boolean>, String> {
|
||||||
|
val contract =
|
||||||
|
if (usesManagePermission()) RequestManageStorageContract() else ActivityResultContracts.RequestPermission()
|
||||||
|
return contract to storagePermissionName
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasStoragePermission() =
|
||||||
|
if (usesManagePermission()) Environment.isExternalStorageManager() else app.checkSelfPermission(
|
||||||
|
storagePermissionName
|
||||||
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.manager.data.platform
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
|
||||||
|
class NetworkInfo(app: Application) {
|
||||||
|
private val connectivityManager = app.getSystemService<ConnectivityManager>()!!
|
||||||
|
|
||||||
|
private fun getCapabilities() = connectivityManager.activeNetwork?.let { connectivityManager.getNetworkCapabilities(it) }
|
||||||
|
fun isConnected() = connectivityManager.activeNetwork != null
|
||||||
|
fun isUnmetered() = getCapabilities()?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) ?: true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if it is safe to download large files.
|
||||||
|
*/
|
||||||
|
fun isSafe() = isConnected() && isUnmetered()
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package app.revanced.manager.data.room
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedAppDao
|
||||||
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||||
|
import app.revanced.manager.data.room.apps.installed.AppliedPatch
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstalledAppDao
|
||||||
|
import app.revanced.manager.data.room.selection.PatchSelection
|
||||||
|
import app.revanced.manager.data.room.selection.SelectedPatch
|
||||||
|
import app.revanced.manager.data.room.selection.SelectionDao
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleDao
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.data.room.options.Option
|
||||||
|
import app.revanced.manager.data.room.options.OptionDao
|
||||||
|
import app.revanced.manager.data.room.options.OptionGroup
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPluginDao
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [PatchBundleEntity::class, PatchSelection::class, SelectedPatch::class, DownloadedApp::class, InstalledApp::class, AppliedPatch::class, OptionGroup::class, Option::class, TrustedDownloaderPlugin::class],
|
||||||
|
version = 1
|
||||||
|
)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun patchBundleDao(): PatchBundleDao
|
||||||
|
abstract fun selectionDao(): SelectionDao
|
||||||
|
abstract fun downloadedAppDao(): DownloadedAppDao
|
||||||
|
abstract fun installedAppDao(): InstalledAppDao
|
||||||
|
abstract fun optionDao(): OptionDao
|
||||||
|
abstract fun trustedDownloaderPluginDao(): TrustedDownloaderPluginDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun generateUid() = Random.Default.nextInt()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.manager.data.room
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
|
import app.revanced.manager.data.room.options.Option.SerializedValue
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun sourceFromString(value: String) = Source.from(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun sourceToString(value: Source) = value.toString()
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fileFromString(value: String) = File(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fileToString(file: File): String = file.path
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun serializedOptionFromString(value: String) = SerializedValue.fromJsonString(value)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun serializedOptionToString(value: SerializedValue) = value.toJsonString()
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.downloaded
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "downloaded_app",
|
||||||
|
primaryKeys = ["package_name", "version"]
|
||||||
|
)
|
||||||
|
data class DownloadedApp(
|
||||||
|
@ColumnInfo(name = "package_name") val packageName: String,
|
||||||
|
@ColumnInfo(name = "version") val version: String,
|
||||||
|
@ColumnInfo(name = "directory") val directory: File,
|
||||||
|
@ColumnInfo(name = "last_used") val lastUsed: Long = System.currentTimeMillis()
|
||||||
|
)
|
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.downloaded
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DownloadedAppDao {
|
||||||
|
@Query("SELECT * FROM downloaded_app")
|
||||||
|
fun getAllApps(): Flow<List<DownloadedApp>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM downloaded_app WHERE package_name = :packageName AND version = :version")
|
||||||
|
suspend fun get(packageName: String, version: String): DownloadedApp?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsert(downloadedApp: DownloadedApp)
|
||||||
|
|
||||||
|
@Query("UPDATE downloaded_app SET last_used = :newValue WHERE package_name = :packageName AND version = :version")
|
||||||
|
suspend fun markUsed(packageName: String, version: String, newValue: Long = System.currentTimeMillis())
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(downloadedApps: Collection<DownloadedApp>)
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.installed
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
@Entity(
|
||||||
|
tableName = "applied_patch",
|
||||||
|
primaryKeys = ["package_name", "bundle", "patch_name"],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
InstalledApp::class,
|
||||||
|
parentColumns = ["current_package_name"],
|
||||||
|
childColumns = ["package_name"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
),
|
||||||
|
ForeignKey(
|
||||||
|
PatchBundleEntity::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["bundle"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
],
|
||||||
|
indices = [Index(value = ["bundle"], unique = false)]
|
||||||
|
)
|
||||||
|
data class AppliedPatch(
|
||||||
|
@ColumnInfo(name = "package_name") val packageName: String,
|
||||||
|
@ColumnInfo(name = "bundle") val bundle: Int,
|
||||||
|
@ColumnInfo(name = "patch_name") val patchName: String
|
||||||
|
) : Parcelable
|
@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.installed
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import app.revanced.manager.R
|
||||||
|
|
||||||
|
enum class InstallType(val stringResource: Int) {
|
||||||
|
DEFAULT(R.string.default_install),
|
||||||
|
MOUNT(R.string.mount_install)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity(tableName = "installed_app")
|
||||||
|
data class InstalledApp(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "current_package_name") val currentPackageName: String,
|
||||||
|
@ColumnInfo(name = "original_package_name") val originalPackageName: String,
|
||||||
|
@ColumnInfo(name = "version") val version: String,
|
||||||
|
@ColumnInfo(name = "install_type") val installType: InstallType
|
||||||
|
)
|
@ -0,0 +1,46 @@
|
|||||||
|
package app.revanced.manager.data.room.apps.installed
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.MapColumn
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface InstalledAppDao {
|
||||||
|
@Query("SELECT * FROM installed_app")
|
||||||
|
fun getAll(): Flow<List<InstalledApp>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM installed_app WHERE current_package_name = :packageName")
|
||||||
|
suspend fun get(packageName: String): InstalledApp?
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT bundle, patch_name FROM applied_patch" +
|
||||||
|
" WHERE package_name = :packageName"
|
||||||
|
)
|
||||||
|
suspend fun getPatchesSelection(packageName: String): Map<@MapColumn("bundle") Int, List<@MapColumn(
|
||||||
|
"patch_name"
|
||||||
|
) String>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun upsertApp(installedApp: InstalledApp, appliedPatches: List<AppliedPatch>) {
|
||||||
|
upsertApp(installedApp)
|
||||||
|
deleteAppliedPatches(installedApp.currentPackageName)
|
||||||
|
insertAppliedPatches(appliedPatches)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsertApp(installedApp: InstalledApp)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertAppliedPatches(appliedPatches: List<AppliedPatch>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM applied_patch WHERE package_name = :packageName")
|
||||||
|
suspend fun deleteAppliedPatches(packageName: String)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(installedApp: InstalledApp)
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package app.revanced.manager.data.room.bundles
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PatchBundleDao {
|
||||||
|
@Query("SELECT * FROM patch_bundles")
|
||||||
|
suspend fun all(): List<PatchBundleEntity>
|
||||||
|
|
||||||
|
@Query("SELECT version, auto_update FROM patch_bundles WHERE uid = :uid")
|
||||||
|
fun getPropsById(uid: Int): Flow<BundleProperties?>
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET version = :patches WHERE uid = :uid")
|
||||||
|
suspend fun updateVersion(uid: Int, patches: String?)
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET auto_update = :value WHERE uid = :uid")
|
||||||
|
suspend fun setAutoUpdate(uid: Int, value: Boolean)
|
||||||
|
|
||||||
|
@Query("UPDATE patch_bundles SET name = :value WHERE uid = :uid")
|
||||||
|
suspend fun setName(uid: Int, value: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_bundles WHERE uid != 0")
|
||||||
|
suspend fun purgeCustomBundles()
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun reset() {
|
||||||
|
purgeCustomBundles()
|
||||||
|
updateVersion(0, null) // Reset the main source
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_bundles WHERE uid = :uid")
|
||||||
|
suspend fun remove(uid: Int)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun add(source: PatchBundleEntity)
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package app.revanced.manager.data.room.bundles
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
|
||||||
|
sealed class Source {
|
||||||
|
object Local : Source() {
|
||||||
|
const val SENTINEL = "local"
|
||||||
|
|
||||||
|
override fun toString() = SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
|
object API : Source() {
|
||||||
|
const val SENTINEL = "api"
|
||||||
|
|
||||||
|
override fun toString() = SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Remote(val url: Url) : Source() {
|
||||||
|
override fun toString() = url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(value: String) = when (value) {
|
||||||
|
Local.SENTINEL -> Local
|
||||||
|
API.SENTINEL -> API
|
||||||
|
else -> Remote(Url(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity(tableName = "patch_bundles")
|
||||||
|
data class PatchBundleEntity(
|
||||||
|
@PrimaryKey val uid: Int,
|
||||||
|
@ColumnInfo(name = "name") val name: String,
|
||||||
|
@ColumnInfo(name = "version") val version: String? = null,
|
||||||
|
@ColumnInfo(name = "source") val source: Source,
|
||||||
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BundleProperties(
|
||||||
|
@ColumnInfo(name = "version") val version: String? = null,
|
||||||
|
@ColumnInfo(name = "auto_update") val autoUpdate: Boolean
|
||||||
|
)
|
@ -0,0 +1,116 @@
|
|||||||
|
package app.revanced.manager.data.room.options
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import app.revanced.manager.patcher.patch.Option
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.JsonNull
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.add
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.float
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.long
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "options",
|
||||||
|
primaryKeys = ["group", "patch_name", "key"],
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
OptionGroup::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["group"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
data class Option(
|
||||||
|
@ColumnInfo(name = "group") val group: Int,
|
||||||
|
@ColumnInfo(name = "patch_name") val patchName: String,
|
||||||
|
@ColumnInfo(name = "key") val key: String,
|
||||||
|
// Encoded as Json.
|
||||||
|
@ColumnInfo(name = "value") val value: SerializedValue,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class SerializedValue(val raw: JsonElement) {
|
||||||
|
fun toJsonString() = json.encodeToString(raw)
|
||||||
|
fun deserializeFor(option: Option<*>): Any? {
|
||||||
|
if (raw is JsonNull) return null
|
||||||
|
|
||||||
|
val errorMessage = "Cannot deserialize value as ${option.type}"
|
||||||
|
try {
|
||||||
|
if (option.type.classifier == List::class) {
|
||||||
|
val elementType = option.type.arguments.first().type!!
|
||||||
|
return raw.jsonArray.map { deserializeBasicType(elementType, it.jsonPrimitive) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return deserializeBasicType(option.type, raw.jsonPrimitive)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw SerializationException(errorMessage, e)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
throw SerializationException(errorMessage, e)
|
||||||
|
} catch (e: kotlinx.serialization.SerializationException) {
|
||||||
|
throw SerializationException(errorMessage, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val json = Json {
|
||||||
|
// Patcher does not forbid the use of these values, so we should support them.
|
||||||
|
allowSpecialFloatingPointValues = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deserializeBasicType(type: KType, value: JsonPrimitive) = when (type) {
|
||||||
|
typeOf<Boolean>() -> value.boolean
|
||||||
|
typeOf<Int>() -> value.int
|
||||||
|
typeOf<Long>() -> value.long
|
||||||
|
typeOf<Float>() -> value.float
|
||||||
|
typeOf<String>() -> value.content.also {
|
||||||
|
if (!value.isString) throw SerializationException(
|
||||||
|
"Expected value to be a string: $value"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw SerializationException("Unknown type: $type")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromJsonString(value: String) = SerializedValue(json.decodeFromString(value))
|
||||||
|
fun fromValue(value: Any?) = SerializedValue(when (value) {
|
||||||
|
null -> JsonNull
|
||||||
|
is Number -> JsonPrimitive(value)
|
||||||
|
is Boolean -> JsonPrimitive(value)
|
||||||
|
is String -> JsonPrimitive(value)
|
||||||
|
is List<*> -> buildJsonArray {
|
||||||
|
var elementClass: KClass<out Any>? = null
|
||||||
|
|
||||||
|
value.forEach {
|
||||||
|
when (it) {
|
||||||
|
null -> throw SerializationException("List elements must not be null")
|
||||||
|
is Number -> add(it)
|
||||||
|
is Boolean -> add(it)
|
||||||
|
is String -> add(it)
|
||||||
|
else -> throw SerializationException("Unknown element type: ${it::class.simpleName}")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementClass == null) elementClass = it::class
|
||||||
|
else if (elementClass != it::class) throw SerializationException("List elements must have the same type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> throw SerializationException("Unknown type: ${value::class.simpleName}")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SerializationException(message: String, cause: Throwable? = null) :
|
||||||
|
Exception(message, cause)
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package app.revanced.manager.data.room.options
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.MapColumn
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class OptionDao {
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT patch_bundle, `group`, patch_name, `key`, value FROM option_groups" +
|
||||||
|
" LEFT JOIN options ON uid = options.`group`" +
|
||||||
|
" WHERE package_name = :packageName"
|
||||||
|
)
|
||||||
|
abstract suspend fun getOptions(packageName: String): Map<@MapColumn("patch_bundle") Int, List<Option>>
|
||||||
|
|
||||||
|
@Query("SELECT uid FROM option_groups WHERE patch_bundle = :bundleUid AND package_name = :packageName")
|
||||||
|
abstract suspend fun getGroupId(bundleUid: Int, packageName: String): Int?
|
||||||
|
|
||||||
|
@Query("SELECT package_name FROM option_groups")
|
||||||
|
abstract fun getPackagesWithOptions(): Flow<List<String>>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract suspend fun createOptionGroup(group: OptionGroup)
|
||||||
|
|
||||||
|
@Query("DELETE FROM option_groups WHERE patch_bundle = :uid")
|
||||||
|
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||||
|
|
||||||
|
@Query("DELETE FROM option_groups WHERE package_name = :packageName")
|
||||||
|
abstract suspend fun clearForPackage(packageName: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM option_groups")
|
||||||
|
abstract suspend fun reset()
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
protected abstract suspend fun insertOptions(patches: List<Option>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM options WHERE `group` = :groupId")
|
||||||
|
protected abstract suspend fun clearGroup(groupId: Int)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open suspend fun updateOptions(options: Map<Int, List<Option>>) =
|
||||||
|
options.forEach { (groupId, options) ->
|
||||||
|
clearGroup(groupId)
|
||||||
|
insertOptions(options)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.manager.data.room.options
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "option_groups",
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
PatchBundleEntity::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["patch_bundle"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)],
|
||||||
|
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
|
||||||
|
)
|
||||||
|
data class OptionGroup(
|
||||||
|
@PrimaryKey val uid: Int,
|
||||||
|
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
|
||||||
|
@ColumnInfo(name = "package_name") val packageName: String
|
||||||
|
)
|
@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "trusted_downloader_plugins")
|
||||||
|
class TrustedDownloaderPlugin(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "package_name") val packageName: String,
|
||||||
|
@ColumnInfo(name = "signature") val signature: ByteArray
|
||||||
|
)
|
@ -0,0 +1,22 @@
|
|||||||
|
package app.revanced.manager.data.room.plugins
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface TrustedDownloaderPluginDao {
|
||||||
|
@Query("SELECT signature FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun getTrustedSignature(packageName: String): ByteArray?
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
suspend fun upsertTrust(plugin: TrustedDownloaderPlugin)
|
||||||
|
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name = :packageName")
|
||||||
|
suspend fun remove(packageName: String)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("DELETE FROM trusted_downloader_plugins WHERE package_name IN (:packageNames)")
|
||||||
|
suspend fun removeAll(packageNames: Set<String>)
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package app.revanced.manager.data.room.selection
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "patch_selections",
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
PatchBundleEntity::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["patch_bundle"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)],
|
||||||
|
indices = [Index(value = ["patch_bundle", "package_name"], unique = true)]
|
||||||
|
)
|
||||||
|
data class PatchSelection(
|
||||||
|
@PrimaryKey val uid: Int,
|
||||||
|
@ColumnInfo(name = "patch_bundle") val patchBundle: Int,
|
||||||
|
@ColumnInfo(name = "package_name") val packageName: String
|
||||||
|
)
|
@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.manager.data.room.selection
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "selected_patches",
|
||||||
|
primaryKeys = ["selection", "patch_name"],
|
||||||
|
foreignKeys = [ForeignKey(
|
||||||
|
PatchSelection::class,
|
||||||
|
parentColumns = ["uid"],
|
||||||
|
childColumns = ["selection"],
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
data class SelectedPatch(
|
||||||
|
@ColumnInfo(name = "selection") val selection: Int,
|
||||||
|
@ColumnInfo(name = "patch_name") val patchName: String
|
||||||
|
)
|
@ -0,0 +1,58 @@
|
|||||||
|
package app.revanced.manager.data.room.selection
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.MapColumn
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class SelectionDao {
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT patch_bundle, patch_name FROM patch_selections" +
|
||||||
|
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
|
||||||
|
" WHERE package_name = :packageName"
|
||||||
|
)
|
||||||
|
abstract suspend fun getSelectedPatches(packageName: String): Map<@MapColumn("patch_bundle") Int, List<@MapColumn(
|
||||||
|
"patch_name"
|
||||||
|
) String>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT package_name, patch_name FROM patch_selections" +
|
||||||
|
" LEFT JOIN selected_patches ON uid = selected_patches.selection" +
|
||||||
|
" WHERE patch_bundle = :bundleUid"
|
||||||
|
)
|
||||||
|
abstract suspend fun exportSelection(bundleUid: Int): Map<@MapColumn("package_name") String, List<@MapColumn(
|
||||||
|
"patch_name"
|
||||||
|
) String>>
|
||||||
|
|
||||||
|
@Query("SELECT uid FROM patch_selections WHERE patch_bundle = :bundleUid AND package_name = :packageName")
|
||||||
|
abstract suspend fun getSelectionId(bundleUid: Int, packageName: String): Int?
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract suspend fun createSelection(selection: PatchSelection)
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_selections WHERE patch_bundle = :uid")
|
||||||
|
abstract suspend fun clearForPatchBundle(uid: Int)
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_selections WHERE package_name = :packageName")
|
||||||
|
abstract suspend fun clearForPackage(packageName: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM patch_selections")
|
||||||
|
abstract suspend fun reset()
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
protected abstract suspend fun selectPatches(patches: List<SelectedPatch>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM selected_patches WHERE selection = :selectionId")
|
||||||
|
protected abstract suspend fun clearSelection(selectionId: Int)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open suspend fun updateSelections(selections: Map<Int, Set<String>>) =
|
||||||
|
selections.forEach { (selectionUid, patches) ->
|
||||||
|
clearSelection(selectionUid)
|
||||||
|
selectPatches(patches.map { SelectedPatch(selectionUid, it) })
|
||||||
|
}
|
||||||
|
}
|
15
app/src/main/java/app/revanced/manager/di/DatabaseModule.kt
Normal file
15
app/src/main/java/app/revanced/manager/di/DatabaseModule.kt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val databaseModule = module {
|
||||||
|
fun provideAppDatabase(context: Context) = Room.databaseBuilder(context, AppDatabase::class.java, "manager").build()
|
||||||
|
|
||||||
|
single {
|
||||||
|
provideAppDatabase(androidContext())
|
||||||
|
}
|
||||||
|
}
|
60
app/src/main/java/app/revanced/manager/di/HttpModule.kt
Normal file
60
app/src/main/java/app/revanced/manager/di/HttpModule.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.revanced.manager.BuildConfig
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.okhttp.*
|
||||||
|
import io.ktor.client.plugins.HttpTimeout
|
||||||
|
import io.ktor.client.plugins.UserAgent
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Cache
|
||||||
|
import okhttp3.Dns
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
val httpModule = module {
|
||||||
|
fun provideHttpClient(context: Context, json: Json) = HttpClient(OkHttp) {
|
||||||
|
engine {
|
||||||
|
config {
|
||||||
|
dns(object : Dns {
|
||||||
|
override fun lookup(hostname: String): List<InetAddress> {
|
||||||
|
val addresses = Dns.SYSTEM.lookup(hostname)
|
||||||
|
return if (hostname == "raw.githubusercontent.com") {
|
||||||
|
addresses.filterIsInstance<Inet4Address>()
|
||||||
|
} else {
|
||||||
|
addresses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
cache(Cache(context.cacheDir.resolve("cache").also { it.mkdirs() }, 1024 * 1024 * 100))
|
||||||
|
followRedirects(true)
|
||||||
|
followSslRedirects(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(json)
|
||||||
|
}
|
||||||
|
install(HttpTimeout) {
|
||||||
|
socketTimeoutMillis = 10000
|
||||||
|
}
|
||||||
|
install(UserAgent) {
|
||||||
|
agent = "ReVanced-Manager/${BuildConfig.VERSION_CODE}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun provideJson() = Json {
|
||||||
|
encodeDefaults = true
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
single {
|
||||||
|
provideHttpClient(androidContext(), get())
|
||||||
|
}
|
||||||
|
singleOf(::provideJson)
|
||||||
|
}
|
11
app/src/main/java/app/revanced/manager/di/ManagerModule.kt
Normal file
11
app/src/main/java/app/revanced/manager/di/ManagerModule.kt
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.domain.manager.KeystoreManager
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val managerModule = module {
|
||||||
|
singleOf(::KeystoreManager)
|
||||||
|
singleOf(::PM)
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val preferencesModule = module {
|
||||||
|
singleOf(::PreferencesManager)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.data.platform.Filesystem
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
|
import app.revanced.manager.domain.repository.*
|
||||||
|
import app.revanced.manager.domain.worker.WorkerRepository
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
|
import org.koin.core.module.dsl.createdAtStart
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val repositoryModule = module {
|
||||||
|
singleOf(::ReVancedAPI)
|
||||||
|
singleOf(::Filesystem) {
|
||||||
|
createdAtStart()
|
||||||
|
}
|
||||||
|
singleOf(::NetworkInfo)
|
||||||
|
singleOf(::PatchBundlePersistenceRepository)
|
||||||
|
singleOf(::PatchSelectionRepository)
|
||||||
|
singleOf(::PatchOptionsRepository)
|
||||||
|
singleOf(::PatchBundleRepository) {
|
||||||
|
// It is best to load patch bundles ASAP
|
||||||
|
createdAtStart()
|
||||||
|
}
|
||||||
|
singleOf(::DownloaderPluginRepository)
|
||||||
|
singleOf(::WorkerRepository)
|
||||||
|
singleOf(::DownloadedAppRepository)
|
||||||
|
singleOf(::InstalledAppRepository)
|
||||||
|
}
|
9
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
9
app/src/main/java/app/revanced/manager/di/RootModule.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.domain.installer.RootInstaller
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val rootModule = module {
|
||||||
|
singleOf(::RootInstaller)
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import org.koin.core.module.dsl.singleOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val serviceModule = module {
|
||||||
|
singleOf(::HttpService)
|
||||||
|
}
|
26
app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
Normal file
26
app/src/main/java/app/revanced/manager/di/ViewModelModule.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.ui.viewmodel.*
|
||||||
|
import org.koin.androidx.viewmodel.dsl.viewModelOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val viewModelModule = module {
|
||||||
|
viewModelOf(::MainViewModel)
|
||||||
|
viewModelOf(::DashboardViewModel)
|
||||||
|
viewModelOf(::SelectedAppInfoViewModel)
|
||||||
|
viewModelOf(::PatchesSelectorViewModel)
|
||||||
|
viewModelOf(::GeneralSettingsViewModel)
|
||||||
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
|
viewModelOf(::AppSelectorViewModel)
|
||||||
|
viewModelOf(::PatcherViewModel)
|
||||||
|
viewModelOf(::UpdateViewModel)
|
||||||
|
viewModelOf(::ChangelogsViewModel)
|
||||||
|
viewModelOf(::ImportExportViewModel)
|
||||||
|
viewModelOf(::AboutViewModel)
|
||||||
|
viewModelOf(::DeveloperOptionsViewModel)
|
||||||
|
viewModelOf(::ContributorViewModel)
|
||||||
|
viewModelOf(::DownloadsViewModel)
|
||||||
|
viewModelOf(::InstalledAppsViewModel)
|
||||||
|
viewModelOf(::InstalledAppInfoViewModel)
|
||||||
|
viewModelOf(::UpdatesSettingsViewModel)
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package app.revanced.manager.di
|
||||||
|
|
||||||
|
import app.revanced.manager.patcher.worker.PatcherWorker
|
||||||
|
import org.koin.androidx.workmanager.dsl.workerOf
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val workerModule = module {
|
||||||
|
workerOf(::PatcherWorker)
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class LocalPatchBundle(name: String, id: Int, directory: File) :
|
||||||
|
PatchBundleSource(name, id, directory) {
|
||||||
|
suspend fun replace(patches: InputStream) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
patchBundleOutputStream().use { outputStream ->
|
||||||
|
patches.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()?.also {
|
||||||
|
saveVersion(it.readManifestAttribute("Version"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.domain.repository.PatchBundlePersistenceRepository
|
||||||
|
import app.revanced.manager.patcher.patch.PatchBundle
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchBundle] source.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
sealed class PatchBundleSource(initialName: String, val uid: Int, directory: File) : KoinComponent {
|
||||||
|
protected val configRepository: PatchBundlePersistenceRepository by inject()
|
||||||
|
private val app: Application by inject()
|
||||||
|
protected val patchesFile = directory.resolve("patches.jar")
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(load())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _nameFlow = MutableStateFlow(initialName)
|
||||||
|
val nameFlow =
|
||||||
|
_nameFlow.map { it.ifEmpty { app.getString(if (isDefault) R.string.bundle_name_default else R.string.bundle_name_fallback) } }
|
||||||
|
|
||||||
|
suspend fun getName() = nameFlow.first()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the bundle has been downloaded to local storage.
|
||||||
|
*/
|
||||||
|
fun hasInstalled() = patchesFile.exists()
|
||||||
|
|
||||||
|
protected fun patchBundleOutputStream(): OutputStream = with(patchesFile) {
|
||||||
|
// Android 14+ requires dex containers to be readonly.
|
||||||
|
try {
|
||||||
|
setWritable(true, true)
|
||||||
|
outputStream()
|
||||||
|
} finally {
|
||||||
|
setReadOnly()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(): State {
|
||||||
|
if (!hasInstalled()) return State.Missing
|
||||||
|
|
||||||
|
return try {
|
||||||
|
State.Loaded(PatchBundle(patchesFile))
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load patch bundle with UID $uid", t)
|
||||||
|
State.Failed(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload(): PatchBundle? {
|
||||||
|
val newState = load()
|
||||||
|
_state.value = newState
|
||||||
|
|
||||||
|
val bundle = newState.patchBundleOrNull()
|
||||||
|
// Try to read the name from the patch bundle manifest if the bundle does not have a name.
|
||||||
|
if (bundle != null && _nameFlow.value.isEmpty()) {
|
||||||
|
bundle.readManifestAttribute("Name")?.let { setName(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a flow that emits the [app.revanced.manager.data.room.bundles.BundleProperties] of this [PatchBundleSource].
|
||||||
|
* The flow will emit null if the associated [PatchBundleSource] is deleted.
|
||||||
|
*/
|
||||||
|
fun propsFlow() = configRepository.getProps(uid).flowOn(Dispatchers.Default)
|
||||||
|
suspend fun getProps() = propsFlow().first()!!
|
||||||
|
|
||||||
|
suspend fun currentVersion() = getProps().version
|
||||||
|
protected suspend fun saveVersion(version: String?) =
|
||||||
|
configRepository.updateVersion(uid, version)
|
||||||
|
|
||||||
|
suspend fun setName(name: String) {
|
||||||
|
configRepository.setName(uid, name)
|
||||||
|
_nameFlow.value = name
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface State {
|
||||||
|
fun patchBundleOrNull(): PatchBundle? = null
|
||||||
|
|
||||||
|
data object Missing : State
|
||||||
|
data class Failed(val throwable: Throwable) : State
|
||||||
|
data class Loaded(val bundle: PatchBundle) : State {
|
||||||
|
override fun patchBundleOrNull() = bundle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Extensions {
|
||||||
|
val PatchBundleSource.isDefault inline get() = uid == 0
|
||||||
|
val PatchBundleSource.asRemoteOrNull inline get() = this as? RemotePatchBundle
|
||||||
|
val PatchBundleSource.nameState
|
||||||
|
@Composable inline get() = nameFlow.collectAsStateWithLifecycle(
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package app.revanced.manager.domain.bundles
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import app.revanced.manager.network.api.ReVancedAPI
|
||||||
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
sealed class RemotePatchBundle(name: String, id: Int, directory: File, val endpoint: String) :
|
||||||
|
PatchBundleSource(name, id, directory) {
|
||||||
|
protected val http: HttpService by inject()
|
||||||
|
|
||||||
|
protected abstract suspend fun getLatestInfo(): ReVancedAsset
|
||||||
|
|
||||||
|
private suspend fun download(info: ReVancedAsset) = withContext(Dispatchers.IO) {
|
||||||
|
patchBundleOutputStream().use {
|
||||||
|
http.streamTo(it) {
|
||||||
|
url(info.downloadUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVersion(info.version)
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadLatest() {
|
||||||
|
download(getLatestInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
val info = getLatestInfo()
|
||||||
|
if (hasInstalled() && info.version == currentVersion())
|
||||||
|
return@withContext false
|
||||||
|
|
||||||
|
download(info)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteLocalFiles() = withContext(Dispatchers.Default) {
|
||||||
|
patchesFile.delete()
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setAutoUpdate(value: Boolean) = configRepository.setAutoUpdate(uid, value)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val updateFailMsg = "Failed to update patch bundle(s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsonPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||||
|
RemotePatchBundle(name, id, directory, endpoint) {
|
||||||
|
override suspend fun getLatestInfo() = withContext(Dispatchers.IO) {
|
||||||
|
http.request<ReVancedAsset> {
|
||||||
|
url(endpoint)
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class APIPatchBundle(name: String, id: Int, directory: File, endpoint: String) :
|
||||||
|
RemotePatchBundle(name, id, directory, endpoint) {
|
||||||
|
private val api: ReVancedAPI by inject()
|
||||||
|
|
||||||
|
override suspend fun getLatestInfo() = api.getPatchesUpdate().getOrThrow()
|
||||||
|
}
|
@ -0,0 +1,181 @@
|
|||||||
|
package app.revanced.manager.domain.installer
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.os.IBinder
|
||||||
|
import app.revanced.manager.IRootSystemService
|
||||||
|
import app.revanced.manager.service.ManagerRootService
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.time.withTimeoutOrNull
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
class RootInstaller(
|
||||||
|
private val app: Application,
|
||||||
|
private val pm: PM
|
||||||
|
) : ServiceConnection {
|
||||||
|
private var remoteFS = CompletableDeferred<FileSystemManager>()
|
||||||
|
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
val ipc = IRootSystemService.Stub.asInterface(service)
|
||||||
|
val binder = ipc.fileSystemService
|
||||||
|
|
||||||
|
remoteFS.complete(FileSystemManager.getRemote(binder))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
remoteFS = CompletableDeferred()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun awaitRemoteFS(): FileSystemManager {
|
||||||
|
if (remoteFS.isActive) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val intent = Intent(app, ManagerRootService::class.java)
|
||||||
|
RootService.bind(intent, this@RootInstaller)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return withTimeoutOrNull(Duration.ofSeconds(20L)) {
|
||||||
|
remoteFS.await()
|
||||||
|
} ?: throw RootServiceException()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getShell() = with(CompletableDeferred<Shell>()) {
|
||||||
|
Shell.getShell(::complete)
|
||||||
|
|
||||||
|
await()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun execute(vararg commands: String) = getShell().newJob().add(*commands).exec()
|
||||||
|
|
||||||
|
fun hasRootAccess() = Shell.isAppGrantedRoot() ?: false
|
||||||
|
|
||||||
|
fun isDeviceRooted() = System.getenv("PATH")?.split(":")?.any { path ->
|
||||||
|
File(path, "su").canExecute()
|
||||||
|
} ?: false
|
||||||
|
|
||||||
|
suspend fun isAppInstalled(packageName: String) =
|
||||||
|
awaitRemoteFS().getFile("$modulesPath/$packageName-revanced").exists()
|
||||||
|
|
||||||
|
suspend fun isAppMounted(packageName: String) = withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir?.let {
|
||||||
|
execute("mount | grep \"$it\"").isSuccess
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun mount(packageName: String) {
|
||||||
|
if (isAppMounted(packageName)) return
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
val patchedAPK = "$modulesPath/$packageName-revanced/$packageName.apk"
|
||||||
|
|
||||||
|
execute("mount -o bind \"$patchedAPK\" \"$stockAPK\"").assertSuccess("Failed to mount APK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun unmount(packageName: String) {
|
||||||
|
if (!isAppMounted(packageName)) return
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val stockAPK = pm.getPackageInfo(packageName)?.applicationInfo?.sourceDir
|
||||||
|
?: throw Exception("Failed to load application info")
|
||||||
|
|
||||||
|
execute("umount -l \"$stockAPK\"").assertSuccess("Failed to unmount APK")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun install(
|
||||||
|
patchedAPK: File,
|
||||||
|
stockAPK: File?,
|
||||||
|
packageName: String,
|
||||||
|
version: String,
|
||||||
|
label: String
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
val remoteFS = awaitRemoteFS()
|
||||||
|
val assets = app.assets
|
||||||
|
val modulePath = "$modulesPath/$packageName-revanced"
|
||||||
|
|
||||||
|
unmount(packageName)
|
||||||
|
|
||||||
|
stockAPK?.let { stockApp ->
|
||||||
|
pm.getPackageInfo(packageName)?.let { packageInfo ->
|
||||||
|
// TODO: get user id programmatically
|
||||||
|
if (pm.getVersionCode(packageInfo) <= pm.getVersionCode(
|
||||||
|
pm.getPackageInfo(patchedAPK)
|
||||||
|
?: error("Failed to get package info for patched app")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
execute("pm uninstall -k --user 0 $packageName").assertSuccess("Failed to uninstall stock app")
|
||||||
|
}
|
||||||
|
|
||||||
|
execute("pm install \"${stockApp.absolutePath}\"").assertSuccess("Failed to install stock app")
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteFS.getFile(modulePath).mkdir()
|
||||||
|
|
||||||
|
listOf(
|
||||||
|
"service.sh",
|
||||||
|
"module.prop",
|
||||||
|
).forEach { file ->
|
||||||
|
assets.open("root/$file").use { inputStream ->
|
||||||
|
remoteFS.getFile("$modulePath/$file").newOutputStream()
|
||||||
|
.use { outputStream ->
|
||||||
|
val content = String(inputStream.readBytes())
|
||||||
|
.replace("__PKG_NAME__", packageName)
|
||||||
|
.replace("__VERSION__", version)
|
||||||
|
.replace("__LABEL__", label)
|
||||||
|
.toByteArray()
|
||||||
|
|
||||||
|
outputStream.write(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"$modulePath/$packageName.apk".let { apkPath ->
|
||||||
|
|
||||||
|
remoteFS.getFile(patchedAPK.absolutePath)
|
||||||
|
.also { if (!it.exists()) throw Exception("File doesn't exist") }
|
||||||
|
.newInputStream().use { inputStream ->
|
||||||
|
remoteFS.getFile(apkPath).newOutputStream().use { outputStream ->
|
||||||
|
inputStream.copyTo(outputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(
|
||||||
|
"chmod 644 $apkPath",
|
||||||
|
"chown system:system $apkPath",
|
||||||
|
"chcon u:object_r:apk_data_file:s0 $apkPath",
|
||||||
|
"chmod +x $modulePath/service.sh"
|
||||||
|
).assertSuccess("Failed to set file permissions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun uninstall(packageName: String) {
|
||||||
|
val remoteFS = awaitRemoteFS()
|
||||||
|
if (isAppMounted(packageName))
|
||||||
|
unmount(packageName)
|
||||||
|
|
||||||
|
remoteFS.getFile("$modulesPath/$packageName-revanced").deleteRecursively()
|
||||||
|
.also { if (!it) throw Exception("Failed to delete files") }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val modulesPath = "/data/adb/modules"
|
||||||
|
|
||||||
|
private fun Shell.Result.assertSuccess(errorMessage: String) {
|
||||||
|
if (!isSuccess) throw Exception(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootServiceException : Exception("Root not available")
|
@ -0,0 +1,95 @@
|
|||||||
|
package app.revanced.manager.domain.manager
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import app.revanced.library.ApkSigner
|
||||||
|
import app.revanced.library.ApkUtils
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.security.UnrecoverableKeyException
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
|
class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||||
|
companion object Constants {
|
||||||
|
/**
|
||||||
|
* Default alias and password for the keystore.
|
||||||
|
*/
|
||||||
|
const val DEFAULT = "ReVanced"
|
||||||
|
private val eightYearsFromNow get() = Date(System.currentTimeMillis() + (365.days * 8).inWholeMilliseconds * 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val keystorePath =
|
||||||
|
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore")
|
||||||
|
|
||||||
|
private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
|
||||||
|
prefs.keystoreCommonName.value = cn
|
||||||
|
prefs.keystorePass.value = pass
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun signingDetails(path: File = keystorePath) = ApkUtils.KeyStoreDetails(
|
||||||
|
keyStore = path,
|
||||||
|
keyStorePassword = null,
|
||||||
|
alias = prefs.keystoreCommonName.get(),
|
||||||
|
password = prefs.keystorePass.get()
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
||||||
|
ApkUtils.signApk(input, output, prefs.keystoreCommonName.get(), signingDetails())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
||||||
|
val keyCertPair = ApkSigner.newPrivateKeyCertificatePair(
|
||||||
|
prefs.keystoreCommonName.get(),
|
||||||
|
eightYearsFromNow
|
||||||
|
)
|
||||||
|
val ks = ApkSigner.newKeyStore(
|
||||||
|
setOf(
|
||||||
|
ApkSigner.KeyStoreEntry(
|
||||||
|
DEFAULT, DEFAULT, keyCertPair
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
keystorePath.outputStream().use {
|
||||||
|
ks.store(it, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePrefs(DEFAULT, DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun import(cn: String, pass: String, keystore: InputStream): Boolean {
|
||||||
|
val keystoreData = withContext(Dispatchers.IO) { keystore.readBytes() }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val ks = ApkSigner.readKeyStore(ByteArrayInputStream(keystoreData), null)
|
||||||
|
|
||||||
|
ApkSigner.readPrivateKeyCertificatePair(ks, cn, pass)
|
||||||
|
} catch (_: UnrecoverableKeyException) {
|
||||||
|
return false
|
||||||
|
} catch (_: IllegalArgumentException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Files.write(keystorePath.toPath(), keystoreData)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePrefs(cn, pass)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasKeystore() = keystorePath.exists()
|
||||||
|
|
||||||
|
suspend fun export(target: OutputStream) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Files.copy(keystorePath.toPath(), target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
package app.revanced.manager.domain.manager
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.revanced.manager.domain.manager.base.BasePreferencesManager
|
||||||
|
import app.revanced.manager.ui.theme.Theme
|
||||||
|
|
||||||
|
class PreferencesManager(
|
||||||
|
context: Context
|
||||||
|
) : BasePreferencesManager(context, "settings") {
|
||||||
|
val dynamicColor = booleanPreference("dynamic_color", true)
|
||||||
|
val theme = enumPreference("theme", Theme.SYSTEM)
|
||||||
|
|
||||||
|
val api = stringPreference("api_url", "https://api.revanced.app")
|
||||||
|
|
||||||
|
val useProcessRuntime = booleanPreference("use_process_runtime", false)
|
||||||
|
val patcherProcessMemoryLimit = intPreference("process_runtime_memory_limit", 700)
|
||||||
|
|
||||||
|
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||||
|
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||||
|
|
||||||
|
val firstLaunch = booleanPreference("first_launch", true)
|
||||||
|
val managerAutoUpdates = booleanPreference("manager_auto_updates", false)
|
||||||
|
val showManagerUpdateDialogOnLaunch = booleanPreference("show_manager_update_dialog_on_launch", true)
|
||||||
|
|
||||||
|
val disablePatchVersionCompatCheck = booleanPreference("disable_patch_version_compatibility_check", false)
|
||||||
|
val disableSelectionWarning = booleanPreference("disable_selection_warning", false)
|
||||||
|
val disableUniversalPatchWarning = booleanPreference("disable_universal_patch_warning", false)
|
||||||
|
val suggestedVersionSafeguard = booleanPreference("suggested_version_safeguard", true)
|
||||||
|
|
||||||
|
val acknowledgedDownloaderPlugins = stringSetPreference("acknowledged_downloader_plugins", emptySet())
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
package app.revanced.manager.domain.manager.base
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.*
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import app.revanced.manager.domain.manager.base.BasePreferencesManager.Companion.editor
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
abstract class BasePreferencesManager(private val context: Context, name: String) {
|
||||||
|
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = name)
|
||||||
|
protected val dataStore get() = context.dataStore
|
||||||
|
|
||||||
|
suspend fun preload() {
|
||||||
|
dataStore.data.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun edit(block: EditorContext.() -> Unit) = dataStore.editor(block)
|
||||||
|
|
||||||
|
protected fun stringPreference(key: String, default: String) =
|
||||||
|
StringPreference(dataStore, key, default)
|
||||||
|
|
||||||
|
protected fun stringSetPreference(key: String, default: Set<String>) =
|
||||||
|
StringSetPreference(dataStore, key, default)
|
||||||
|
|
||||||
|
protected fun booleanPreference(key: String, default: Boolean) =
|
||||||
|
BooleanPreference(dataStore, key, default)
|
||||||
|
|
||||||
|
protected fun intPreference(key: String, default: Int) = IntPreference(dataStore, key, default)
|
||||||
|
|
||||||
|
protected fun floatPreference(key: String, default: Float) =
|
||||||
|
FloatPreference(dataStore, key, default)
|
||||||
|
|
||||||
|
protected inline fun <reified E : Enum<E>> enumPreference(
|
||||||
|
key: String,
|
||||||
|
default: E
|
||||||
|
) = EnumPreference(dataStore, key, default, enumValues())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
suspend inline fun DataStore<Preferences>.editor(crossinline block: EditorContext.() -> Unit) {
|
||||||
|
edit {
|
||||||
|
EditorContext(it).run(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditorContext(private val prefs: MutablePreferences) {
|
||||||
|
var <T> Preference<T>.value
|
||||||
|
get() = prefs.run { read() }
|
||||||
|
set(value) = prefs.run { write(value) }
|
||||||
|
|
||||||
|
operator fun Preference<Set<String>>.plusAssign(value: String) = prefs.run {
|
||||||
|
write(read() + value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Preference<T>(
|
||||||
|
private val dataStore: DataStore<Preferences>,
|
||||||
|
val default: T
|
||||||
|
) {
|
||||||
|
internal abstract fun Preferences.read(): T
|
||||||
|
internal abstract fun MutablePreferences.write(value: T)
|
||||||
|
|
||||||
|
val flow = dataStore.data.map { with(it) { read() } ?: default }.distinctUntilChanged()
|
||||||
|
|
||||||
|
suspend fun get() = flow.first()
|
||||||
|
fun getBlocking() = runBlocking { get() }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
|
||||||
|
getBlocking()
|
||||||
|
})
|
||||||
|
|
||||||
|
suspend fun update(value: T) = dataStore.editor {
|
||||||
|
this@Preference.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnumPreference<E : Enum<E>>(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: E,
|
||||||
|
private val enumValues: Array<E>
|
||||||
|
) : Preference<E>(dataStore, default) {
|
||||||
|
private val key = stringPreferencesKey(key)
|
||||||
|
override fun Preferences.read() =
|
||||||
|
this[key]?.let { name ->
|
||||||
|
enumValues.find { it.name == name }
|
||||||
|
} ?: default
|
||||||
|
|
||||||
|
override fun MutablePreferences.write(value: E) {
|
||||||
|
this[key] = value.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class BasePreference<T>(dataStore: DataStore<Preferences>, default: T) :
|
||||||
|
Preference<T>(dataStore, default) {
|
||||||
|
protected abstract val key: Preferences.Key<T>
|
||||||
|
override fun Preferences.read() = this[key] ?: default
|
||||||
|
override fun MutablePreferences.write(value: T) {
|
||||||
|
this[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringPreference(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: String
|
||||||
|
) : BasePreference<String>(dataStore, default) {
|
||||||
|
override val key = stringPreferencesKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringSetPreference(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: Set<String>
|
||||||
|
) : BasePreference<Set<String>>(dataStore, default) {
|
||||||
|
override val key = stringSetPreferencesKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BooleanPreference(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: Boolean
|
||||||
|
) : BasePreference<Boolean>(dataStore, default) {
|
||||||
|
override val key = booleanPreferencesKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntPreference(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: Int
|
||||||
|
) : BasePreference<Int>(dataStore, default) {
|
||||||
|
override val key = intPreferencesKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
class FloatPreference(
|
||||||
|
dataStore: DataStore<Preferences>,
|
||||||
|
key: String,
|
||||||
|
default: Float
|
||||||
|
) : BasePreference<Float>(dataStore, default) {
|
||||||
|
override val key = floatPreferencesKey(key)
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Parcelable
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
|
import app.revanced.manager.data.room.apps.downloaded.DownloadedApp
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.plugin.downloader.OutputDownloadScope
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FilterOutputStream
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
import kotlin.io.path.outputStream
|
||||||
|
|
||||||
|
class DownloadedAppRepository(
|
||||||
|
private val app: Application,
|
||||||
|
db: AppDatabase,
|
||||||
|
private val pm: PM
|
||||||
|
) {
|
||||||
|
private val dir = app.getDir("downloaded-apps", Context.MODE_PRIVATE)
|
||||||
|
private val dao = db.downloadedAppDao()
|
||||||
|
|
||||||
|
fun getAll() = dao.getAllApps().distinctUntilChanged()
|
||||||
|
|
||||||
|
fun getApkFileForApp(app: DownloadedApp): File =
|
||||||
|
getApkFileForDir(dir.resolve(app.directory))
|
||||||
|
|
||||||
|
private fun getApkFileForDir(directory: File) = directory.listFiles()!!.first()
|
||||||
|
|
||||||
|
suspend fun download(
|
||||||
|
plugin: LoadedDownloaderPlugin,
|
||||||
|
data: Parcelable,
|
||||||
|
expectedPackageName: String,
|
||||||
|
expectedVersion: String?,
|
||||||
|
onDownload: suspend (downloadProgress: Pair<Long, Long?>) -> Unit,
|
||||||
|
): File {
|
||||||
|
// Converted integers cannot contain / or .. unlike the package name or version, so they are safer to use here.
|
||||||
|
val relativePath = File(generateUid().toString())
|
||||||
|
val saveDir = dir.resolve(relativePath).also { it.mkdirs() }
|
||||||
|
val targetFile = saveDir.resolve("base.apk").toPath()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val downloadSize = AtomicLong(0)
|
||||||
|
val downloadedBytes = AtomicLong(0)
|
||||||
|
|
||||||
|
channelFlow {
|
||||||
|
val scope = object : OutputDownloadScope {
|
||||||
|
override val pluginPackageName = plugin.packageName
|
||||||
|
override val hostPackageName = app.packageName
|
||||||
|
override suspend fun reportSize(size: Long) {
|
||||||
|
require(size > 0) { "Size must be greater than zero" }
|
||||||
|
require(
|
||||||
|
downloadSize.compareAndSet(
|
||||||
|
0,
|
||||||
|
size
|
||||||
|
)
|
||||||
|
) { "Download size has already been set" }
|
||||||
|
send(downloadedBytes.get() to size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun emitProgress(bytes: Long) {
|
||||||
|
val newValue = downloadedBytes.addAndGet(bytes)
|
||||||
|
val totalSize = downloadSize.get()
|
||||||
|
if (totalSize < 1) return
|
||||||
|
trySend(newValue to totalSize).getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFile.outputStream(StandardOpenOption.CREATE_NEW).buffered().use {
|
||||||
|
val stream = object : FilterOutputStream(it) {
|
||||||
|
override fun write(b: Int) = out.write(b).also { emitProgress(1) }
|
||||||
|
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) =
|
||||||
|
out.write(b, off, len).also {
|
||||||
|
emitProgress(
|
||||||
|
(len - off).toLong()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.download(scope, data, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.conflate()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { (downloaded, size) -> onDownload(downloaded to size) }
|
||||||
|
|
||||||
|
if (downloadedBytes.get() < 1) error("Downloader did not download anything.")
|
||||||
|
val pkgInfo =
|
||||||
|
pm.getPackageInfo(targetFile.toFile()) ?: error("Downloaded APK file is invalid")
|
||||||
|
if (pkgInfo.packageName != expectedPackageName) error("Downloaded APK has the wrong package name. Expected: $expectedPackageName, Actual: ${pkgInfo.packageName}")
|
||||||
|
if (expectedVersion != null && pkgInfo.versionName != expectedVersion) error("Downloaded APK has the wrong version. Expected: $expectedVersion, Actual: ${pkgInfo.versionName}")
|
||||||
|
|
||||||
|
// Delete the previous copy (if present).
|
||||||
|
dao.get(pkgInfo.packageName, pkgInfo.versionName!!)?.directory?.let {
|
||||||
|
if (!dir.resolve(it).deleteRecursively()) throw Exception("Failed to delete existing directory")
|
||||||
|
}
|
||||||
|
dao.upsert(
|
||||||
|
DownloadedApp(
|
||||||
|
packageName = pkgInfo.packageName,
|
||||||
|
version = pkgInfo.versionName!!,
|
||||||
|
directory = relativePath,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
saveDir.deleteRecursively()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the Apk file.
|
||||||
|
return getApkFileForDir(saveDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(packageName: String, version: String, markUsed: Boolean = false) =
|
||||||
|
dao.get(packageName, version)?.also {
|
||||||
|
if (markUsed) dao.markUsed(packageName, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(downloadedApps: Collection<DownloadedApp>) {
|
||||||
|
downloadedApps.forEach {
|
||||||
|
dir.resolve(it.directory).deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
dao.delete(downloadedApps)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.plugins.TrustedDownloaderPlugin
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.network.downloader.DownloaderPluginState
|
||||||
|
import app.revanced.manager.network.downloader.LoadedDownloaderPlugin
|
||||||
|
import app.revanced.manager.network.downloader.ParceledDownloaderData
|
||||||
|
import app.revanced.manager.plugin.downloader.DownloaderBuilder
|
||||||
|
import app.revanced.manager.plugin.downloader.PluginHostApi
|
||||||
|
import app.revanced.manager.plugin.downloader.Scope
|
||||||
|
import app.revanced.manager.util.PM
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
|
||||||
|
@OptIn(PluginHostApi::class)
|
||||||
|
class DownloaderPluginRepository(
|
||||||
|
private val pm: PM,
|
||||||
|
private val prefs: PreferencesManager,
|
||||||
|
private val app: Application,
|
||||||
|
db: AppDatabase
|
||||||
|
) {
|
||||||
|
private val trustDao = db.trustedDownloaderPluginDao()
|
||||||
|
private val _pluginStates = MutableStateFlow(emptyMap<String, DownloaderPluginState>())
|
||||||
|
val pluginStates = _pluginStates.asStateFlow()
|
||||||
|
val loadedPluginsFlow = pluginStates.map { states ->
|
||||||
|
states.values.filterIsInstance<DownloaderPluginState.Loaded>().map { it.plugin }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val acknowledgedDownloaderPlugins = prefs.acknowledgedDownloaderPlugins
|
||||||
|
private val installedPluginPackageNames = MutableStateFlow(emptySet<String>())
|
||||||
|
val newPluginPackageNames = combine(
|
||||||
|
installedPluginPackageNames,
|
||||||
|
acknowledgedDownloaderPlugins.flow
|
||||||
|
) { installed, acknowledged ->
|
||||||
|
installed subtract acknowledged
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() {
|
||||||
|
val plugins =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
pm.getPackagesWithFeature(PLUGIN_FEATURE)
|
||||||
|
.associate { it.packageName to loadPlugin(it.packageName) }
|
||||||
|
}
|
||||||
|
|
||||||
|
_pluginStates.value = plugins
|
||||||
|
installedPluginPackageNames.value = plugins.keys
|
||||||
|
|
||||||
|
val acknowledgedPlugins = acknowledgedDownloaderPlugins.get()
|
||||||
|
val uninstalledPlugins = acknowledgedPlugins subtract installedPluginPackageNames.value
|
||||||
|
if (uninstalledPlugins.isNotEmpty()) {
|
||||||
|
Log.d(tag, "Uninstalled plugins: ${uninstalledPlugins.joinToString(", ")}")
|
||||||
|
acknowledgedDownloaderPlugins.update(acknowledgedPlugins subtract uninstalledPlugins)
|
||||||
|
trustDao.removeAll(uninstalledPlugins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unwrapParceledData(data: ParceledDownloaderData): Pair<LoadedDownloaderPlugin, Parcelable> {
|
||||||
|
val plugin =
|
||||||
|
(_pluginStates.value[data.pluginPackageName] as? DownloaderPluginState.Loaded)?.plugin
|
||||||
|
?: throw Exception("Downloader plugin with name ${data.pluginPackageName} is not available")
|
||||||
|
|
||||||
|
return plugin to data.unwrapWith(plugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPlugin(packageName: String): DownloaderPluginState {
|
||||||
|
try {
|
||||||
|
if (!verify(packageName)) return DownloaderPluginState.Untrusted
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(tag, "Got exception while verifying plugin $packageName", e)
|
||||||
|
return DownloaderPluginState.Failed(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val packageInfo = pm.getPackageInfo(packageName, flags = PackageManager.GET_META_DATA)!!
|
||||||
|
val className = packageInfo.applicationInfo!!.metaData.getString(METADATA_PLUGIN_CLASS)
|
||||||
|
?: throw Exception("Missing metadata attribute $METADATA_PLUGIN_CLASS")
|
||||||
|
|
||||||
|
val classLoader =
|
||||||
|
PathClassLoader(packageInfo.applicationInfo!!.sourceDir, app.classLoader)
|
||||||
|
val pluginContext = app.createPackageContext(packageName, 0)
|
||||||
|
|
||||||
|
val downloader = classLoader
|
||||||
|
.loadClass(className)
|
||||||
|
.getDownloaderBuilder()
|
||||||
|
.build(
|
||||||
|
scopeImpl = object : Scope {
|
||||||
|
override val hostPackageName = app.packageName
|
||||||
|
override val pluginPackageName = pluginContext.packageName
|
||||||
|
},
|
||||||
|
context = pluginContext
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloaderPluginState.Loaded(
|
||||||
|
LoadedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
with(pm) { packageInfo.label() },
|
||||||
|
packageInfo.versionName!!,
|
||||||
|
downloader.get,
|
||||||
|
downloader.download,
|
||||||
|
classLoader
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(tag, "Failed to load plugin $packageName", t)
|
||||||
|
DownloaderPluginState.Failed(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun trustPackage(packageName: String) {
|
||||||
|
trustDao.upsertTrust(
|
||||||
|
TrustedDownloaderPlugin(
|
||||||
|
packageName,
|
||||||
|
pm.getSignature(packageName).toByteArray()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
reload()
|
||||||
|
prefs.edit {
|
||||||
|
acknowledgedDownloaderPlugins += packageName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun revokeTrustForPackage(packageName: String) =
|
||||||
|
trustDao.remove(packageName).also { reload() }
|
||||||
|
|
||||||
|
suspend fun acknowledgeAllNewPlugins() =
|
||||||
|
acknowledgedDownloaderPlugins.update(installedPluginPackageNames.value)
|
||||||
|
|
||||||
|
private suspend fun verify(packageName: String): Boolean {
|
||||||
|
val expectedSignature =
|
||||||
|
trustDao.getTrustedSignature(packageName) ?: return false
|
||||||
|
|
||||||
|
return pm.hasSignature(packageName, expectedSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val PLUGIN_FEATURE = "app.revanced.manager.plugin.downloader"
|
||||||
|
const val METADATA_PLUGIN_CLASS = "app.revanced.manager.plugin.downloader.class"
|
||||||
|
|
||||||
|
const val PUBLIC_STATIC = Modifier.PUBLIC or Modifier.STATIC
|
||||||
|
val Int.isPublicStatic get() = (this and PUBLIC_STATIC) == PUBLIC_STATIC
|
||||||
|
val Class<*>.isDownloaderBuilder get() = DownloaderBuilder::class.java.isAssignableFrom(this)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun Class<*>.getDownloaderBuilder() =
|
||||||
|
declaredMethods
|
||||||
|
.firstOrNull { it.modifiers.isPublicStatic && it.returnType.isDownloaderBuilder && it.parameterTypes.isEmpty() }
|
||||||
|
?.let { it(null) as DownloaderBuilder<Parcelable> }
|
||||||
|
?: throw Exception("Could not find a valid downloader implementation in class $canonicalName")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.apps.installed.AppliedPatch
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstallType
|
||||||
|
import app.revanced.manager.data.room.apps.installed.InstalledApp
|
||||||
|
import app.revanced.manager.util.PatchSelection
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
class InstalledAppRepository(
|
||||||
|
db: AppDatabase
|
||||||
|
) {
|
||||||
|
private val dao = db.installedAppDao()
|
||||||
|
|
||||||
|
fun getAll() = dao.getAll().distinctUntilChanged()
|
||||||
|
|
||||||
|
suspend fun get(packageName: String) = dao.get(packageName)
|
||||||
|
|
||||||
|
suspend fun getAppliedPatches(packageName: String): PatchSelection =
|
||||||
|
dao.getPatchesSelection(packageName).mapValues { (_, patches) -> patches.toSet() }
|
||||||
|
|
||||||
|
suspend fun addOrUpdate(
|
||||||
|
currentPackageName: String,
|
||||||
|
originalPackageName: String,
|
||||||
|
version: String,
|
||||||
|
installType: InstallType,
|
||||||
|
patchSelection: PatchSelection
|
||||||
|
) {
|
||||||
|
dao.upsertApp(
|
||||||
|
InstalledApp(
|
||||||
|
currentPackageName = currentPackageName,
|
||||||
|
originalPackageName = originalPackageName,
|
||||||
|
version = version,
|
||||||
|
installType = installType
|
||||||
|
),
|
||||||
|
patchSelection.flatMap { (uid, patches) ->
|
||||||
|
patches.map { patch ->
|
||||||
|
AppliedPatch(
|
||||||
|
packageName = currentPackageName,
|
||||||
|
bundle = uid,
|
||||||
|
patchName = patch
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(installedApp: InstalledApp) {
|
||||||
|
dao.delete(installedApp)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.data.room.bundles.Source
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
|
||||||
|
class PatchBundlePersistenceRepository(db: AppDatabase) {
|
||||||
|
private val dao = db.patchBundleDao()
|
||||||
|
|
||||||
|
suspend fun loadConfiguration(): List<PatchBundleEntity> {
|
||||||
|
val all = dao.all()
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
dao.add(defaultSource)
|
||||||
|
return listOf(defaultSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reset() = dao.reset()
|
||||||
|
|
||||||
|
suspend fun create(name: String, source: Source, autoUpdate: Boolean = false) =
|
||||||
|
PatchBundleEntity(
|
||||||
|
uid = generateUid(),
|
||||||
|
name = name,
|
||||||
|
version = null,
|
||||||
|
source = source,
|
||||||
|
autoUpdate = autoUpdate
|
||||||
|
).also {
|
||||||
|
dao.add(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(uid: Int) = dao.remove(uid)
|
||||||
|
|
||||||
|
suspend fun updateVersion(uid: Int, version: String?) =
|
||||||
|
dao.updateVersion(uid, version)
|
||||||
|
|
||||||
|
suspend fun setAutoUpdate(uid: Int, value: Boolean) = dao.setAutoUpdate(uid, value)
|
||||||
|
|
||||||
|
suspend fun setName(uid: Int, name: String) = dao.setName(uid, name)
|
||||||
|
|
||||||
|
fun getProps(id: Int) = dao.getPropsById(id).distinctUntilChanged()
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val defaultSource = PatchBundleEntity(
|
||||||
|
uid = 0,
|
||||||
|
name = "",
|
||||||
|
version = null,
|
||||||
|
source = Source.API,
|
||||||
|
autoUpdate = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,184 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.library.mostCommonCompatibleVersions
|
||||||
|
import app.revanced.manager.R
|
||||||
|
import app.revanced.manager.data.platform.NetworkInfo
|
||||||
|
import app.revanced.manager.data.room.bundles.PatchBundleEntity
|
||||||
|
import app.revanced.manager.domain.bundles.APIPatchBundle
|
||||||
|
import app.revanced.manager.domain.bundles.JsonPatchBundle
|
||||||
|
import app.revanced.manager.data.room.bundles.Source as SourceInfo
|
||||||
|
import app.revanced.manager.domain.bundles.LocalPatchBundle
|
||||||
|
import app.revanced.manager.domain.bundles.RemotePatchBundle
|
||||||
|
import app.revanced.manager.domain.bundles.PatchBundleSource
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
|
import app.revanced.manager.util.flatMapLatestAndCombine
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import app.revanced.manager.util.uiSafe
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class PatchBundleRepository(
|
||||||
|
private val app: Application,
|
||||||
|
private val persistenceRepo: PatchBundlePersistenceRepository,
|
||||||
|
private val networkInfo: NetworkInfo,
|
||||||
|
private val prefs: PreferencesManager,
|
||||||
|
) {
|
||||||
|
private val bundlesDir = app.getDir("patch_bundles", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
private val _sources: MutableStateFlow<Map<Int, PatchBundleSource>> =
|
||||||
|
MutableStateFlow(emptyMap())
|
||||||
|
val sources = _sources.map { it.values.toList() }
|
||||||
|
|
||||||
|
val bundles = sources.flatMapLatestAndCombine(
|
||||||
|
combiner = {
|
||||||
|
it.mapNotNull { (uid, state) ->
|
||||||
|
val bundle = state.patchBundleOrNull() ?: return@mapNotNull null
|
||||||
|
uid to bundle
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
it.state.map { state -> it.uid to state }
|
||||||
|
}
|
||||||
|
|
||||||
|
val suggestedVersions = bundles.map {
|
||||||
|
val allPatches =
|
||||||
|
it.values.flatMap { bundle -> bundle.patches.map(PatchInfo::toPatcherPatch) }.toSet()
|
||||||
|
|
||||||
|
allPatches.mostCommonCompatibleVersions(countUnusedPatches = true)
|
||||||
|
.mapValues { (_, versions) ->
|
||||||
|
if (versions.keys.size < 2)
|
||||||
|
return@mapValues versions.keys.firstOrNull()
|
||||||
|
|
||||||
|
// The entries are ordered from most compatible to least compatible.
|
||||||
|
// If there are entries with the same number of compatible patches, older versions will be first, which is undesirable.
|
||||||
|
// This means we have to pick the last entry we find that has the highest patch count.
|
||||||
|
// The order may change in future versions of ReVanced Library.
|
||||||
|
var currentHighestPatchCount = -1
|
||||||
|
versions.entries.last { (_, patchCount) ->
|
||||||
|
if (patchCount >= currentHighestPatchCount) {
|
||||||
|
currentHighestPatchCount = patchCount
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun isVersionAllowed(packageName: String, version: String) =
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
if (!prefs.suggestedVersionSafeguard.get()) return@withContext true
|
||||||
|
|
||||||
|
val suggestedVersion = suggestedVersions.first()[packageName] ?: return@withContext true
|
||||||
|
suggestedVersion == version
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the directory of the [PatchBundleSource] with the specified [uid], creating it if needed.
|
||||||
|
*/
|
||||||
|
private fun directoryOf(uid: Int) = bundlesDir.resolve(uid.toString()).also { it.mkdirs() }
|
||||||
|
|
||||||
|
private fun PatchBundleEntity.load(): PatchBundleSource {
|
||||||
|
val dir = directoryOf(uid)
|
||||||
|
|
||||||
|
return when (source) {
|
||||||
|
is SourceInfo.Local -> LocalPatchBundle(name, uid, dir)
|
||||||
|
is SourceInfo.API -> APIPatchBundle(name, uid, dir, SourceInfo.API.SENTINEL)
|
||||||
|
is SourceInfo.Remote -> JsonPatchBundle(
|
||||||
|
name,
|
||||||
|
uid,
|
||||||
|
dir,
|
||||||
|
source.url.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reload() = withContext(Dispatchers.Default) {
|
||||||
|
val entities = persistenceRepo.loadConfiguration().onEach {
|
||||||
|
Log.d(tag, "Bundle: $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
_sources.value = entities.associate {
|
||||||
|
it.uid to it.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reset() = withContext(Dispatchers.Default) {
|
||||||
|
persistenceRepo.reset()
|
||||||
|
_sources.value = emptyMap()
|
||||||
|
bundlesDir.apply {
|
||||||
|
deleteRecursively()
|
||||||
|
mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun remove(bundle: PatchBundleSource) = withContext(Dispatchers.Default) {
|
||||||
|
persistenceRepo.delete(bundle.uid)
|
||||||
|
directoryOf(bundle.uid).deleteRecursively()
|
||||||
|
|
||||||
|
_sources.update {
|
||||||
|
it.filterKeys { key ->
|
||||||
|
key != bundle.uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addBundle(patchBundle: PatchBundleSource) =
|
||||||
|
_sources.update { it.toMutableMap().apply { put(patchBundle.uid, patchBundle) } }
|
||||||
|
|
||||||
|
suspend fun createLocal(patches: InputStream) = withContext(Dispatchers.Default) {
|
||||||
|
val uid = persistenceRepo.create("", SourceInfo.Local).uid
|
||||||
|
val bundle = LocalPatchBundle("", uid, directoryOf(uid))
|
||||||
|
|
||||||
|
bundle.replace(patches)
|
||||||
|
addBundle(bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createRemote(url: String, autoUpdate: Boolean) = withContext(Dispatchers.Default) {
|
||||||
|
val entity = persistenceRepo.create("", SourceInfo.from(url), autoUpdate)
|
||||||
|
addBundle(entity.load())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <reified T> getBundlesByType() =
|
||||||
|
sources.first().filterIsInstance<T>()
|
||||||
|
|
||||||
|
suspend fun reloadApiBundles() {
|
||||||
|
getBundlesByType<APIPatchBundle>().forEach {
|
||||||
|
it.deleteLocalFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun redownloadRemoteBundles() =
|
||||||
|
getBundlesByType<RemotePatchBundle>().forEach { it.downloadLatest() }
|
||||||
|
|
||||||
|
suspend fun updateCheck() =
|
||||||
|
uiSafe(app, R.string.source_download_fail, "Failed to update bundles") {
|
||||||
|
coroutineScope {
|
||||||
|
if (!networkInfo.isSafe()) {
|
||||||
|
Log.d(tag, "Skipping update check because the network is down or metered.")
|
||||||
|
return@coroutineScope
|
||||||
|
}
|
||||||
|
|
||||||
|
getBundlesByType<RemotePatchBundle>().forEach {
|
||||||
|
launch {
|
||||||
|
if (!it.getProps().autoUpdate) return@launch
|
||||||
|
Log.d(tag, "Updating patch bundle: ${it.getName()}")
|
||||||
|
it.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.options.Option
|
||||||
|
import app.revanced.manager.data.room.options.OptionGroup
|
||||||
|
import app.revanced.manager.patcher.patch.PatchInfo
|
||||||
|
import app.revanced.manager.util.Options
|
||||||
|
import app.revanced.manager.util.tag
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
class PatchOptionsRepository(db: AppDatabase) {
|
||||||
|
private val dao = db.optionDao()
|
||||||
|
|
||||||
|
private suspend fun getOrCreateGroup(bundleUid: Int, packageName: String) =
|
||||||
|
dao.getGroupId(bundleUid, packageName) ?: OptionGroup(
|
||||||
|
uid = AppDatabase.generateUid(),
|
||||||
|
patchBundle = bundleUid,
|
||||||
|
packageName = packageName
|
||||||
|
).also { dao.createOptionGroup(it) }.uid
|
||||||
|
|
||||||
|
suspend fun getOptions(
|
||||||
|
packageName: String,
|
||||||
|
bundlePatches: Map<Int, Map<String, PatchInfo>>
|
||||||
|
): Options {
|
||||||
|
val options = dao.getOptions(packageName)
|
||||||
|
// Bundle -> Patches
|
||||||
|
return buildMap<Int, MutableMap<String, MutableMap<String, Any?>>>(options.size) {
|
||||||
|
options.forEach { (sourceUid, bundlePatchOptionsList) ->
|
||||||
|
// Patches -> Patch options
|
||||||
|
this[sourceUid] =
|
||||||
|
bundlePatchOptionsList.fold(mutableMapOf()) { bundlePatchOptions, dbOption ->
|
||||||
|
val deserializedPatchOptions =
|
||||||
|
bundlePatchOptions.getOrPut(dbOption.patchName, ::mutableMapOf)
|
||||||
|
|
||||||
|
val option =
|
||||||
|
bundlePatches[sourceUid]?.get(dbOption.patchName)?.options?.find { it.key == dbOption.key }
|
||||||
|
if (option != null) {
|
||||||
|
try {
|
||||||
|
deserializedPatchOptions[option.key] =
|
||||||
|
dbOption.value.deserializeFor(option)
|
||||||
|
} catch (e: Option.SerializationException) {
|
||||||
|
Log.w(
|
||||||
|
tag,
|
||||||
|
"Option ${dbOption.patchName}:${option.key} could not be deserialized",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bundlePatchOptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveOptions(packageName: String, options: Options) =
|
||||||
|
dao.updateOptions(options.entries.associate { (sourceUid, bundlePatchOptions) ->
|
||||||
|
val groupId = getOrCreateGroup(sourceUid, packageName)
|
||||||
|
|
||||||
|
groupId to bundlePatchOptions.flatMap { (patchName, patchOptions) ->
|
||||||
|
patchOptions.mapNotNull { (key, value) ->
|
||||||
|
val serialized = try {
|
||||||
|
Option.SerializedValue.fromValue(value)
|
||||||
|
} catch (e: Option.SerializationException) {
|
||||||
|
Log.e(tag, "Option $patchName:$key could not be serialized", e)
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
|
||||||
|
Option(groupId, patchName, key, serialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fun getPackagesWithSavedOptions() =
|
||||||
|
dao.getPackagesWithOptions().map(Iterable<String>::toSet).distinctUntilChanged()
|
||||||
|
|
||||||
|
suspend fun clearOptionsForPackage(packageName: String) = dao.clearForPackage(packageName)
|
||||||
|
suspend fun clearOptionsForPatchBundle(uid: Int) = dao.clearForPatchBundle(uid)
|
||||||
|
suspend fun reset() = dao.reset()
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package app.revanced.manager.domain.repository
|
||||||
|
|
||||||
|
import app.revanced.manager.data.room.AppDatabase
|
||||||
|
import app.revanced.manager.data.room.AppDatabase.Companion.generateUid
|
||||||
|
import app.revanced.manager.data.room.selection.PatchSelection
|
||||||
|
|
||||||
|
class PatchSelectionRepository(db: AppDatabase) {
|
||||||
|
private val dao = db.selectionDao()
|
||||||
|
|
||||||
|
private suspend fun getOrCreateSelection(bundleUid: Int, packageName: String) =
|
||||||
|
dao.getSelectionId(bundleUid, packageName) ?: PatchSelection(
|
||||||
|
uid = generateUid(),
|
||||||
|
patchBundle = bundleUid,
|
||||||
|
packageName = packageName
|
||||||
|
).also { dao.createSelection(it) }.uid
|
||||||
|
|
||||||
|
suspend fun getSelection(packageName: String): Map<Int, Set<String>> =
|
||||||
|
dao.getSelectedPatches(packageName).mapValues { it.value.toSet() }
|
||||||
|
|
||||||
|
suspend fun updateSelection(packageName: String, selection: Map<Int, Set<String>>) =
|
||||||
|
dao.updateSelections(selection.mapKeys { (sourceUid, _) ->
|
||||||
|
getOrCreateSelection(
|
||||||
|
sourceUid,
|
||||||
|
packageName
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
suspend fun clearSelection(packageName: String) {
|
||||||
|
dao.clearForPackage(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun reset() = dao.reset()
|
||||||
|
|
||||||
|
suspend fun export(bundleUid: Int): SerializedSelection = dao.exportSelection(bundleUid)
|
||||||
|
|
||||||
|
suspend fun import(bundleUid: Int, selection: SerializedSelection) {
|
||||||
|
dao.clearForPatchBundle(bundleUid)
|
||||||
|
dao.updateSelections(selection.entries.associate { (packageName, patches) ->
|
||||||
|
getOrCreateSelection(bundleUid, packageName) to patches.toSet()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Map] of package name -> selected patches.
|
||||||
|
*/
|
||||||
|
typealias SerializedSelection = Map<String, List<String>>
|
@ -0,0 +1,7 @@
|
|||||||
|
package app.revanced.manager.domain.worker
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
|
||||||
|
abstract class Worker<ARGS>(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters)
|
@ -0,0 +1,36 @@
|
|||||||
|
package app.revanced.manager.domain.worker
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.OutOfQuotaPolicy
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class WorkerRepository(app: Application) {
|
||||||
|
val workManager = WorkManager.getInstance(app)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The standard WorkManager communication APIs use [androidx.work.Data], which has too many limitations.
|
||||||
|
* We can get around those limits by passing inputs using global variables instead.
|
||||||
|
*/
|
||||||
|
val workerInputs = mutableMapOf<UUID, Any>()
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun <A : Any, W : Worker<A>> claimInput(worker: W): A {
|
||||||
|
val data = workerInputs[worker.id] ?: throw IllegalStateException("Worker was not launched via WorkerRepository")
|
||||||
|
workerInputs.remove(worker.id)
|
||||||
|
|
||||||
|
return data as A
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified W : Worker<A>, A : Any> launchExpedited(name: String, input: A): UUID {
|
||||||
|
val request =
|
||||||
|
OneTimeWorkRequest.Builder(W::class.java) // create Worker
|
||||||
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
|
.build()
|
||||||
|
workerInputs[request.id] = input
|
||||||
|
workManager.enqueueUniqueWork(name, ExistingWorkPolicy.REPLACE, request)
|
||||||
|
return request.id
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package app.revanced.manager.network.api
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import app.revanced.manager.domain.manager.PreferencesManager
|
||||||
|
import app.revanced.manager.network.dto.ReVancedAsset
|
||||||
|
import app.revanced.manager.network.dto.ReVancedGitRepository
|
||||||
|
import app.revanced.manager.network.dto.ReVancedInfo
|
||||||
|
import app.revanced.manager.network.service.HttpService
|
||||||
|
import app.revanced.manager.network.utils.APIResponse
|
||||||
|
import app.revanced.manager.network.utils.getOrThrow
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import io.ktor.client.request.url
|
||||||
|
|
||||||
|
class ReVancedAPI(
|
||||||
|
private val client: HttpService,
|
||||||
|
private val prefs: PreferencesManager
|
||||||
|
) {
|
||||||
|
private suspend fun apiUrl() = prefs.api.get()
|
||||||
|
|
||||||
|
private suspend inline fun <reified T> request(api: String, route: String): APIResponse<T> =
|
||||||
|
withContext(
|
||||||
|
Dispatchers.IO
|
||||||
|
) {
|
||||||
|
client.request {
|
||||||
|
url("$api/v4/$route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend inline fun <reified T> request(route: String) = request<T>(apiUrl(), route)
|
||||||
|
|
||||||
|
suspend fun getAppUpdate() =
|
||||||
|
getLatestAppInfo().getOrThrow().takeIf { it.version != Build.VERSION.RELEASE }
|
||||||
|
|
||||||
|
suspend fun getLatestAppInfo() = request<ReVancedAsset>("manager")
|
||||||
|
|
||||||
|
suspend fun getPatchesUpdate() = request<ReVancedAsset>("patches")
|
||||||
|
|
||||||
|
suspend fun getContributors() = request<List<ReVancedGitRepository>>("contributors")
|
||||||
|
|
||||||
|
suspend fun getInfo(api: String? = null) = request<ReVancedInfo>(api ?: apiUrl(), "about")
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user