You've already forked Magisk
mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-09-06 06:36:58 +00:00
Compare commits
357 Commits
manager-v8
...
v22.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0f2164bc5 | ||
|
|
f6e4a27fdd | ||
|
|
257ceb99f7 | ||
|
|
706d53065b | ||
|
|
0f95a7babe | ||
|
|
7cb2806878 | ||
|
|
9c0e18975c | ||
|
|
3da318b48e | ||
|
|
dfe1f2c108 | ||
|
|
f42a87b51a | ||
|
|
ab25857176 | ||
|
|
7da36079c1 | ||
|
|
2bef967af1 | ||
|
|
7e4194418a | ||
|
|
aa02057895 | ||
|
|
fb8dc07599 | ||
|
|
66e30a7723 | ||
|
|
0298ab99c4 | ||
|
|
d11358671e | ||
|
|
8b5cb4c7b0 | ||
|
|
aad52ae743 | ||
|
|
8ddab84745 | ||
|
|
6865652125 | ||
|
|
ed4d0867e8 | ||
|
|
1c71e02454 | ||
|
|
f332e87cab | ||
|
|
023dbc6cb5 | ||
|
|
4dd3f55407 | ||
|
|
7b9a71c9af | ||
|
|
901d22cdfa | ||
|
|
93e1266ee7 | ||
|
|
0a4e7eea41 | ||
|
|
e3801d6965 | ||
|
|
336f1687c1 | ||
|
|
d4e2f2df6e | ||
|
|
f152b4c26e | ||
|
|
bd935b0553 | ||
|
|
a9b3b7a359 | ||
|
|
7a007b342a | ||
|
|
0783f3d5b6 | ||
|
|
afe3c2bc1b | ||
|
|
82f8948fd4 | ||
|
|
b9cdc755d1 | ||
|
|
a6f81c66e5 | ||
|
|
1ff45ac5f5 | ||
|
|
48bde7375f | ||
|
|
0601fa3b3d | ||
|
|
d0d3c8dbfd | ||
|
|
8057de1973 | ||
|
|
43c1105d62 | ||
|
|
6adf516b30 | ||
|
|
bf80b08b5f | ||
|
|
3e0b1df46d | ||
|
|
84811c80b6 | ||
|
|
45e0df9c57 | ||
|
|
bc51ce7c7b | ||
|
|
b693d13b93 | ||
|
|
39982d57ef | ||
|
|
15e27e54fb | ||
|
|
851404205b | ||
|
|
117ae71025 | ||
|
|
027ec70262 | ||
|
|
55fdee4d65 | ||
|
|
0d42f937dd | ||
|
|
ac8372dd26 | ||
|
|
5e56a6bbee | ||
|
|
3c6c409df0 | ||
|
|
d05408c89f | ||
|
|
ee0ec3fbfa | ||
|
|
122a73e086 | ||
|
|
29a9b18c4c | ||
|
|
1561272109 | ||
|
|
3e61ab0d25 | ||
|
|
a49dc6ccb7 | ||
|
|
60f3d62f00 | ||
|
|
e613855a4f | ||
|
|
22662d7e03 | ||
|
|
6e7e5be1a2 | ||
|
|
8b2ab778c9 | ||
|
|
35f3766ecf | ||
|
|
995304dabb | ||
|
|
803982a271 | ||
|
|
9164bf22c2 | ||
|
|
911a576893 | ||
|
|
79ee85c0f9 | ||
|
|
483dbcdc40 | ||
|
|
a1096b5bf0 | ||
|
|
5ac0e64edb | ||
|
|
60b2624607 | ||
|
|
d2e2847b03 | ||
|
|
b9669f54f7 | ||
|
|
8c7bd77d33 | ||
|
|
ba1ce16b8b | ||
|
|
68090943f4 | ||
|
|
a4fb1297b0 | ||
|
|
860a05abf2 | ||
|
|
8bb2f356c0 | ||
|
|
4950020635 | ||
|
|
0a6140c6eb | ||
|
|
bba2ac8817 | ||
|
|
331b1f542f | ||
|
|
ccb55205e6 | ||
|
|
9cc91b30b3 | ||
|
|
e836caf31e | ||
|
|
beaa1e5be2 | ||
|
|
ea545bae26 | ||
|
|
1c9ec2df45 | ||
|
|
b76c80e2ce | ||
|
|
236990f4a3 | ||
|
|
1ed32df20d | ||
|
|
8476eb9f4b | ||
|
|
735af7843b | ||
|
|
ded73e958b | ||
|
|
6dcb84d4f4 | ||
|
|
501bc9f438 | ||
|
|
f88e812b63 | ||
|
|
be6386c410 | ||
|
|
2af4fd17c4 | ||
|
|
f870418bd0 | ||
|
|
00659e4795 | ||
|
|
cdda10207e | ||
|
|
701700279f | ||
|
|
a9d804724a | ||
|
|
e033a9ab47 | ||
|
|
059e5fb8aa | ||
|
|
a78f255928 | ||
|
|
1d10e69288 | ||
|
|
63590d379c | ||
|
|
5f63e88984 | ||
|
|
75584e2b19 | ||
|
|
1426ee2ebd | ||
|
|
b6643b7bfc | ||
|
|
721dfdf553 | ||
|
|
2963747d14 | ||
|
|
e7350d5041 | ||
|
|
f37e8f4ca8 | ||
|
|
594c2accc0 | ||
|
|
7acfac6a91 | ||
|
|
0646f48e14 | ||
|
|
37565fd067 | ||
|
|
26b2e7dc5d | ||
|
|
c3313623e4 | ||
|
|
2089223690 | ||
|
|
52e1b84d41 | ||
|
|
8794141b7f | ||
|
|
f6126dd20e | ||
|
|
18acfda99b | ||
|
|
bec5edca84 | ||
|
|
6fb20b3ee5 | ||
|
|
eaf4d8064b | ||
|
|
2a5f5b1bba | ||
|
|
c538a77937 | ||
|
|
aa9e7b1ed1 | ||
|
|
a3066eddab | ||
|
|
d1729fa787 | ||
|
|
93961dde2c | ||
|
|
1024e68eb6 | ||
|
|
6ae2c9387d | ||
|
|
fba83e2330 | ||
|
|
f1295cb7d6 | ||
|
|
dc61dfbde6 | ||
|
|
21466426da | ||
|
|
3f0136362b | ||
|
|
e92d77bbec | ||
|
|
07bd36c94b | ||
|
|
b1dbbdef12 | ||
|
|
3e479726ec | ||
|
|
4cc41eccb3 | ||
|
|
8f08ae59ac | ||
|
|
e8d4e492d6 | ||
|
|
b8090a8e18 | ||
|
|
c609a01e55 | ||
|
|
c97fb385cd | ||
|
|
da6c57750e | ||
|
|
6951d926f7 | ||
|
|
6623195bd5 | ||
|
|
ec31bb9a82 | ||
|
|
8618cc383a | ||
|
|
4b01e3a3c7 | ||
|
|
7f748c23c1 | ||
|
|
963d248cc7 | ||
|
|
657056e636 | ||
|
|
9d5efea66e | ||
|
|
658d74e026 | ||
|
|
5113f6d375 | ||
|
|
96405c26d0 | ||
|
|
4ea5f34bf3 | ||
|
|
dbd13a2019 | ||
|
|
06773235da | ||
|
|
e57556a8af | ||
|
|
b54b78c29d | ||
|
|
317336f771 | ||
|
|
b4e52f6135 | ||
|
|
f2ca042915 | ||
|
|
1060dd2906 | ||
|
|
2e0f7a82fa | ||
|
|
5798536559 | ||
|
|
ab9a83c82f | ||
|
|
c87fdbea0f | ||
|
|
ec8fffe61c | ||
|
|
61d52991f1 | ||
|
|
9100186dce | ||
|
|
d2bc2cfcf8 | ||
|
|
5a71998b4e | ||
|
|
42278f12ff | ||
|
|
f5593e051c | ||
|
|
a27e30cf54 | ||
|
|
79140c7636 | ||
|
|
1f4c595cd3 | ||
|
|
b5b62e03af | ||
|
|
67e2a4720e | ||
|
|
f5c2d72429 | ||
|
|
2f5331ab48 | ||
|
|
7f8257152f | ||
|
|
0cd80f2556 | ||
|
|
1717387876 | ||
|
|
109363ebf6 | ||
|
|
716c4fa386 | ||
|
|
9a09b4eb20 | ||
|
|
95a5b57265 | ||
|
|
13fbf397d1 | ||
|
|
20be99ec8a | ||
|
|
04c53c3578 | ||
|
|
51bc27a869 | ||
|
|
71b083794c | ||
|
|
b100d0c503 | ||
|
|
76061296c9 | ||
|
|
bb303d2da1 | ||
|
|
c91c070343 | ||
|
|
aec06a6f61 | ||
|
|
e8ba671fc2 | ||
|
|
1860e5d133 | ||
|
|
f2cb3c38fe | ||
|
|
9a28dd4f6e | ||
|
|
d2acd59ea8 | ||
|
|
79dfdb29e7 | ||
|
|
eb21c8b42e | ||
|
|
541bb53553 | ||
|
|
fe8997efae | ||
|
|
23455c722c | ||
|
|
5ce29c30d2 | ||
|
|
70d67728fd | ||
|
|
e546884b08 | ||
|
|
b36e6d987d | ||
|
|
53c3dd5e8b | ||
|
|
da723b207a | ||
|
|
e050f77198 | ||
|
|
540b4b7ea9 | ||
|
|
bbef22daf7 | ||
|
|
9ed110c91b | ||
|
|
a30d510eb1 | ||
|
|
ef98eaed8f | ||
|
|
2a257f327c | ||
|
|
4060c2107c | ||
|
|
cd23d27048 | ||
|
|
18b86e4fd2 | ||
|
|
5f2e22a259 | ||
|
|
4e97b18977 | ||
|
|
f9bde347bc | ||
|
|
947a7d6a2f | ||
|
|
872ab2e99b | ||
|
|
90b8813bb7 | ||
|
|
88d0f63294 | ||
|
|
79fa0d3a90 | ||
|
|
8e61080a4a | ||
|
|
3f9a64417b | ||
|
|
eb959379e8 | ||
|
|
41a644afb9 | ||
|
|
6b42db943d | ||
|
|
1c325459eb | ||
|
|
6d88d8ad95 | ||
|
|
246997f273 | ||
|
|
b6144ae582 | ||
|
|
afe17c73b4 | ||
|
|
b51b884fc7 | ||
|
|
d3e4b29e62 | ||
|
|
24059e7403 | ||
|
|
107a2a6682 | ||
|
|
56b4ab6672 | ||
|
|
4662454938 | ||
|
|
db4f78d463 | ||
|
|
880de21596 | ||
|
|
622dd84c9e | ||
|
|
f983bfc883 | ||
|
|
45cdb3fdb0 | ||
|
|
9a707236b8 | ||
|
|
e9e6ad3bb0 | ||
|
|
ab78a81d15 | ||
|
|
18340099b7 | ||
|
|
a013696a41 | ||
|
|
8a2a6d9232 | ||
|
|
12aa6d86e4 | ||
|
|
7d08969d28 | ||
|
|
dda4aa8488 | ||
|
|
cdaef3d801 | ||
|
|
9159166128 | ||
|
|
dc0882e043 | ||
|
|
c811f015ef | ||
|
|
d8f0b66fe1 | ||
|
|
dc3d57deba | ||
|
|
d089698475 | ||
|
|
8ed2dd6687 | ||
|
|
50305ca1fe | ||
|
|
3e91567636 | ||
|
|
0b4dd63d36 | ||
|
|
38d0f85deb | ||
|
|
c5b452f369 | ||
|
|
6ce9225f52 | ||
|
|
13a8820603 | ||
|
|
503997a09a | ||
|
|
17efdff134 | ||
|
|
984f32f994 | ||
|
|
eee7f097e3 | ||
|
|
086059ec30 | ||
|
|
7ff22c68c7 | ||
|
|
1232113772 | ||
|
|
039d4936cb | ||
|
|
784dd80965 | ||
|
|
1ffe9bd83b | ||
|
|
0c28b23224 | ||
|
|
ec1af9dc1e | ||
|
|
ff4cea229a | ||
|
|
3f81f9371f | ||
|
|
60e89a7d22 | ||
|
|
c50daa5c9e | ||
|
|
58d00ab863 | ||
|
|
ce916459c5 | ||
|
|
4094d560ab | ||
|
|
4dbf7eb04b | ||
|
|
a39577c44d | ||
|
|
125ee46685 | ||
|
|
ce84f1762c | ||
|
|
a687d1347b | ||
|
|
6d9db20614 | ||
|
|
c62dfc1bcc | ||
|
|
aabe2696fe | ||
|
|
ae0d605310 | ||
|
|
2a694596b5 | ||
|
|
ff0a76606e | ||
|
|
dead74801d | ||
|
|
ab207a1bb3 | ||
|
|
f152e8c33d | ||
|
|
797ba4fbf4 | ||
|
|
a848f10bba | ||
|
|
552ec1eb35 | ||
|
|
1385d2a4f4 | ||
|
|
3b5c9abf7a | ||
|
|
e0fa032bd3 | ||
|
|
7b69650fcd | ||
|
|
08a8df489f | ||
|
|
9f35a8a520 | ||
|
|
0df891b336 | ||
|
|
385853a290 | ||
|
|
fa3ef8a1c1 | ||
|
|
c93ada03c7 | ||
|
|
0064b01ae0 | ||
|
|
1469b82aa2 |
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
## READ BEFORE OPENING ISSUES
|
||||||
|
|
||||||
|
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
|
||||||
|
|
||||||
|
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
|
||||||
|
|
||||||
|
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||||
|
|
||||||
|
If you experience a crash of Magisk app, dump the full `logcat` **when the crash happens**.
|
||||||
|
|
||||||
|
If you experience other issues related to Magisk, upload `magisk.log`, and preferably also include a boot `logcat` (start dumping `logcat` when the device boots up)
|
||||||
|
|
||||||
|
**DO NOT** open issues regarding root detection.
|
||||||
|
|
||||||
|
**DO NOT** ask for instructions.
|
||||||
|
|
||||||
|
**DO NOT** report issues if you have any modules installed.
|
||||||
|
|
||||||
|
Without following the rules above, your issue will be closed without explanation.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
Device:
|
||||||
|
Android version:
|
||||||
|
Magisk version name:
|
||||||
|
Magisk version code:
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: XDA Community Support
|
||||||
|
url: https://forum.xda-developers.com/f/magisk.5903/
|
||||||
|
about: Please ask and answer questions here.
|
||||||
|
|
||||||
90
.github/workflows/build.yml
vendored
Normal file
90
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
name: Magisk Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths:
|
||||||
|
- 'app/**'
|
||||||
|
- 'native/**'
|
||||||
|
- 'stub/**'
|
||||||
|
- 'buildSrc/**'
|
||||||
|
- 'build.py'
|
||||||
|
- 'gradle.properties'
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest, windows-latest, macOS-latest ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v1
|
||||||
|
with:
|
||||||
|
java-version: '11'
|
||||||
|
|
||||||
|
- name: Set up Python 3
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Set up GitHub env (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: |
|
||||||
|
$ndk_ver = Select-String -Path "gradle.properties" -Pattern "^magisk.fullNdkVersion=" | % { $_ -replace ".*=" }
|
||||||
|
echo "ANDROID_SDK_ROOT=$env:ANDROID_SDK_ROOT" >> $env:GITHUB_ENV
|
||||||
|
echo "MAGISK_NDK_VERSION=$ndk_ver" >> $env:GITHUB_ENV
|
||||||
|
echo "GRADLE_OPTS=-Dorg.gradle.daemon=false" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set up GitHub env (Unix)
|
||||||
|
if: runner.os != 'Windows'
|
||||||
|
run: |
|
||||||
|
ndk_ver=$(sed -n 's/^magisk.fullNdkVersion=//p' gradle.properties)
|
||||||
|
echo ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT >> $GITHUB_ENV
|
||||||
|
echo MAGISK_NDK_VERSION=$ndk_ver >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||||
|
restore-keys: ${{ runner.os }}-gradle-
|
||||||
|
|
||||||
|
- name: Cache NDK
|
||||||
|
id: ndk-cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/magisk
|
||||||
|
key: ${{ runner.os }}-ndk-${{ env.MAGISK_NDK_VERSION }}
|
||||||
|
|
||||||
|
- name: Set up NDK
|
||||||
|
if: steps.ndk-cache.outputs.cache-hit != 'true'
|
||||||
|
run: python build.py ndk
|
||||||
|
|
||||||
|
- name: Build release
|
||||||
|
run: python build.py -vr all
|
||||||
|
|
||||||
|
- name: Build debug
|
||||||
|
run: python build.py -v all
|
||||||
|
|
||||||
|
# Only upload artifacts built on Linux
|
||||||
|
- name: Upload build artifact
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: ${{ github.sha }}
|
||||||
|
path: out
|
||||||
26
.github/workflows/issues.yml
vendored
Normal file
26
.github/workflows/issues.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: Check Issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Read latest version code
|
||||||
|
run: |
|
||||||
|
ver=$(sed -n 's/^magisk.versionCode=//p' gradle.properties)
|
||||||
|
echo MAGISK_VERSION_CODE=$ver >> $GITHUB_ENV
|
||||||
|
- if: contains(github.event.issue.body, format('Magisk version code{0} ', ':')) != true
|
||||||
|
id: close
|
||||||
|
name: Close Issue(template)
|
||||||
|
uses: peter-evans/close-issue@v1
|
||||||
|
with:
|
||||||
|
comment: This issue is being automatically closed because it does not follow the issue template.
|
||||||
|
- if: steps.close.conclusion == 'skipped' && contains(github.event.issue.body, format('Magisk version code{0} {1}', ':', env.MAGISK_VERSION_CODE)) != true
|
||||||
|
name: Close Issue(latest canary)
|
||||||
|
uses: peter-evans/close-issue@v1
|
||||||
|
with:
|
||||||
|
comment: This issue is being automatically closed because latest canary Magisk version code is ${{ env.MAGISK_VERSION_CODE }}.
|
||||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -25,6 +25,12 @@
|
|||||||
[submodule "pcre"]
|
[submodule "pcre"]
|
||||||
path = native/jni/external/pcre
|
path = native/jni/external/pcre
|
||||||
url = https://android.googlesource.com/platform/external/pcre
|
url = https://android.googlesource.com/platform/external/pcre
|
||||||
|
[submodule "xhook"]
|
||||||
|
path = native/jni/external/xhook
|
||||||
|
url = https://github.com/iqiyi/xHook.git
|
||||||
|
[submodule "libcxx"]
|
||||||
|
path = native/jni/external/libcxx
|
||||||
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
[submodule "termux-elf-cleaner"]
|
[submodule "termux-elf-cleaner"]
|
||||||
path = tools/termux-elf-cleaner
|
path = tools/termux-elf-cleaner
|
||||||
url = https://github.com/termux/termux-elf-cleaner.git
|
url = https://github.com/termux/termux-elf-cleaner.git
|
||||||
|
|||||||
26
README.MD
26
README.MD
@@ -1,7 +1,6 @@
|
|||||||

|

|
||||||
|
|
||||||

|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/count/count.json)
|
||||||

|
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
@@ -15,11 +14,11 @@ Here are some feature highlights:
|
|||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.2/MagiskManager-v8.0.2.apk)
|
Please note that the only source of official Magisk information and downloads is [Github](https://github.com/topjohnwu/Magisk/). Beware of any other websites.
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
|
|
||||||
<br>
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v22.0)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v22.0)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v21.0)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
@@ -34,17 +33,14 @@ Here are some feature highlights:
|
|||||||
- Android 4.4+: All core features available
|
- Android 4.4+: All core features available
|
||||||
- Android 6.0+: Guaranteed MagiskHide support
|
- Android 6.0+: Guaranteed MagiskHide support
|
||||||
- Android 7.0+: Full MagiskHide protection
|
- Android 7.0+: Full MagiskHide protection
|
||||||
- Android 9.0+: Magisk Manager stealth mode
|
|
||||||
|
|
||||||
## Bug Reports
|
## Bug Reports
|
||||||
|
|
||||||
Canary Channels are cutting edge builds for those adventurous. To access canary builds, install the Canary Magisk Manager, switch to the Canary Channel in settings and upgrade.
|
|
||||||
|
|
||||||
**Only bug reports from Canary builds will be accepted.**
|
**Only bug reports from Canary builds will be accepted.**
|
||||||
|
|
||||||
For installation issues, upload both boot image and install logs.<br>
|
For installation issues, upload both boot image and install logs.<br>
|
||||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
For Magisk Manager crashes, record and upload the logcat when the crash occurs.
|
For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||||
|
|
||||||
## Building and Development
|
## Building and Development
|
||||||
|
|
||||||
@@ -58,15 +54,15 @@ For Magisk Manager crashes, record and upload the logcat when the crash occurs.
|
|||||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
||||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
||||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||||
- Set configurations in `config.prop`. A sample `config.prop.sample` is provided.
|
|
||||||
- To start building, run `build.py` to see your options. \
|
- To start building, run `build.py` to see your options. \
|
||||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||||
- To start development, open the project in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building.
|
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources.
|
||||||
- `build.py` builds in debug mode by default. If you want release builds (with `-r, --release`), you need a Java Keystore to sign APKs and zips. For more information, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||||
|
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
||||||
|
|
||||||
## Translation Contributions
|
## Translation Contributions
|
||||||
|
|
||||||
Default string resources for Magisk Manager and its stub APK are located here:
|
Default string resources for the Magisk app and its stub APK are located here:
|
||||||
|
|
||||||
- `app/src/main/res/values/strings.xml`
|
- `app/src/main/res/values/strings.xml`
|
||||||
- `stub/src/main/res/values/strings.xml`
|
- `stub/src/main/res/values/strings.xml`
|
||||||
|
|||||||
5
app/.gitignore
vendored
5
app/.gitignore
vendored
@@ -6,6 +6,7 @@
|
|||||||
app/release
|
app/release
|
||||||
*.hprof
|
*.hprof
|
||||||
.externalNativeBuild/
|
.externalNativeBuild/
|
||||||
public.certificate.x509.pem
|
|
||||||
private.key.pk8
|
|
||||||
*.apk
|
*.apk
|
||||||
|
src/main/assets
|
||||||
|
src/main/jniLibs
|
||||||
|
src/main/resources
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import org.apache.tools.ant.filters.FixCrLfFilter
|
||||||
|
import java.io.PrintStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("android.extensions")
|
kotlin("plugin.parcelize")
|
||||||
kotlin("kapt")
|
kotlin("kapt")
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
}
|
}
|
||||||
@@ -20,9 +23,9 @@ android {
|
|||||||
applicationId = "com.topjohnwu.magisk"
|
applicationId = "com.topjohnwu.magisk"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
versionName = Config["appVersion"]
|
versionName = Config.version
|
||||||
versionCode = Config["appVersionCode"]?.toInt()
|
versionCode = Config.versionCode
|
||||||
buildConfigField("int", "LATEST_MAGISK", Config["versionCode"] ?: "Integer.MAX_VALUE")
|
ndk.abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||||
|
|
||||||
javaCompileOptions.annotationProcessorOptions.arguments(
|
javaCompileOptions.annotationProcessorOptions.arguments(
|
||||||
mapOf("room.incremental" to "true")
|
mapOf("room.incremental" to "true")
|
||||||
@@ -50,13 +53,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
packagingOptions {
|
||||||
exclude("/META-INF/**")
|
exclude("/META-INF/*")
|
||||||
exclude("/org/bouncycastle/**")
|
exclude("/org/bouncycastle/**")
|
||||||
exclude("/kotlin/**")
|
exclude("/kotlin/**")
|
||||||
exclude("/kotlinx/**")
|
exclude("/kotlinx/**")
|
||||||
exclude("/okhttp3/**")
|
exclude("/okhttp3/**")
|
||||||
exclude("/*.txt")
|
exclude("/*.txt")
|
||||||
exclude("/*.bin")
|
exclude("/*.bin")
|
||||||
|
doNotStrip("**/*.so")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
@@ -64,40 +68,146 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidExtensions {
|
val syncLibs by tasks.registering(Sync::class) {
|
||||||
isExperimental = true
|
into("src/main/jniLibs")
|
||||||
|
into("armeabi-v7a") {
|
||||||
|
from(rootProject.file("native/out/armeabi-v7a")) {
|
||||||
|
include("busybox", "magiskboot", "magiskinit", "magisk")
|
||||||
|
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
|
||||||
|
}
|
||||||
|
from(rootProject.file("native/out/arm64-v8a")) {
|
||||||
|
include("magisk")
|
||||||
|
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
into("x86") {
|
||||||
|
from(rootProject.file("native/out/x86")) {
|
||||||
|
include("busybox", "magiskboot", "magiskinit", "magisk")
|
||||||
|
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
|
||||||
|
}
|
||||||
|
from(rootProject.file("native/out/x86_64")) {
|
||||||
|
include("magisk")
|
||||||
|
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onlyIf {
|
||||||
|
if (inputs.sourceFiles.files.size != 10)
|
||||||
|
throw StopExecutionException("Please build binaries first! (./build.py binary)")
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val copyUtils = tasks.register("copyUtils", Copy::class) {
|
val createStubLibs by tasks.registering {
|
||||||
from(rootProject.file("scripts/util_functions.sh"))
|
dependsOn(syncLibs)
|
||||||
into("src/main/res/raw")
|
doLast {
|
||||||
|
val arm64 = project.file("src/main/jniLibs/arm64-v8a/libstub.so")
|
||||||
|
arm64.parentFile.mkdirs()
|
||||||
|
arm64.createNewFile()
|
||||||
|
val x64 = project.file("src/main/jniLibs/x86_64/libstub.so")
|
||||||
|
x64.parentFile.mkdirs()
|
||||||
|
x64.createNewFile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks["preBuild"]?.dependsOn(copyUtils)
|
val syncAssets by tasks.registering(Sync::class) {
|
||||||
|
dependsOn(createStubLibs)
|
||||||
|
inputs.property("version", Config.version)
|
||||||
|
inputs.property("versionCode", Config.versionCode)
|
||||||
|
into("src/main/assets")
|
||||||
|
from(rootProject.file("scripts")) {
|
||||||
|
include("util_functions.sh", "boot_patch.sh", "uninstaller.sh", "addon.d.sh")
|
||||||
|
}
|
||||||
|
into("chromeos") {
|
||||||
|
from(rootProject.file("tools/futility"))
|
||||||
|
from(rootProject.file("tools/keys")) {
|
||||||
|
include("kernel_data_key.vbprivk", "kernel.keyblock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filesMatching("**/util_functions.sh") {
|
||||||
|
filter {
|
||||||
|
it.replace("#MAGISK_VERSION_STUB",
|
||||||
|
"MAGISK_VER='${Config.version}'\n" +
|
||||||
|
"MAGISK_VER_CODE=${Config.versionCode}")
|
||||||
|
}
|
||||||
|
filter<FixCrLfFilter>("eol" to FixCrLfFilter.CrLf.newInstance("lf"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val syncResources by tasks.registering(Sync::class) {
|
||||||
|
dependsOn(syncAssets)
|
||||||
|
into("src/main/resources/META-INF/com/google/android")
|
||||||
|
from(rootProject.file("scripts/update_binary.sh")) {
|
||||||
|
rename { "update-binary" }
|
||||||
|
}
|
||||||
|
from(rootProject.file("scripts/flash_script.sh")) {
|
||||||
|
rename { "updater-script" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks["preBuild"]?.dependsOn(syncResources)
|
||||||
|
|
||||||
|
android.applicationVariants.all {
|
||||||
|
val keysDir = rootProject.file("tools/keys")
|
||||||
|
val outSrcDir = File(buildDir, "generated/source/keydata/$name")
|
||||||
|
val outSrc = File(outSrcDir, "com/topjohnwu/signing/KeyData.java")
|
||||||
|
|
||||||
|
fun PrintStream.newField(name: String, file: File) {
|
||||||
|
println("public static byte[] $name() {")
|
||||||
|
print("byte[] buf = {")
|
||||||
|
val bytes = file.readBytes()
|
||||||
|
print(bytes.joinToString(",") { "(byte)(${it.toInt() and 0xff})" })
|
||||||
|
println("};")
|
||||||
|
println("return buf;")
|
||||||
|
println("}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val genSrcTask = tasks.register("generate${name.capitalize()}KeyData") {
|
||||||
|
inputs.dir(keysDir)
|
||||||
|
outputs.file(outSrc)
|
||||||
|
doLast {
|
||||||
|
outSrc.parentFile.mkdirs()
|
||||||
|
PrintStream(outSrc).use {
|
||||||
|
it.println("package com.topjohnwu.signing;")
|
||||||
|
it.println("public final class KeyData {")
|
||||||
|
|
||||||
|
it.newField("testCert", File(keysDir, "testkey.x509.pem"))
|
||||||
|
it.newField("testKey", File(keysDir, "testkey.pk8"))
|
||||||
|
it.newField("verityCert", File(keysDir, "verity.x509.pem"))
|
||||||
|
it.newField("verityKey", File(keysDir, "verity.pk8"))
|
||||||
|
|
||||||
|
it.println("}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerJavaGeneratingTask(genSrcTask.get(), outSrcDir)
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||||
implementation(kotlin("stdlib"))
|
implementation(kotlin("stdlib"))
|
||||||
implementation(project(":app:shared"))
|
implementation(project(":app:shared"))
|
||||||
implementation(project(":app:signing"))
|
|
||||||
|
|
||||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||||
|
|
||||||
|
val vBC = "1.68"
|
||||||
|
implementation("org.bouncycastle:bcprov-jdk15on:${vBC}")
|
||||||
|
implementation("org.bouncycastle:bcpkix-jdk15on:${vBC}")
|
||||||
|
|
||||||
val vBAdapt = "4.0.0"
|
val vBAdapt = "4.0.0"
|
||||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
||||||
implementation("${bindingAdapter}:${vBAdapt}")
|
implementation("${bindingAdapter}:${vBAdapt}")
|
||||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
||||||
|
|
||||||
val vMarkwon = "4.6.0"
|
val vMarkwon = "4.6.2"
|
||||||
implementation("io.noties.markwon:core:${vMarkwon}")
|
implementation("io.noties.markwon:core:${vMarkwon}")
|
||||||
implementation("io.noties.markwon:html:${vMarkwon}")
|
implementation("io.noties.markwon:html:${vMarkwon}")
|
||||||
implementation("io.noties.markwon:image:${vMarkwon}")
|
implementation("io.noties.markwon:image:${vMarkwon}")
|
||||||
implementation("com.caverock:androidsvg:1.4")
|
implementation("com.caverock:androidsvg:1.4")
|
||||||
|
|
||||||
val vLibsu = "3.0.2"
|
val vLibsu = "3.1.1"
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
||||||
|
|
||||||
@@ -124,7 +234,7 @@ dependencies {
|
|||||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||||
|
|
||||||
val vRoom = "2.3.0-alpha03"
|
val vRoom = "2.3.0-beta03"
|
||||||
implementation("androidx.room:room-runtime:${vRoom}")
|
implementation("androidx.room:room-runtime:${vRoom}")
|
||||||
implementation("androidx.room:room-ktx:${vRoom}")
|
implementation("androidx.room:room-ktx:${vRoom}")
|
||||||
kapt("androidx.room:room-compiler:${vRoom}")
|
kapt("androidx.room:room-compiler:${vRoom}")
|
||||||
@@ -133,17 +243,16 @@ dependencies {
|
|||||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||||
|
|
||||||
implementation("androidx.biometric:biometric:1.0.1")
|
implementation("androidx.biometric:biometric:1.1.0")
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.browser:browser:1.2.0")
|
implementation("androidx.browser:browser:1.3.0")
|
||||||
implementation("androidx.preference:preference:1.1.1")
|
implementation("androidx.preference:preference:1.1.1")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.2.5")
|
implementation("androidx.fragment:fragment-ktx:1.3.1")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.4.0")
|
implementation("androidx.work:work-runtime-ktx:2.5.0")
|
||||||
implementation("androidx.transition:transition:1.3.1")
|
implementation("androidx.transition:transition:1.4.0")
|
||||||
implementation("androidx.multidex:multidex:2.0.1")
|
implementation("androidx.multidex:multidex:2.0.1")
|
||||||
implementation("androidx.core:core-ktx:1.3.2")
|
implementation("androidx.core:core-ktx:1.3.2")
|
||||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
|
implementation("com.google.android.material:material:1.3.0")
|
||||||
implementation("com.google.android.material:material:1.2.1")
|
|
||||||
}
|
}
|
||||||
|
|||||||
17
app/proguard-rules.pro
vendored
17
app/proguard-rules.pro
vendored
@@ -18,16 +18,10 @@
|
|||||||
|
|
||||||
# Kotlin
|
# Kotlin
|
||||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||||
public static void checkExpressionValueIsNotNull(...);
|
public static void check*(...);
|
||||||
public static void checkNotNullExpressionValue(...);
|
public static void throw*(...);
|
||||||
public static void checkReturnedValueIsNotNull(...);
|
|
||||||
public static void checkFieldIsNotNull(...);
|
|
||||||
public static void checkParameterIsNotNull(...);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stubs
|
|
||||||
-keep class a.* { *; }
|
|
||||||
|
|
||||||
# Snet
|
# Snet
|
||||||
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
|
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
|
||||||
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
|
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
|
||||||
@@ -35,14 +29,17 @@
|
|||||||
void onResponse(org.json.JSONObject);
|
void onResponse(org.json.JSONObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Stub
|
||||||
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
|
|
||||||
# Strip Timber verbose and debug logging
|
# Strip Timber verbose and debug logging
|
||||||
-assumenosideeffects class timber.log.Timber.Tree {
|
-assumenosideeffects class timber.log.Timber$Tree {
|
||||||
public void v(**);
|
public void v(**);
|
||||||
public void d(**);
|
public void d(**);
|
||||||
}
|
}
|
||||||
|
|
||||||
# Excessive obfuscation
|
# Excessive obfuscation
|
||||||
-repackageclasses
|
-repackageclasses 'a'
|
||||||
-allowaccessmodification
|
-allowaccessmodification
|
||||||
|
|
||||||
# QOL
|
# QOL
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.shared">
|
package="com.topjohnwu.shared"
|
||||||
|
android:installLocation="internalOnly">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Magisk Manager"
|
android:allowBackup="false"
|
||||||
android:installLocation="internalOnly"
|
android:label="Magisk"
|
||||||
android:usesCleartextTraffic="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
tools:ignore="UnusedAttribute">
|
android:usesCleartextTraffic="true"
|
||||||
</application>
|
tools:ignore="UnusedAttribute" />
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
@@ -10,20 +13,36 @@ import com.topjohnwu.magisk.FileProvider;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
public class APKInstall {
|
public class APKInstall {
|
||||||
|
|
||||||
|
public static Intent installIntent(Context c, File apk) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
if (Build.VERSION.SDK_INT >= 24) {
|
||||||
|
intent.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
|
||||||
|
} else {
|
||||||
|
//noinspection ResultOfMethodCallIgnored SetWorldReadable
|
||||||
|
apk.setReadable(true, false);
|
||||||
|
intent.setData(Uri.fromFile(apk));
|
||||||
|
}
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
public static void install(Context c, File apk) {
|
public static void install(Context c, File apk) {
|
||||||
c.startActivity(installIntent(c, apk));
|
c.startActivity(installIntent(c, apk));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Intent installIntent(Context c, File apk) {
|
public static void registerInstallReceiver(Context c, BroadcastReceiver r) {
|
||||||
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
IntentFilter filter = new IntentFilter();
|
||||||
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
|
||||||
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
filter.addDataScheme("package");
|
||||||
install.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
|
c.getApplicationContext().registerReceiver(r, filter);
|
||||||
} else {
|
|
||||||
apk.setReadable(true, false);
|
|
||||||
install.setData(Uri.fromFile(apk));
|
|
||||||
}
|
}
|
||||||
return install;
|
|
||||||
|
public static void installHideResult(Activity c, File apk) {
|
||||||
|
Intent intent = installIntent(c, apk);
|
||||||
|
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
|
||||||
|
c.startActivityForResult(intent, 0); // Ignore result, use install receiver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
app/signing/.gitignore
vendored
1
app/signing/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id("java-library")
|
|
||||||
id("java")
|
|
||||||
id("com.github.johnrengelman.shadow") version "6.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
java {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
|
|
||||||
val jar by tasks.getting(Jar::class) {
|
|
||||||
manifest {
|
|
||||||
attributes["Main-Class"] = "com.topjohnwu.signing.ZipSigner"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val shadowJar by tasks.getting(ShadowJar::class) {
|
|
||||||
archiveBaseName.set("zipsigner")
|
|
||||||
archiveClassifier.set(null as String?)
|
|
||||||
archiveVersion.set("4.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
jcenter()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
|
||||||
|
|
||||||
api("org.bouncycastle:bcprov-jdk15on:1.67")
|
|
||||||
api("org.bouncycastle:bcpkix-jdk15on:1.67")
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.topjohnwu.signing;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class BootSigner {
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
|
||||||
String certPath = "";
|
|
||||||
if (args.length >= 2) {
|
|
||||||
/* args[1] is the path to a public key certificate */
|
|
||||||
certPath = args[1];
|
|
||||||
}
|
|
||||||
boolean signed = SignBoot.verifySignature(System.in,
|
|
||||||
certPath.isEmpty() ? null : new FileInputStream(certPath));
|
|
||||||
System.exit(signed ? 0 : 1);
|
|
||||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
|
||||||
InputStream cert = null;
|
|
||||||
InputStream key = null;
|
|
||||||
String name = "/boot";
|
|
||||||
|
|
||||||
if (args.length >= 3) {
|
|
||||||
cert = new FileInputStream(args[1]);
|
|
||||||
key = new FileInputStream(args[2]);
|
|
||||||
}
|
|
||||||
if (args.length == 2) {
|
|
||||||
name = args[1];
|
|
||||||
} else if (args.length >= 4) {
|
|
||||||
name = args[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean success = SignBoot.doSignature(name, System.in, System.out, cert, key);
|
|
||||||
System.exit(success ? 0 : 1);
|
|
||||||
} else {
|
|
||||||
System.err.println(
|
|
||||||
"BootSigner <actions> [args]\n" +
|
|
||||||
"Input from stdin, outputs to stdout\n" +
|
|
||||||
"\n" +
|
|
||||||
"Actions:\n" +
|
|
||||||
" -verify [x509.pem]\n" +
|
|
||||||
" verify image, cert is optional\n" +
|
|
||||||
" -sign [x509.pem] [pk8] [name]\n" +
|
|
||||||
" sign image, name, cert and key pair are optional\n" +
|
|
||||||
" name should be /boot (default) or /recovery\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package com.topjohnwu.signing;
|
|
||||||
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
public class ZipSigner {
|
|
||||||
|
|
||||||
private static void usage() {
|
|
||||||
System.err.println("ZipSigner usage:");
|
|
||||||
System.err.println(" zipsigner.jar input.jar output.jar");
|
|
||||||
System.err.println(" sign jar with AOSP test keys");
|
|
||||||
System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar");
|
|
||||||
System.err.println(" sign jar with certificate / private key pair");
|
|
||||||
System.err.println(" zipsigner.jar keyStore keyStorePass alias keyPass input.jar output.jar");
|
|
||||||
System.err.println(" sign jar with Java KeyStore");
|
|
||||||
System.exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(JarMap input, FileOutputStream output) throws Exception {
|
|
||||||
sign(SignApk.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
|
||||||
SignApk.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(InputStream certIs, InputStream keyIs,
|
|
||||||
JarMap input, FileOutputStream output) throws Exception {
|
|
||||||
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
|
||||||
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
|
||||||
SignApk.sign(cert, key, input, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
|
|
||||||
JarMap in, FileOutputStream out) throws Exception {
|
|
||||||
KeyStore ks;
|
|
||||||
try {
|
|
||||||
ks = KeyStore.getInstance("JKS");
|
|
||||||
try (InputStream is = new FileInputStream(keyStore)) {
|
|
||||||
ks.load(is, keyStorePass.toCharArray());
|
|
||||||
}
|
|
||||||
} catch (KeyStoreException|IOException|CertificateException|NoSuchAlgorithmException e) {
|
|
||||||
ks = KeyStore.getInstance("PKCS12");
|
|
||||||
try (InputStream is = new FileInputStream(keyStore)) {
|
|
||||||
ks.load(is, keyStorePass.toCharArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
|
||||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
|
||||||
SignApk.sign(cert, key, in, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
if (args.length != 2 && args.length != 4 && args.length != 6)
|
|
||||||
usage();
|
|
||||||
|
|
||||||
Security.insertProviderAt(new BouncyCastleProvider(), 1);
|
|
||||||
|
|
||||||
try (JarMap in = JarMap.open(args[args.length - 2], false);
|
|
||||||
FileOutputStream out = new FileOutputStream(args[args.length - 1])) {
|
|
||||||
if (args.length == 2) {
|
|
||||||
sign(in, out);
|
|
||||||
} else if (args.length == 4) {
|
|
||||||
try (InputStream cert = new FileInputStream(args[0]);
|
|
||||||
InputStream key = new FileInputStream(args[1])) {
|
|
||||||
sign(cert, key, in, out);
|
|
||||||
}
|
|
||||||
} else if (args.length == 6) {
|
|
||||||
sign(args[0], args[1], args[2], args[3], in, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,21 +3,19 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.magisk">
|
package="com.topjohnwu.magisk">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
||||||
android:maxSdkVersion="29" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".core.App"
|
||||||
|
android:extractNativeLibs="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:name="a.e"
|
android:multiArch="true"
|
||||||
android:allowBackup="true"
|
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||||
|
|
||||||
<!-- Splash -->
|
<!-- Splash -->
|
||||||
<activity
|
<activity
|
||||||
android:name="a.c"
|
android:name=".core.SplashActivity"
|
||||||
|
android:exported="true"
|
||||||
android:theme="@style/SplashTheme">
|
android:theme="@style/SplashTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
@@ -30,11 +28,11 @@
|
|||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<activity android:name="a.b" />
|
<activity android:name=".ui.MainActivity" />
|
||||||
|
|
||||||
<!-- Superuser -->
|
<!-- Superuser -->
|
||||||
<activity
|
<activity
|
||||||
android:name="a.m"
|
android:name=".ui.surequest.SuRequestActivity"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
@@ -47,31 +45,30 @@
|
|||||||
|
|
||||||
<!-- Receiver -->
|
<!-- Receiver -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name="a.h"
|
android:name=".core.Receiver"
|
||||||
android:directBootAware="true">
|
android:directBootAware="true"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.REBOOT" />
|
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
|
<action android:name="android.intent.action.UID_REMOVED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||||
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
|
|
||||||
|
|
||||||
<data android:scheme="package" />
|
<data android:scheme="package" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- DownloadService -->
|
<!-- DownloadService -->
|
||||||
<service android:name="a.j" />
|
<service android:name=".core.download.DownloadService" />
|
||||||
|
|
||||||
<!-- FileProvider -->
|
<!-- FileProvider -->
|
||||||
<provider
|
<provider
|
||||||
android:name="a.p"
|
android:name=".core.Provider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true" />
|
||||||
</provider>
|
|
||||||
|
|
||||||
<!-- Hardcode GMS version -->
|
<!-- Hardcode GMS version -->
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -82,18 +79,14 @@
|
|||||||
<provider
|
<provider
|
||||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||||
android:authorities="${applicationId}.workmanager-init"
|
android:authorities="${applicationId}.workmanager-init"
|
||||||
tools:node="remove" />
|
tools:node="remove"
|
||||||
|
tools:ignore="ExportedContentProvider" />
|
||||||
|
|
||||||
<!-- We don't invalidate Room -->
|
<!-- We don't invalidate Room -->
|
||||||
<service
|
<service
|
||||||
android:name="androidx.room.MultiInstanceInvalidationService"
|
android:name="androidx.room.MultiInstanceInvalidationService"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
<!-- We don't use Device Credentials -->
|
|
||||||
<activity
|
|
||||||
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
|
|
||||||
tools:node="remove" />
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
@file:JvmName("a")
|
|
||||||
package a
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.core.App
|
|
||||||
import com.topjohnwu.magisk.core.Provider
|
|
||||||
import com.topjohnwu.magisk.core.Receiver
|
|
||||||
import com.topjohnwu.magisk.core.SplashActivity
|
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
import com.topjohnwu.signing.BootSigner
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
|
||||||
BootSigner.main(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
class b : MainActivity()
|
|
||||||
|
|
||||||
class c : SplashActivity()
|
|
||||||
|
|
||||||
class e : App {
|
|
||||||
constructor() : super()
|
|
||||||
constructor(o: Any) : super(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
class h : Receiver()
|
|
||||||
|
|
||||||
class j : DownloadService()
|
|
||||||
|
|
||||||
class m : SuRequestActivity()
|
|
||||||
|
|
||||||
class p : Provider()
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -7,13 +10,13 @@ import androidx.appcompat.app.AppCompatDelegate
|
|||||||
import androidx.core.content.res.use
|
import androidx.core.content.res.use
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
|
import com.topjohnwu.magisk.ui.inflater.LayoutInflaterFactory
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
|
|
||||||
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||||
@@ -24,7 +27,7 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
protected open val themeRes: Int = Theme.selected.themeRes
|
protected open val themeRes: Int = Theme.selected.themeRes
|
||||||
|
|
||||||
private val navHostFragment by lazy {
|
private val navHostFragment by lazy {
|
||||||
supportFragmentManager.findFragmentById(navHost) as? NavHostFragment
|
supportFragmentManager.findFragmentById(navHostId) as? NavHostFragment
|
||||||
}
|
}
|
||||||
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
|
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
|
||||||
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
|
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
|
||||||
@@ -32,7 +35,7 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
override val viewRoot: View get() = binding.root
|
override val viewRoot: View get() = binding.root
|
||||||
open val navigation: NavController? get() = navHostFragment?.navController
|
open val navigation: NavController? get() = navHostFragment?.navController
|
||||||
|
|
||||||
open val navHost: Int = 0
|
open val navHostId: Int = 0
|
||||||
open val snackbarView get() = binding.root
|
open val snackbarView get() = binding.root
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -41,6 +44,8 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||||
|
|
||||||
setTheme(themeRes)
|
setTheme(themeRes)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -52,11 +57,28 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
.use { it.getDrawable(0) }
|
.use { it.getDrawable(0) }
|
||||||
.also { window.setBackgroundDrawable(it) }
|
.also { window.setBackgroundDrawable(it) }
|
||||||
|
|
||||||
directionsDispatcher.observe(this) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
it?.navigate()
|
window?.decorView?.let {
|
||||||
// we don't want the directions to be re-dispatched, so we preemptively set them to null
|
it.systemUiVisibility = (it.systemUiVisibility
|
||||||
if (it != null) {
|
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
directionsDispatcher.value = null
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
window?.decorView?.post {
|
||||||
|
// If navigation bar is short enough (gesture navigation enabled), make it transparent
|
||||||
|
if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 < Resources.getSystem().displayMetrics.density * 40) {
|
||||||
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
window.isStatusBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,8 +88,6 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = this
|
it.lifecycleOwner = this
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureInsets()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
|
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
|
||||||
@@ -98,14 +118,4 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.navigate(this)
|
navigation?.navigate(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private val directionsDispatcher = MutableLiveData<NavDirections?>()
|
|
||||||
|
|
||||||
fun postDirections(navDirections: NavDirections) =
|
|
||||||
directionsDispatcher.postValue(navDirections)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
|
||||||
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
|
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
|
||||||
@@ -17,47 +14,8 @@ interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called for all [ViewEvent]s published by associated viewModel.
|
* Called for all [ViewEvent]s published by associated viewModel.
|
||||||
*/
|
*/
|
||||||
fun onEventDispatched(event: ViewEvent) {}
|
fun onEventDispatched(event: ViewEvent) {}
|
||||||
|
|
||||||
fun ensureInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(viewRoot) { _, insets ->
|
|
||||||
insets.asInsets()
|
|
||||||
.also { viewModel.insets = it }
|
|
||||||
.let { consumeSystemWindowInsets(it) }
|
|
||||||
?.subtractBy(insets) ?: insets
|
|
||||||
}
|
|
||||||
if (ViewCompat.isAttachedToWindow(viewRoot)) {
|
|
||||||
ViewCompat.requestApplyInsets(viewRoot)
|
|
||||||
} else {
|
|
||||||
viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
|
||||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
|
||||||
override fun onViewAttachedToWindow(v: View) {
|
|
||||||
ViewCompat.requestApplyInsets(v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun WindowInsetsCompat.asInsets() = Insets.of(
|
|
||||||
systemWindowInsetLeft,
|
|
||||||
systemWindowInsetTop,
|
|
||||||
systemWindowInsetRight,
|
|
||||||
systemWindowInsetBottom
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun Insets.subtractBy(insets: WindowInsetsCompat) =
|
|
||||||
WindowInsetsCompat.Builder(insets).setSystemWindowInsets(
|
|
||||||
Insets.of(
|
|
||||||
insets.systemWindowInsetLeft - left,
|
|
||||||
insets.systemWindowInsetTop - top,
|
|
||||||
insets.systemWindowInsetRight - right,
|
|
||||||
insets.systemWindowInsetBottom - bottom
|
|
||||||
)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.view.KeyEvent
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.graphics.Insets
|
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.OnRebindCallback
|
import androidx.databinding.OnRebindCallback
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
@@ -24,8 +23,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
override val viewRoot: View get() = binding.root
|
override val viewRoot: View get() = binding.root
|
||||||
private val navigation get() = activity.navigation
|
private val navigation get() = activity.navigation
|
||||||
|
|
||||||
override fun consumeSystemWindowInsets(insets: Insets) = insets
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
startObserveEvents()
|
startObserveEvents()
|
||||||
@@ -43,6 +40,11 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
activity.supportActionBar?.subtitle = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
||||||
is ContextExecutor -> event(requireContext())
|
is ContextExecutor -> event(requireContext())
|
||||||
is ActivityExecutor -> event(activity)
|
is ActivityExecutor -> event(activity)
|
||||||
@@ -65,7 +67,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ensureInsets()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
@@ -107,7 +106,7 @@ abstract class BaseViewModel(
|
|||||||
_viewEvents.postValue(this)
|
_viewEvents.postValue(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.publish() {
|
fun NavDirections.navigate() {
|
||||||
_viewEvents.postValue(NavigationEvent(this))
|
_viewEvents.postValue(NavigationEvent(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import androidx.multidex.MultiDex
|
|||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.DynAPK
|
||||||
|
import com.topjohnwu.magisk.core.utils.AppShellInit
|
||||||
|
import com.topjohnwu.magisk.core.utils.BusyBoxInit
|
||||||
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
|
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
|
||||||
import com.topjohnwu.magisk.core.utils.RootInit
|
|
||||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||||
import com.topjohnwu.magisk.di.koinModules
|
import com.topjohnwu.magisk.di.koinModules
|
||||||
import com.topjohnwu.magisk.ktx.unwrap
|
import com.topjohnwu.magisk.ktx.unwrap
|
||||||
@@ -19,6 +20,7 @@ import com.topjohnwu.superuser.Shell
|
|||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
open class App() : Application() {
|
open class App() : Application() {
|
||||||
@@ -31,7 +33,7 @@ open class App() : Application() {
|
|||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||||
.setInitializers(RootInit::class.java)
|
.setInitializers(BusyBoxInit::class.java, AppShellInit::class.java)
|
||||||
.setTimeout(2))
|
.setTimeout(2))
|
||||||
Shell.EXECUTOR = IODispatcherExecutor()
|
Shell.EXECUTOR = IODispatcherExecutor()
|
||||||
|
|
||||||
@@ -61,12 +63,18 @@ open class App() : Application() {
|
|||||||
val wrapped = impl.wrap()
|
val wrapped = impl.wrap()
|
||||||
super.attachBaseContext(wrapped)
|
super.attachBaseContext(wrapped)
|
||||||
|
|
||||||
|
val info = base.applicationInfo
|
||||||
|
val libDir = runCatching {
|
||||||
|
info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
|
||||||
|
}.getOrNull() ?: info.nativeLibraryDir
|
||||||
|
Const.NATIVE_LIB_DIR = File(libDir)
|
||||||
|
|
||||||
// Normal startup
|
// Normal startup
|
||||||
startKoin {
|
startKoin {
|
||||||
androidContext(wrapped)
|
androidContext(wrapped)
|
||||||
modules(koinModules)
|
modules(koinModules)
|
||||||
}
|
}
|
||||||
ResMgr.init(impl)
|
AssetHack.init(impl)
|
||||||
app.registerActivityLifecycleCallbacks(ForegroundTracker)
|
app.registerActivityLifecycleCallbacks(ForegroundTracker)
|
||||||
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import com.topjohnwu.magisk.ktx.inject
|
|||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Config : PreferenceModel, DBConfig {
|
object Config : PreferenceModel, DBConfig {
|
||||||
@@ -159,12 +158,13 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
|
|
||||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||||
|
|
||||||
fun load(pkg: String) {
|
fun load(pkg: String?) {
|
||||||
try {
|
// Only try to load prefs when fresh install and a previous package name is set
|
||||||
|
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
||||||
prefs.edit { parsePrefs(it) }
|
prefs.edit { parsePrefs(it) }
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {}
|
}
|
||||||
|
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
// Settings migration
|
// Settings migration
|
||||||
|
|||||||
@@ -1,44 +1,53 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
object Const {
|
object Const {
|
||||||
|
|
||||||
|
val CPU_ABI: String
|
||||||
|
val CPU_ABI_32: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
CPU_ABI = Build.SUPPORTED_ABIS[0]
|
||||||
|
CPU_ABI_32 = Build.SUPPORTED_32_BIT_ABIS.firstOrNull() ?: CPU_ABI
|
||||||
|
} else {
|
||||||
|
CPU_ABI = Build.CPU_ABI
|
||||||
|
CPU_ABI_32 = CPU_ABI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Paths
|
// Paths
|
||||||
lateinit var MAGISKTMP: String
|
lateinit var MAGISKTMP: String
|
||||||
|
lateinit var NATIVE_LIB_DIR: File
|
||||||
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
val MAGISK_PATH get() = "$MAGISKTMP/modules"
|
||||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
const val TMPDIR = "/dev/tmp"
|
||||||
const val MAGISK_LOG = "/cache/magisk.log"
|
const val MAGISK_LOG = "/cache/magisk.log"
|
||||||
|
|
||||||
// Versions
|
// Versions
|
||||||
const val SNET_EXT_VER = 15
|
const val SNET_EXT_VER = 15
|
||||||
const val SNET_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
|
const val SNET_REVISION = "22.0"
|
||||||
const val BOOTCTL_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
|
const val BOOTCTL_REVISION = "22.0"
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
|
||||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
|
||||||
val USER_ID = Process.myUid() / 100000
|
val USER_ID = Process.myUid() / 100000
|
||||||
|
|
||||||
object Version {
|
object Version {
|
||||||
const val MIN_VERSION = "v19.0"
|
const val MIN_VERSION = "v20.4"
|
||||||
const val MIN_VERCODE = 19000
|
const val MIN_VERCODE = 20400
|
||||||
|
|
||||||
fun atLeast_20_2() = Info.env.magiskVersionCode >= 20200 || isCanary()
|
|
||||||
fun atLeast_20_4() = Info.env.magiskVersionCode >= 20400 || isCanary()
|
|
||||||
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
|
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
|
||||||
|
fun atLeast_21_2() = Info.env.magiskVersionCode >= 21200 || isCanary()
|
||||||
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
|
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
object ID {
|
object ID {
|
||||||
const val FETCH_ZIP = 2
|
|
||||||
const val SELECT_FILE = 3
|
|
||||||
const val MAX_ACTIVITY_RESULT = 10
|
|
||||||
|
|
||||||
// notifications
|
// notifications
|
||||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
|
||||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||||
const val HIDE_MANAGER_NOTIFICATION_ID = 8
|
|
||||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
||||||
@@ -48,9 +57,13 @@ object Const {
|
|||||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||||
|
|
||||||
|
val CHANGELOG_URL = if (BuildConfig.VERSION_CODE % 100 != 0) Info.remote.magisk.note
|
||||||
|
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.VERSION_CODE}.md"
|
||||||
|
|
||||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||||
const val GITHUB_API_URL = "https://api.github.com/"
|
const val GITHUB_API_URL = "https://api.github.com/"
|
||||||
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
|
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
|
||||||
|
const val GITHUB_OLD_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
|
||||||
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
||||||
const val OFFICIAL_REPO = "https://magisk-modules-repo.github.io/submission/modules.json"
|
const val OFFICIAL_REPO = "https://magisk-modules-repo.github.io/submission/modules.json"
|
||||||
}
|
}
|
||||||
@@ -58,7 +71,7 @@ object Const {
|
|||||||
object Key {
|
object Key {
|
||||||
// intents
|
// intents
|
||||||
const val OPEN_SECTION = "section"
|
const val OPEN_SECTION = "section"
|
||||||
const val HIDDEN_PKG = "hidden_pkg"
|
const val PREV_PKG = "prev_pkg"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Value {
|
object Value {
|
||||||
|
|||||||
@@ -14,44 +14,38 @@ import android.content.Intent
|
|||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.util.DisplayMetrics
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.DynAPK
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
|
||||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||||
import com.topjohnwu.magisk.ktx.forceGetDeclaredField
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
|
|
||||||
fun AssetManager.addAssetPath(path: String) {
|
fun AssetManager.addAssetPath(path: String) {
|
||||||
DynAPK.addAssetPath(this, path)
|
DynAPK.addAssetPath(this, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.wrap(global: Boolean = true): Context =
|
fun Context.wrap(inject: Boolean = false): Context =
|
||||||
if (global) GlobalResContext(this) else ResContext(this)
|
if (inject) ReInjectedContext(this) else InjectedContext(this)
|
||||||
|
|
||||||
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
fun Context.wrapJob(): Context = object : InjectedContext(this) {
|
||||||
|
|
||||||
override fun getApplicationContext(): Context {
|
override fun getApplicationContext() = this
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
override fun getSystemService(name: String): Any? {
|
override fun getSystemService(name: String): Any? {
|
||||||
return if (!isRunningAsStub) super.getSystemService(name) else
|
return super.getSystemService(name).let {
|
||||||
when (name) {
|
when {
|
||||||
Context.JOB_SCHEDULER_SERVICE ->
|
!isRunningAsStub -> it
|
||||||
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
|
name == JOB_SCHEDULER_SERVICE -> JobSchedulerWrapper(it as JobScheduler)
|
||||||
else -> super.getSystemService(name)
|
else -> it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Class<*>.cmp(pkg: String): ComponentName {
|
fun Class<*>.cmp(pkg: String) =
|
||||||
val name = ClassMap[this].name
|
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||||
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
inline fun <reified T> Activity.redirect() = Intent(intent)
|
||||||
.setComponent(T::class.java.cmp(packageName))
|
.setComponent(T::class.java.cmp(packageName))
|
||||||
@@ -59,34 +53,27 @@ inline fun <reified T> Activity.redirect() = Intent(intent)
|
|||||||
|
|
||||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||||
|
|
||||||
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
private open class InjectedContext(base: Context) : ContextWrapper(base) {
|
||||||
open val mRes: Resources get() = ResMgr.resource
|
open val res: Resources get() = AssetHack.resource
|
||||||
|
override fun getAssets(): AssetManager = res.assets
|
||||||
override fun getResources(): Resources {
|
override fun getResources() = res
|
||||||
return mRes
|
override fun getClassLoader() = javaClass.classLoader!!
|
||||||
}
|
|
||||||
|
|
||||||
override fun getClassLoader(): ClassLoader {
|
|
||||||
return javaClass.classLoader!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createConfigurationContext(config: Configuration): Context {
|
override fun createConfigurationContext(config: Configuration): Context {
|
||||||
return ResContext(super.createConfigurationContext(config))
|
return super.createConfigurationContext(config).wrap(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ResContext(base: Context) : GlobalResContext(base) {
|
private class ReInjectedContext(base: Context) : InjectedContext(base) {
|
||||||
override val mRes by lazy { base.resources.patch() }
|
override val res by lazy { base.resources.patch() }
|
||||||
|
|
||||||
private fun Resources.patch(): Resources {
|
private fun Resources.patch(): Resources {
|
||||||
updateConfig()
|
updateConfig()
|
||||||
if (isRunningAsStub)
|
if (isRunningAsStub)
|
||||||
assets.addAssetPath(ResMgr.apk)
|
assets.addAssetPath(AssetHack.apk)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object ResMgr {
|
object AssetHack {
|
||||||
|
|
||||||
lateinit var resource: Resources
|
lateinit var resource: Resources
|
||||||
lateinit var apk: String
|
lateinit var apk: String
|
||||||
@@ -101,62 +88,39 @@ object ResMgr {
|
|||||||
apk = context.packageResourcePath
|
apk = context.packageResourcePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newResource(): Resources {
|
||||||
|
val asset = AssetManager::class.java.newInstance()
|
||||||
|
asset.addAssetPath(apk)
|
||||||
|
val config = Configuration(resource.configuration)
|
||||||
|
val metrics = DisplayMetrics()
|
||||||
|
metrics.setTo(resource.displayMetrics)
|
||||||
|
return Resources(asset, metrics, config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(28)
|
@RequiresApi(28)
|
||||||
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
||||||
|
override fun schedule(job: JobInfo) = base.schedule(job.patch())
|
||||||
override fun schedule(job: JobInfo): Int {
|
override fun enqueue(job: JobInfo, work: JobWorkItem) = base.enqueue(job.patch(), work)
|
||||||
return base.schedule(job.patch())
|
override fun cancel(jobId: Int) = base.cancel(jobId)
|
||||||
}
|
override fun cancelAll() = base.cancelAll()
|
||||||
|
override fun getAllPendingJobs(): List<JobInfo> = base.allPendingJobs
|
||||||
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
|
override fun getPendingJob(jobId: Int) = base.getPendingJob(jobId)
|
||||||
return base.enqueue(job.patch(), work)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel(jobId: Int) {
|
|
||||||
base.cancel(jobId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancelAll() {
|
|
||||||
base.cancelAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getAllPendingJobs(): List<JobInfo> {
|
|
||||||
return base.allPendingJobs
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPendingJob(jobId: Int): JobInfo? {
|
|
||||||
return base.getPendingJob(jobId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JobInfo.patch(): JobInfo {
|
private fun JobInfo.patch(): JobInfo {
|
||||||
// We need to swap out the service of JobInfo
|
// Swap out the service of JobInfo
|
||||||
val name = service.className
|
val component = service.run {
|
||||||
val component = ComponentName(
|
ComponentName(packageName,
|
||||||
service.packageName,
|
Info.stub?.classToComponent?.get(className) ?: className)
|
||||||
Info.stubChk.classToComponent[name] ?: name
|
}
|
||||||
)
|
javaClass.getDeclaredField("service").apply {
|
||||||
|
isAccessible = true
|
||||||
|
}.set(this, component)
|
||||||
|
|
||||||
javaClass.forceGetDeclaredField("service")?.set(this, component)
|
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ClassMap {
|
|
||||||
|
|
||||||
private val map = mapOf(
|
|
||||||
App::class.java to a.e::class.java,
|
|
||||||
MainActivity::class.java to a.b::class.java,
|
|
||||||
SplashActivity::class.java to a.c::class.java,
|
|
||||||
Receiver::class.java to a.h::class.java,
|
|
||||||
DownloadService::class.java to a.j::class.java,
|
|
||||||
SuRequestActivity::class.java to a.m::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep a reference to these resources to prevent it from
|
// Keep a reference to these resources to prevent it from
|
||||||
// being removed when running "remove unused resources"
|
// being removed when running "remove unused resources"
|
||||||
val shouldKeepResources = listOf(
|
val shouldKeepResources = listOf(
|
||||||
|
|||||||
@@ -1,41 +1,45 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import androidx.databinding.ObservableBoolean
|
import androidx.databinding.ObservableBoolean
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.DynAPK
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
||||||
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.magisk.utils.CachedValue
|
import com.topjohnwu.magisk.ktx.getProperty
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import java.io.FileInputStream
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
|
||||||
object Info {
|
object Info {
|
||||||
|
|
||||||
val envRef = CachedValue { loadState() }
|
|
||||||
|
|
||||||
@JvmStatic val env by envRef
|
|
||||||
|
|
||||||
var stub: DynAPK.Data? = null
|
var stub: DynAPK.Data? = null
|
||||||
val stubChk: DynAPK.Data
|
|
||||||
get() = stub as DynAPK.Data
|
|
||||||
|
|
||||||
var remote = UpdateInfo()
|
val EMPTY_REMOTE = UpdateInfo()
|
||||||
|
var remote = EMPTY_REMOTE
|
||||||
|
suspend fun getRemote(svc: NetworkService): UpdateInfo? {
|
||||||
|
return if (remote === EMPTY_REMOTE) {
|
||||||
|
svc.fetchUpdate()?.apply { remote = this }
|
||||||
|
} else remote
|
||||||
|
}
|
||||||
|
|
||||||
// Device state
|
// Device state
|
||||||
var crypto = ""
|
@JvmStatic val env by lazy { loadState() }
|
||||||
@JvmStatic var isSAR = false
|
@JvmField var isSAR = false
|
||||||
@JvmStatic var isAB = false
|
@JvmField var isAB = false
|
||||||
|
@JvmField val isVirtualAB = getProperty("ro.virtual_ab.enabled", "false") == "true"
|
||||||
@JvmStatic val isFDE get() = crypto == "block"
|
@JvmStatic val isFDE get() = crypto == "block"
|
||||||
@JvmStatic var ramdisk = false
|
@JvmField var ramdisk = false
|
||||||
@JvmStatic var hasGMS = true
|
@JvmField var hasGMS = true
|
||||||
@JvmStatic var isPixel = false
|
@JvmField val isPixel = Build.BRAND == "google"
|
||||||
@JvmStatic val cryptoText get() = crypto.capitalize(Locale.US)
|
@JvmField val isEmulator = getProperty("ro.kernel.qemu", "0") == "1"
|
||||||
|
var crypto = ""
|
||||||
|
var noDataExec = false
|
||||||
|
|
||||||
val isConnected by lazy {
|
val isConnected by lazy {
|
||||||
ObservableBoolean(false).also { field ->
|
ObservableBoolean(false).also { field ->
|
||||||
@@ -47,15 +51,13 @@ object Info {
|
|||||||
|
|
||||||
val isNewReboot by lazy {
|
val isNewReboot by lazy {
|
||||||
try {
|
try {
|
||||||
FileInputStream("/proc/sys/kernel/random/boot_id").bufferedReader().use {
|
val id = File("/proc/sys/kernel/random/boot_id").readText()
|
||||||
val id = it.readLine()
|
|
||||||
if (id != Config.bootId) {
|
if (id != Config.bootId) {
|
||||||
Config.bootId = id
|
Config.bootId = id
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -64,7 +66,7 @@ object Info {
|
|||||||
private fun loadState() = Env(
|
private fun loadState() = Env(
|
||||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
fastCmd("magisk -v").split(":".toRegex())[0],
|
||||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1),
|
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1),
|
||||||
Shell.su("magiskhide --status").exec().isSuccess
|
Shell.su("magiskhide status").exec().isSuccess
|
||||||
)
|
)
|
||||||
|
|
||||||
class Env(
|
class Env(
|
||||||
@@ -73,8 +75,8 @@ object Info {
|
|||||||
hide: Boolean = false
|
hide: Boolean = false
|
||||||
) {
|
) {
|
||||||
val magiskHide get() = Config.magiskHide
|
val magiskHide get() = Config.magiskHide
|
||||||
val magiskVersionCode = when (code) {
|
val magiskVersionCode = when {
|
||||||
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
|
code < Const.Version.MIN_VERCODE -> -1
|
||||||
else -> if (Shell.rootAccess()) code else -1
|
else -> if (Shell.rootAccess()) code else -1
|
||||||
}
|
}
|
||||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
@@ -15,30 +15,33 @@ open class Receiver : BaseReceiver() {
|
|||||||
|
|
||||||
private val policyDB: PolicyDao by inject()
|
private val policyDB: PolicyDao by inject()
|
||||||
|
|
||||||
private fun getPkg(intent: Intent): String {
|
@SuppressLint("InlinedApi")
|
||||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
private fun getPkg(intent: Intent): String? {
|
||||||
|
val pkg = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME)
|
||||||
|
return pkg ?: intent.data?.schemeSpecificPart
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUid(intent: Intent): Int? {
|
||||||
|
val uid = intent.getIntExtra(Intent.EXTRA_UID, -1)
|
||||||
|
return if (uid == -1) null else uid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||||
intent ?: return
|
intent ?: return
|
||||||
|
|
||||||
fun rmPolicy(pkg: String) = GlobalScope.launch {
|
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||||
policyDB.delete(pkg)
|
policyDB.delete(uid)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (intent.action ?: return) {
|
when (intent.action ?: return) {
|
||||||
Intent.ACTION_REBOOT -> {
|
|
||||||
SuCallbackHandler(context, intent.getStringExtra("action"), intent.extras)
|
|
||||||
}
|
|
||||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||||
// This will only work pre-O
|
// This will only work pre-O
|
||||||
if (Config.suReAuth)
|
if (Config.suReAuth)
|
||||||
rmPolicy(getPkg(intent))
|
getUid(intent)?.let { rmPolicy(it) }
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
Intent.ACTION_UID_REMOVED -> {
|
||||||
val pkg = getPkg(intent)
|
getUid(intent)?.let { rmPolicy(it) }
|
||||||
rmPolicy(pkg)
|
getPkg(intent)?.let { Shell.su("magiskhide rm $it").submit() }
|
||||||
Shell.su("magiskhide --rm $pkg").submit()
|
|
||||||
}
|
}
|
||||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.app.Activity
|
import android.content.Intent
|
||||||
import android.content.Context
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
import com.topjohnwu.magisk.ui.MainActivity
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import java.util.concurrent.CountDownLatch
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
open class SplashActivity : Activity() {
|
open class SplashActivity : BaseActivity() {
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
private val latch = CountDownLatch(1)
|
||||||
super.attachBaseContext(base.wrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setTheme(R.style.SplashTheme)
|
setTheme(R.style.SplashTheme)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
// Pre-initialize root shell
|
||||||
initAndStart()
|
Shell.getShell(null) { initAndStart() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRepackage(pkg: String?) {
|
private fun handleRepackage(pkg: String?) {
|
||||||
@@ -40,18 +38,31 @@ open class SplashActivity : Activity() {
|
|||||||
if (Config.suManager.isNotEmpty())
|
if (Config.suManager.isNotEmpty())
|
||||||
Config.suManager = ""
|
Config.suManager = ""
|
||||||
pkg ?: return
|
pkg ?: return
|
||||||
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess)
|
||||||
|
uninstallApp(pkg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initAndStart() {
|
private fun initAndStart() {
|
||||||
// Pre-initialize root shell
|
if (isRunningAsStub && !Shell.rootAccess()) {
|
||||||
Shell.getShell()
|
runOnUiThread {
|
||||||
|
MagiskDialog(this)
|
||||||
|
.applyTitle(R.string.unsupport_nonroot_stub_title)
|
||||||
|
.applyMessage(R.string.unsupport_nonroot_stub_msg)
|
||||||
|
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
|
titleRes = R.string.install
|
||||||
|
onClick { HideAPK.restore(this@SplashActivity) }
|
||||||
|
}
|
||||||
|
.cancellable(false)
|
||||||
|
.reveal()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val hiddenPackage = intent.getStringExtra(Const.Key.HIDDEN_PKG)
|
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
||||||
|
|
||||||
Config.load(hiddenPackage ?: APPLICATION_ID)
|
Config.load(prevPkg)
|
||||||
handleRepackage(hiddenPackage)
|
handleRepackage(prevPkg)
|
||||||
Notifications.setup(this)
|
Notifications.setup(this)
|
||||||
UpdateCheckService.schedule(this)
|
UpdateCheckService.schedule(this)
|
||||||
Shortcuts.setupDynamic(this)
|
Shortcuts.setupDynamic(this)
|
||||||
@@ -60,11 +71,21 @@ open class SplashActivity : Activity() {
|
|||||||
get<NetworkService>()
|
get<NetworkService>()
|
||||||
|
|
||||||
DONE = true
|
DONE = true
|
||||||
|
startActivity(redirect<MainActivity>())
|
||||||
redirect<MainActivity>().also { startActivity(it) }
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun uninstallApp(pkg: String) {
|
||||||
|
val uri = Uri.Builder().scheme("package").opaquePart(pkg).build()
|
||||||
|
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
|
||||||
|
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||||
|
startActivityForResult(intent) { _, _ ->
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
latch.await()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var DONE = false
|
var DONE = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.inject
|
import org.koin.core.inject
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -18,20 +16,15 @@ class UpdateCheckService(context: Context, workerParams: WorkerParameters)
|
|||||||
private val svc: NetworkService by inject()
|
private val svc: NetworkService by inject()
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
// Make sure shell initializer was ran
|
return svc.fetchUpdate()?.run {
|
||||||
withContext(Dispatchers.IO) {
|
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
|
||||||
Shell.getShell()
|
|
||||||
}
|
|
||||||
return svc.fetchUpdate()?.let {
|
|
||||||
if (BuildConfig.VERSION_CODE < it.app.versionCode)
|
|
||||||
Notifications.managerUpdate(applicationContext)
|
Notifications.managerUpdate(applicationContext)
|
||||||
else if (Info.env.isActive && Info.env.magiskVersionCode < it.magisk.versionCode)
|
|
||||||
Notifications.magiskUpdate(applicationContext)
|
|
||||||
Result.success()
|
Result.success()
|
||||||
} ?: Result.failure()
|
} ?: Result.failure()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@SuppressLint("NewApi")
|
||||||
fun schedule(context: Context) {
|
fun schedule(context: Context) {
|
||||||
if (Config.checkUpdate) {
|
if (Config.checkUpdate) {
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.collection.SparseArrayCompat
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.set
|
import com.topjohnwu.magisk.ktx.set
|
||||||
@@ -26,6 +25,13 @@ typealias ActivityResultCallback = BaseActivity.(Int, Intent?) -> Unit
|
|||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val resultCallbacks by lazy { SparseArrayCompat<ActivityResultCallback>() }
|
private val resultCallbacks by lazy { SparseArrayCompat<ActivityResultCallback>() }
|
||||||
|
private val newRequestCode: Int get() {
|
||||||
|
var requestCode: Int
|
||||||
|
do {
|
||||||
|
requestCode = Random.nextInt(0, 1 shl 15)
|
||||||
|
} while (resultCallbacks.containsKey(requestCode))
|
||||||
|
return requestCode
|
||||||
|
}
|
||||||
|
|
||||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||||
// Force applying our preferred local
|
// Force applying our preferred local
|
||||||
@@ -34,7 +40,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base.wrap(false))
|
super.attachBaseContext(base.wrap(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
|
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||||
@@ -49,10 +55,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
|
||||||
request.onSuccess()
|
request.onSuccess()
|
||||||
} else {
|
} else {
|
||||||
var requestCode: Int
|
val requestCode = newRequestCode
|
||||||
do {
|
|
||||||
requestCode = Random.nextInt(Const.ID.MAX_ACTIVITY_RESULT + 1, 1 shl 15)
|
|
||||||
} while (resultCallbacks.containsKey(requestCode))
|
|
||||||
resultCallbacks[requestCode] = { result, _ ->
|
resultCallbacks[requestCode] = { result, _ ->
|
||||||
if (result > 0)
|
if (result > 0)
|
||||||
request.onSuccess()
|
request.onSuccess()
|
||||||
@@ -92,7 +95,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startActivityForResult(intent: Intent, requestCode: Int, callback: ActivityResultCallback) {
|
fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) {
|
||||||
|
val requestCode = newRequestCode
|
||||||
resultCallbacks[requestCode] = callback
|
resultCallbacks[requestCode] = callback
|
||||||
try {
|
try {
|
||||||
startActivityForResult(intent, requestCode)
|
startActivityForResult(intent, requestCode)
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
|
|
||||||
sealed class Action : Parcelable {
|
|
||||||
|
|
||||||
sealed class Flash : Action() {
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Primary : Flash()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Secondary : Flash()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Download : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object Uninstall : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
object EnvFix : Action()
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class Patch(val fileUri: Uri) : Action()
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -8,11 +8,8 @@ import androidx.lifecycle.MutableLiveData
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.ForegroundTracker
|
import com.topjohnwu.magisk.core.ForegroundTracker
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.checkSum
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -69,18 +66,11 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
|
|||||||
// -- Download logic
|
// -- Download logic
|
||||||
|
|
||||||
private suspend fun Subject.startDownload() {
|
private suspend fun Subject.startDownload() {
|
||||||
val skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
|
|
||||||
if (!skip) {
|
|
||||||
val stream = service.fetchFile(url).toProgressStream(this)
|
val stream = service.fetchFile(url).toProgressStream(this)
|
||||||
when (this) {
|
when (this) {
|
||||||
is Subject.Module -> // Download and process on-the-fly
|
is Subject.Module -> // Download and process on-the-fly
|
||||||
stream.toModule(file, service.fetchInstaller().byteStream())
|
stream.toModule(file, service.fetchInstaller().byteStream())
|
||||||
else -> {
|
is Subject.Manager -> handleAPK(this, stream)
|
||||||
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
|
|
||||||
if (this is Subject.Manager)
|
|
||||||
handleAPK(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val newId = notifyFinish(this)
|
val newId = notifyFinish(this)
|
||||||
if (ForegroundTracker.hasForeground)
|
if (ForegroundTracker.hasForeground)
|
||||||
@@ -187,7 +177,7 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
|
|||||||
companion object : KoinComponent {
|
companion object : KoinComponent {
|
||||||
const val ACTION_KEY = "download_action"
|
const val ACTION_KEY = "download_action"
|
||||||
|
|
||||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>>()
|
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||||
|
|
||||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||||
progressBroadcast.value = null
|
progressBroadcast.value = null
|
||||||
|
|||||||
@@ -1,46 +1,38 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.download.Action.*
|
import com.topjohnwu.magisk.arch.BaseUIActivity
|
||||||
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
|
import com.topjohnwu.magisk.core.ForegroundTracker
|
||||||
import com.topjohnwu.magisk.core.download.Subject.*
|
import com.topjohnwu.magisk.core.download.Action.Flash
|
||||||
|
import com.topjohnwu.magisk.core.download.Subject.Manager
|
||||||
|
import com.topjohnwu.magisk.core.download.Subject.Module
|
||||||
import com.topjohnwu.magisk.core.intent
|
import com.topjohnwu.magisk.core.intent
|
||||||
import com.topjohnwu.magisk.core.tasks.EnvFixTask
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import kotlin.random.Random.Default.nextInt
|
import kotlin.random.Random.Default.nextInt
|
||||||
|
|
||||||
@SuppressLint("Registered")
|
|
||||||
open class DownloadService : BaseDownloader() {
|
open class DownloadService : BaseDownloader() {
|
||||||
|
|
||||||
private val context get() = this
|
private val context get() = this
|
||||||
|
|
||||||
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
|
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
|
||||||
is Magisk -> subject.onFinish(id)
|
|
||||||
is Module -> subject.onFinish(id)
|
is Module -> subject.onFinish(id)
|
||||||
is Manager -> subject.onFinish(id)
|
is Manager -> subject.onFinish(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun Magisk.onFinish(id: Int) = when (val action = action) {
|
|
||||||
Uninstall -> FlashFragment.uninstall(file, id)
|
|
||||||
EnvFix -> {
|
|
||||||
remove(id)
|
|
||||||
EnvFixTask(file).exec()
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
is Patch -> FlashFragment.patch(file, action.fileUri, id)
|
|
||||||
is Flash -> FlashFragment.flash(file, action is Secondary, id)
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Module.onFinish(id: Int) = when (action) {
|
private fun Module.onFinish(id: Int) = when (action) {
|
||||||
is Flash -> FlashFragment.install(file, id)
|
Flash -> {
|
||||||
|
UiThreadHandler.run {
|
||||||
|
(ForegroundTracker.foreground as? BaseUIActivity<*, *>)
|
||||||
|
?.navigation?.navigate(FlashFragment.install(file, id))
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,22 +45,13 @@ open class DownloadService : BaseDownloader() {
|
|||||||
|
|
||||||
override fun Notification.Builder.setIntent(subject: Subject)
|
override fun Notification.Builder.setIntent(subject: Subject)
|
||||||
= when (subject) {
|
= when (subject) {
|
||||||
is Magisk -> setIntent(subject)
|
|
||||||
is Module -> setIntent(subject)
|
is Module -> setIntent(subject)
|
||||||
is Manager -> setIntent(subject)
|
is Manager -> setIntent(subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Magisk)
|
|
||||||
= when (val action = subject.action) {
|
|
||||||
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
|
|
||||||
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, action is Secondary))
|
|
||||||
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, action.fileUri))
|
|
||||||
else -> setContentIntent(Intent())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Module)
|
private fun Notification.Builder.setIntent(subject: Module)
|
||||||
= when (subject.action) {
|
= when (subject.action) {
|
||||||
is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
|
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
|
||||||
else -> setContentIntent(Intent())
|
else -> setContentIntent(Intent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,22 @@ package com.topjohnwu.magisk.core.download
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.DynAPK
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
import com.topjohnwu.magisk.ktx.relaunchApp
|
||||||
|
import com.topjohnwu.magisk.ktx.withStreams
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
|
||||||
private fun Context.patch(apk: File) {
|
private fun Context.patch(apk: File) {
|
||||||
val patched = File(apk.parent, "patched.apk")
|
val patched = File(apk.parent, "patched.apk")
|
||||||
HideAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
|
||||||
apk.delete()
|
apk.delete()
|
||||||
patched.renameTo(apk)
|
patched.renameTo(apk)
|
||||||
}
|
}
|
||||||
@@ -22,30 +25,51 @@ private fun Context.patch(apk: File) {
|
|||||||
private fun BaseDownloader.notifyHide(id: Int) {
|
private fun BaseDownloader.notifyHide(id: Int) {
|
||||||
update(id) {
|
update(id) {
|
||||||
it.setProgress(0, 0, true)
|
it.setProgress(0, 0, true)
|
||||||
.setContentTitle(getString(R.string.hide_manager_title))
|
.setContentTitle(getString(R.string.hide_app_title))
|
||||||
.setContentText("")
|
.setContentText("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) {
|
private class DupOutputStream(
|
||||||
|
private val o1: OutputStream,
|
||||||
|
private val o2: OutputStream
|
||||||
|
) : OutputStream() {
|
||||||
|
override fun write(b: Int) {
|
||||||
|
o1.write(b)
|
||||||
|
o2.write(b)
|
||||||
|
}
|
||||||
|
override fun write(b: ByteArray?, off: Int, len: Int) {
|
||||||
|
o1.write(b, off, len)
|
||||||
|
o2.write(b, off, len)
|
||||||
|
}
|
||||||
|
override fun close() {
|
||||||
|
o1.close()
|
||||||
|
o2.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager, stream: InputStream) {
|
||||||
|
fun write(output: OutputStream) {
|
||||||
|
val ext = subject.externalFile.outputStream()
|
||||||
|
val o = DupOutputStream(ext, output)
|
||||||
|
withStreams(stream, o) { src, out -> src.copyTo(out) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunningAsStub) {
|
||||||
val apk = subject.file.toFile()
|
val apk = subject.file.toFile()
|
||||||
val id = subject.notifyID()
|
val id = subject.notifyID()
|
||||||
if (isRunningAsStub) {
|
write(DynAPK.update(this).outputStream())
|
||||||
// Move to upgrade location
|
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||||
apk.copyTo(DynAPK.update(this), overwrite = true)
|
|
||||||
apk.delete()
|
|
||||||
if (Info.stubChk.version < subject.stub.versionCode) {
|
|
||||||
notifyHide(id)
|
|
||||||
// Also upgrade stub
|
// Also upgrade stub
|
||||||
service.fetchFile(subject.stub.link).byteStream().use { it.writeTo(apk) }
|
notifyHide(id)
|
||||||
|
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||||
patch(apk)
|
patch(apk)
|
||||||
} else {
|
} else {
|
||||||
// Simply relaunch the app
|
// Simply relaunch the app
|
||||||
stopSelf()
|
stopSelf()
|
||||||
relaunchApp(this)
|
relaunchApp(this)
|
||||||
}
|
}
|
||||||
} else if (packageName != BuildConfig.APPLICATION_ID) {
|
} else {
|
||||||
notifyHide(id)
|
write(subject.file.outputStream())
|
||||||
patch(apk)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
import com.topjohnwu.magisk.ktx.forEach
|
||||||
|
import com.topjohnwu.magisk.ktx.withStreams
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
@@ -25,8 +26,7 @@ fun InputStream.toModule(file: Uri, installer: InputStream) {
|
|||||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||||
|
|
||||||
var off = -1
|
var off = -1
|
||||||
var entry: ZipEntry? = zin.nextEntry
|
zin.forEach { entry ->
|
||||||
while (entry != null) {
|
|
||||||
if (off < 0) {
|
if (off < 0) {
|
||||||
off = entry.name.indexOf('/') + 1
|
off = entry.name.indexOf('/') + 1
|
||||||
}
|
}
|
||||||
@@ -38,8 +38,6 @@ fun InputStream.toModule(file: Uri, installer: InputStream) {
|
|||||||
zin.copyTo(zout)
|
zin.copyTo(zout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entry = zin.nextEntry
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import android.os.Parcelable
|
|||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
|
||||||
import com.topjohnwu.magisk.core.model.StubJson
|
import com.topjohnwu.magisk.core.model.StubJson
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import kotlinx.android.parcel.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
|
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
|
||||||
|
|
||||||
@@ -40,71 +39,26 @@ sealed class Subject : Parcelable {
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Manager(
|
class Manager(
|
||||||
private val app: ManagerJson = Info.remote.app,
|
private val json: MagiskJson = Info.remote.magisk,
|
||||||
val stub: StubJson = Info.remote.stub
|
val stub: StubJson = Info.remote.stub
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val action get() = Action.Download
|
override val action get() = Action.Download
|
||||||
override val title: String get() = "MagiskManager-${app.version}(${app.versionCode})"
|
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
|
||||||
override val url: String get() = app.link
|
override val url: String get() = json.link
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
cachedFile("manager.apk")
|
cachedFile("manager.apk")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Magisk : Subject() {
|
sealed class Action : Parcelable {
|
||||||
|
@Parcelize
|
||||||
val magisk: MagiskJson = Info.remote.magisk
|
object Flash : Action()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
private class Internal(
|
object Download : Action()
|
||||||
override val action: Action
|
|
||||||
) : Magisk() {
|
|
||||||
override val url: String get() = magisk.link
|
|
||||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode})"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
cachedFile("magisk.zip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private class Uninstall : Magisk() {
|
|
||||||
override val action get() = Action.Uninstall
|
|
||||||
override val url: String get() = Info.remote.uninstaller.link
|
|
||||||
override val title: String get() = "uninstall.zip"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
cachedFile(title)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
private class Download : Magisk() {
|
|
||||||
override val action get() = Action.Download
|
|
||||||
override val url: String get() = magisk.link
|
|
||||||
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
|
|
||||||
|
|
||||||
@IgnoredOnParcel
|
|
||||||
override val file by lazy {
|
|
||||||
MediaStoreUtils.getFile(title).uri
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
operator fun invoke(config: Action) = when (config) {
|
|
||||||
Action.Download -> Download()
|
|
||||||
Action.Uninstall -> Uninstall()
|
|
||||||
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
|
|
||||||
else -> throw IllegalArgumentException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,6 @@ class PolicyDao(
|
|||||||
}
|
}
|
||||||
}.commit()
|
}.commit()
|
||||||
|
|
||||||
suspend fun delete(packageName: String) = buildQuery<Delete> {
|
|
||||||
condition {
|
|
||||||
equals("package_name", packageName)
|
|
||||||
}
|
|
||||||
}.commit()
|
|
||||||
|
|
||||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
||||||
condition {
|
condition {
|
||||||
equals("uid", uid)
|
equals("uid", uid)
|
||||||
|
|||||||
@@ -2,33 +2,17 @@ package com.topjohnwu.magisk.core.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val app: ManagerJson = ManagerJson(),
|
|
||||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
|
||||||
val magisk: MagiskJson = MagiskJson(),
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
val stub: StubJson = StubJson()
|
val stub: StubJson = StubJson()
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class UninstallerJson(
|
|
||||||
val link: String = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
|
||||||
data class MagiskJson(
|
|
||||||
val version: String = "",
|
|
||||||
val versionCode: Int = -1,
|
|
||||||
val link: String = "",
|
|
||||||
val note: String = "",
|
|
||||||
val md5: String = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ManagerJson(
|
data class MagiskJson(
|
||||||
val version: String = "",
|
val version: String = "",
|
||||||
val versionCode: Int = -1,
|
val versionCode: Int = -1,
|
||||||
val link: String = "",
|
val link: String = "",
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ class LocalModule(path: String) : Module() {
|
|||||||
val dir = "$PERSIST/$id"
|
val dir = "$PERSIST/$id"
|
||||||
if (enable) {
|
if (enable) {
|
||||||
disableFile.delete()
|
disableFile.delete()
|
||||||
if (Const.Version.isCanary())
|
if (Const.Version.atLeast_21_2())
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
Shell.su("copy_sepolicy_rules").submit()
|
||||||
else
|
else
|
||||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||||
} else {
|
} else {
|
||||||
!disableFile.createNewFile()
|
!disableFile.createNewFile()
|
||||||
if (Const.Version.isCanary())
|
if (Const.Version.atLeast_21_2())
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
Shell.su("copy_sepolicy_rules").submit()
|
||||||
else
|
else
|
||||||
Shell.su("rm -rf $dir").submit()
|
Shell.su("rm -rf $dir").submit()
|
||||||
@@ -45,13 +45,13 @@ class LocalModule(path: String) : Module() {
|
|||||||
set(remove) {
|
set(remove) {
|
||||||
if (remove) {
|
if (remove) {
|
||||||
removeFile.createNewFile()
|
removeFile.createNewFile()
|
||||||
if (Const.Version.isCanary())
|
if (Const.Version.atLeast_21_2())
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
Shell.su("copy_sepolicy_rules").submit()
|
||||||
else
|
else
|
||||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||||
} else {
|
} else {
|
||||||
!removeFile.delete()
|
!removeFile.delete()
|
||||||
if (Const.Version.isCanary())
|
if (Const.Version.atLeast_21_2())
|
||||||
Shell.su("copy_sepolicy_rules").submit()
|
Shell.su("copy_sepolicy_rules").submit()
|
||||||
else
|
else
|
||||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.topjohnwu.magisk.core.model.ModuleJson
|
|||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.magisk.ktx.legalFilename
|
import com.topjohnwu.magisk.ktx.legalFilename
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
@file:SuppressLint("InlinedApi")
|
||||||
|
|
||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
import com.topjohnwu.magisk.ktx.getLabel
|
||||||
|
|
||||||
|
|
||||||
data class SuPolicy(
|
data class SuPolicy(
|
||||||
var uid: Int,
|
var uid: Int,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
@@ -38,7 +40,7 @@ fun SuPolicy.toMap() = mapOf(
|
|||||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
||||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||||
val packageName = get("package_name").orEmpty()
|
val packageName = get("package_name").orEmpty()
|
||||||
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
|
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||||
|
|
||||||
if (info.uid != uid)
|
if (info.uid != uid)
|
||||||
throw PackageManager.NameNotFoundException()
|
throw PackageManager.NameNotFoundException()
|
||||||
@@ -59,7 +61,7 @@ fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
|||||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
||||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||||
?: throw PackageManager.NameNotFoundException()
|
?: throw PackageManager.NameNotFoundException()
|
||||||
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
|
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||||
return SuPolicy(
|
return SuPolicy(
|
||||||
uid = info.uid,
|
uid = info.uid,
|
||||||
packageName = pkg,
|
packageName = pkg,
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ package com.topjohnwu.magisk.core.tasks
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
@@ -25,61 +24,50 @@ open class FlashZip(
|
|||||||
private val logs: MutableList<String>
|
private val logs: MutableList<String>
|
||||||
): KoinComponent {
|
): KoinComponent {
|
||||||
|
|
||||||
val context: Context by inject()
|
private val context: Context by inject()
|
||||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
private val installDir = File(context.cacheDir, "flash")
|
||||||
if (!exists()) mkdirs()
|
private lateinit var zipFile: File
|
||||||
}
|
|
||||||
private val tmpFile: File = File(installFolder, "install.zip")
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun unzipAndCheck(): Boolean {
|
|
||||||
val parentFile = tmpFile.parentFile ?: return false
|
|
||||||
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
|
|
||||||
|
|
||||||
val updaterScript = File(parentFile, "updater-script")
|
|
||||||
return Shell
|
|
||||||
.su("grep -q '#MAGISK' $updaterScript")
|
|
||||||
.exec()
|
|
||||||
.isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun flash(): Boolean {
|
private fun flash(): Boolean {
|
||||||
|
installDir.deleteRecursively()
|
||||||
|
installDir.mkdirs()
|
||||||
|
|
||||||
|
zipFile = if (mUri.scheme == "file") {
|
||||||
|
mUri.toFile()
|
||||||
|
} else {
|
||||||
|
File(installDir, "install.zip").also {
|
||||||
console.add("- Copying zip to temp directory")
|
console.add("- Copying zip to temp directory")
|
||||||
|
try {
|
||||||
runCatching {
|
mUri.inputStream().writeTo(it)
|
||||||
mUri.inputStream().writeTo(tmpFile)
|
} catch (e: IOException) {
|
||||||
}.getOrElse {
|
when (e) {
|
||||||
when (it) {
|
|
||||||
is FileNotFoundException -> console.add("! Invalid Uri")
|
is FileNotFoundException -> console.add("! Invalid Uri")
|
||||||
is IOException -> console.add("! Cannot copy to cache")
|
else -> console.add("! Cannot copy to cache")
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isMagiskModule = runCatching {
|
val isValid = runCatching {
|
||||||
unzipAndCheck()
|
zipFile.unzip(installDir, "META-INF/com/google/android", true)
|
||||||
|
val script = File(installDir, "updater-script")
|
||||||
|
script.readText().contains("#MAGISK")
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
console.add("! Unzip error")
|
console.add("! Unzip error")
|
||||||
throw it
|
throw it
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMagiskModule) {
|
if (!isValid) {
|
||||||
console.add("! This zip is not a Magisk Module!")
|
console.add("! This zip is not a Magisk module!")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.add("- Installing ${mUri.displayName}")
|
console.add("- Installing ${mUri.displayName}")
|
||||||
|
|
||||||
val parentFile = tmpFile.parent ?: return false
|
return Shell.su("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||||
|
.to(console, logs).exec().isSuccess
|
||||||
return Shell
|
|
||||||
.su(
|
|
||||||
"cd $parentFile",
|
|
||||||
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
|
|
||||||
)
|
|
||||||
.to(console, logs)
|
|
||||||
.exec().isSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun exec() = withContext(Dispatchers.IO) {
|
open suspend fun exec() = withContext(Dispatchers.IO) {
|
||||||
@@ -94,25 +82,7 @@ open class FlashZip(
|
|||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
false
|
false
|
||||||
} finally {
|
} finally {
|
||||||
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}").submit()
|
Shell.su("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Uninstall(
|
|
||||||
uri: Uri,
|
|
||||||
console: MutableList<String>,
|
|
||||||
log: MutableList<String>
|
|
||||||
) : FlashZip(uri, console, log) {
|
|
||||||
|
|
||||||
override suspend fun exec(): Boolean {
|
|
||||||
val success = super.exec()
|
|
||||||
if (success) {
|
|
||||||
UiThreadHandler.handler.postDelayed(3000) {
|
|
||||||
Shell.su("pm uninstall " + context.packageName).exec()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return success
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.Activity
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build.VERSION.SDK_INT
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.DynAPK
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.*
|
import com.topjohnwu.magisk.core.Config
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.Provider
|
||||||
import com.topjohnwu.magisk.core.utils.AXML
|
import com.topjohnwu.magisk.core.utils.AXML
|
||||||
import com.topjohnwu.magisk.core.utils.Keygen
|
import com.topjohnwu.magisk.core.utils.Keygen
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.inject
|
import com.topjohnwu.magisk.ktx.inject
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.signing.JarMap
|
import com.topjohnwu.signing.JarMap
|
||||||
import com.topjohnwu.signing.SignApk
|
import com.topjohnwu.signing.SignApk
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
object HideAPK {
|
object HideAPK {
|
||||||
|
|
||||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||||
private const val ALPHADOTS = "$ALPHA....."
|
private const val ALPHADOTS = "$ALPHA....."
|
||||||
private const val APP_NAME = "Magisk Manager"
|
private const val APP_NAME = "Magisk"
|
||||||
|
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||||
|
|
||||||
// Some arbitrary limit
|
// Some arbitrary limit
|
||||||
const val MAX_LABEL_LENGTH = 32
|
const val MAX_LABEL_LENGTH = 32
|
||||||
@@ -48,7 +52,7 @@ object HideAPK {
|
|||||||
var next: Char
|
var next: Char
|
||||||
var prev = 0.toChar()
|
var prev = 0.toChar()
|
||||||
for (i in 0 until len) {
|
for (i in 0 until len) {
|
||||||
next = if (prev == '.' || prev == 0.toChar() || i == len - 1) {
|
next = if (prev == '.' || i == 0 || i == len - 1) {
|
||||||
ALPHA[random.nextInt(ALPHA.length)]
|
ALPHA[random.nextInt(ALPHA.length)]
|
||||||
} else {
|
} else {
|
||||||
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
|
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
|
||||||
@@ -58,20 +62,20 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
if (!builder.contains('.')) {
|
if (!builder.contains('.')) {
|
||||||
// Pick a random index and set it as dot
|
// Pick a random index and set it as dot
|
||||||
val idx = random.nextInt(len - 1)
|
val idx = random.nextInt(len - 2)
|
||||||
builder[idx] = '.'
|
builder[idx + 1] = '.'
|
||||||
}
|
}
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun patch(
|
fun patch(
|
||||||
context: Context,
|
context: Context,
|
||||||
apk: String, out: String,
|
apk: File, out: File,
|
||||||
pkg: String, label: CharSequence
|
pkg: String, label: CharSequence
|
||||||
): Boolean {
|
): Boolean {
|
||||||
try {
|
try {
|
||||||
val jar = JarMap.open(apk)
|
val jar = JarMap.open(apk, true)
|
||||||
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||||
val xml = AXML(jar.getRawData(je))
|
val xml = AXML(jar.getRawData(je))
|
||||||
|
|
||||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
|
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
|
||||||
@@ -89,102 +93,76 @@ object HideAPK {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun patchAndHide(context: Context, label: String): Boolean {
|
private class WaitPackageReceiver(
|
||||||
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
|
private val pkg: String,
|
||||||
val src = if (dlStub) {
|
activity: Activity
|
||||||
val stub = File(context.cacheDir, "stub.apk")
|
) : BroadcastReceiver() {
|
||||||
try {
|
|
||||||
svc.fetchFile(Info.remote.stub.link).byteStream().use {
|
private val activity = WeakReference(activity)
|
||||||
it.writeTo(stub)
|
|
||||||
|
private fun launchApp(): Unit = activity.get()?.run {
|
||||||
|
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return
|
||||||
|
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
||||||
|
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
intent.putExtra(Const.Key.PREV_PKG, packageName)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
} ?: Unit
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.action ?: return) {
|
||||||
|
Intent.ACTION_PACKAGE_REPLACED, Intent.ACTION_PACKAGE_ADDED -> {
|
||||||
|
val newPkg = intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||||
|
if (newPkg == pkg) {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
launchApp()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
|
||||||
|
val stub = File(activity.cacheDir, "stub.apk")
|
||||||
|
try {
|
||||||
|
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
stub.path
|
|
||||||
} else {
|
|
||||||
context.packageCodePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new random package name and signature
|
// Generate a new random package name and signature
|
||||||
val repack = File(context.cacheDir, "patched.apk")
|
val repack = File(activity.cacheDir, "patched.apk")
|
||||||
val pkg = genPackageName()
|
val pkg = genPackageName()
|
||||||
Config.keyStoreRaw = ""
|
Config.keyStoreRaw = ""
|
||||||
|
|
||||||
if (!patch(context, src, repack.path, pkg, label))
|
if (!patch(activity, stub, repack, pkg, label))
|
||||||
return false
|
return false
|
||||||
|
|
||||||
// Install the application
|
// Install and auto launch app
|
||||||
|
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(pkg, activity))
|
||||||
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
|
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
|
||||||
return false
|
APKInstall.installHideResult(activity, repack)
|
||||||
|
|
||||||
context.apply {
|
|
||||||
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return false
|
|
||||||
Config.suManager = pkg
|
|
||||||
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
suspend fun hide(activity: Activity, label: String) {
|
||||||
fun hide(context: Context, label: String) {
|
|
||||||
val dialog = ProgressDialog.show(context, context.getString(R.string.hide_manager_title), "", true)
|
|
||||||
GlobalScope.launch {
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
patchAndHide(context, label)
|
patchAndHide(activity, label)
|
||||||
}
|
}
|
||||||
if (!result) {
|
if (!result) {
|
||||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
|
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun downloadAndRestore(context: Context): Boolean {
|
fun restore(activity: Activity) {
|
||||||
val apk = if (isRunningAsStub) {
|
val apk = DynAPK.current(activity)
|
||||||
DynAPK.current(context)
|
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(APPLICATION_ID, activity))
|
||||||
} else {
|
Shell.su("adb_pm_install $apk").submit {
|
||||||
File(context.cacheDir, "manager.apk").also { apk ->
|
if (!it.isSuccess)
|
||||||
try {
|
APKInstall.installHideResult(activity, apk)
|
||||||
svc.fetchFile(Info.remote.app.link).byteStream().use {
|
|
||||||
it.writeTo(apk)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Timber.e(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Shell.su("adb_pm_install $apk").exec().isSuccess)
|
|
||||||
return false
|
|
||||||
|
|
||||||
context.apply {
|
|
||||||
val intent = packageManager.getLaunchIntentForPackage(APPLICATION_ID) ?: return false
|
|
||||||
Config.suManager = ""
|
|
||||||
grantUriPermission(APPLICATION_ID, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
grantUriPermission(APPLICATION_ID, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
intent.putExtra(Const.Key.HIDDEN_PKG, packageName)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun restore(context: Context) {
|
|
||||||
val dialog = ProgressDialog.show(context, context.getString(R.string.restore_img_msg), "", true)
|
|
||||||
GlobalScope.launch {
|
|
||||||
val result = withContext(Dispatchers.IO) {
|
|
||||||
downloadAndRestore(context)
|
|
||||||
}
|
|
||||||
if (!result) {
|
|
||||||
Utils.toast(R.string.restore_manager_fail_toast, Toast.LENGTH_LONG)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
package com.topjohnwu.magisk.core.tasks
|
package com.topjohnwu.magisk.core.tasks
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
|
import com.topjohnwu.magisk.DynAPK
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.*
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.di.Protected
|
import com.topjohnwu.magisk.di.Protected
|
||||||
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
import com.topjohnwu.magisk.ktx.reboot
|
||||||
|
import com.topjohnwu.magisk.ktx.symlink
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
import com.topjohnwu.magisk.ktx.withStreams
|
||||||
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.signing.SignBoot
|
import com.topjohnwu.signing.SignBoot
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@@ -35,160 +35,171 @@ import org.kamranzafar.jtar.TarHeader
|
|||||||
import org.kamranzafar.jtar.TarInputStream
|
import org.kamranzafar.jtar.TarInputStream
|
||||||
import org.kamranzafar.jtar.TarOutputStream
|
import org.kamranzafar.jtar.TarOutputStream
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
import org.koin.core.get
|
|
||||||
import org.koin.core.inject
|
import org.koin.core.inject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.zip.ZipEntry
|
import java.security.SecureRandom
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.*
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
abstract class MagiskInstallImpl : KoinComponent {
|
abstract class MagiskInstallImpl protected constructor(
|
||||||
|
protected val console: MutableList<String> = NOPList.getInstance(),
|
||||||
|
private val logs: MutableList<String> = NOPList.getInstance()
|
||||||
|
) : KoinComponent {
|
||||||
|
|
||||||
protected lateinit var installDir: File
|
protected var installDir = File("xxx")
|
||||||
private lateinit var srcBoot: String
|
private lateinit var srcBoot: File
|
||||||
private lateinit var zipUri: Uri
|
|
||||||
|
|
||||||
protected val console: MutableList<String>
|
|
||||||
private val logs: MutableList<String>
|
|
||||||
private var tarOut: TarOutputStream? = null
|
|
||||||
|
|
||||||
|
private val shell = Shell.getShell()
|
||||||
private val service: NetworkService by inject()
|
private val service: NetworkService by inject()
|
||||||
protected val context: Context by inject()
|
protected val context: Context by inject(Protected)
|
||||||
|
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||||
protected constructor() {
|
|
||||||
console = NOPList.getInstance()
|
|
||||||
logs = NOPList.getInstance()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
|
|
||||||
console = out
|
|
||||||
logs = err
|
|
||||||
zipUri = zip
|
|
||||||
installDir = File(get<Context>(Protected).filesDir.parent, "install")
|
|
||||||
"rm -rf $installDir".sh()
|
|
||||||
installDir.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findImage(): Boolean {
|
private fun findImage(): Boolean {
|
||||||
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||||
if (srcBoot.isEmpty()) {
|
if (bootPath.isEmpty()) {
|
||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
console.add("- Target image: $srcBoot")
|
srcBoot = SuFile(bootPath)
|
||||||
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findSecondaryImage(): Boolean {
|
private fun findSecondary(): Boolean {
|
||||||
val slot = "echo \$SLOT".fsh()
|
val slot = "echo \$SLOT".fsh()
|
||||||
val target = if (slot == "_a") "_b" else "_a"
|
val target = if (slot == "_a") "_b" else "_a"
|
||||||
console.add("- Target slot: $target")
|
console.add("- Target slot: $target")
|
||||||
srcBoot = arrayOf(
|
val bootPath = arrayOf(
|
||||||
"SLOT=$target",
|
"SLOT=$target",
|
||||||
"find_boot_image",
|
"find_boot_image",
|
||||||
"SLOT=$slot",
|
"SLOT=$slot",
|
||||||
"echo \"\$BOOTIMAGE\"").fsh()
|
"echo \"\$BOOTIMAGE\"").fsh()
|
||||||
if (srcBoot.isEmpty()) {
|
if (bootPath.isEmpty()) {
|
||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
console.add("- Target image: $srcBoot")
|
srcBoot = SuFile(bootPath)
|
||||||
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
private fun extractFiles(): Boolean {
|
||||||
private fun extractZip(): Boolean {
|
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||||
val arch = if (Build.VERSION.SDK_INT >= 21) {
|
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||||
val abis = listOf(*Build.SUPPORTED_ABIS)
|
|
||||||
if (abis.contains("x86")) "x86" else "arm"
|
|
||||||
} else {
|
|
||||||
if (Build.CPU_ABI == "x86") "x86" else "arm"
|
|
||||||
}
|
|
||||||
|
|
||||||
console.add("- Device platform: " + Build.CPU_ABI)
|
installDir = File(context.filesDir.parent, "install")
|
||||||
|
installDir.deleteRecursively()
|
||||||
|
installDir.mkdirs()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
|
// Extract binaries
|
||||||
lateinit var ze: ZipEntry
|
if (isRunningAsStub) {
|
||||||
while (zi.nextEntry?.let { ze = it } != null) {
|
val zf = ZipFile(DynAPK.current(context))
|
||||||
if (ze.isDirectory)
|
zf.entries().asSequence().filter {
|
||||||
continue
|
!it.isDirectory && it.name.startsWith("lib/${Const.CPU_ABI_32}/")
|
||||||
var name: String? = null
|
}.forEach {
|
||||||
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
|
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
|
||||||
for (n in names) {
|
val name = n.substring(3, n.length - 3)
|
||||||
ze.name.run {
|
val dest = File(installDir, name)
|
||||||
if (startsWith(n)) {
|
zf.getInputStream(it).writeTo(dest)
|
||||||
name = substring(lastIndexOf('/') + 1)
|
}
|
||||||
|
} else {
|
||||||
|
val libs = Const.NATIVE_LIB_DIR.listFiles { _, name ->
|
||||||
|
name.startsWith("lib") && name.endsWith(".so")
|
||||||
|
} ?: emptyArray()
|
||||||
|
for (lib in libs) {
|
||||||
|
val name = lib.name.substring(3, lib.name.length - 3)
|
||||||
|
symlink(lib.path, "$installDir/$name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
name ?: continue
|
|
||||||
break
|
// Extract scripts
|
||||||
|
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
|
||||||
|
val dest = File(installDir, script)
|
||||||
|
context.assets.open(script).writeTo(dest)
|
||||||
}
|
}
|
||||||
if (name == null && ze.name.startsWith("chromeos/"))
|
// Extract chromeos tools
|
||||||
name = ze.name
|
File(installDir, "chromeos").mkdir()
|
||||||
name?.also {
|
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
|
||||||
val dest = if (installDir is SuFile)
|
val name = "chromeos/$file"
|
||||||
SuFile(installDir, it)
|
val dest = File(installDir, name)
|
||||||
else
|
context.assets.open(name).writeTo(dest)
|
||||||
File(installDir, it)
|
|
||||||
dest.parentFile!!.mkdirs()
|
|
||||||
SuFileOutputStream(dest).use { s -> zi.copyTo(s) }
|
|
||||||
} ?: continue
|
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
} catch (e: IOException) {
|
console.add("! Unable to extract files")
|
||||||
console.add("! Cannot unzip zip")
|
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val init64 = SuFile.open(installDir, "magiskinit64")
|
if (useRootDir) {
|
||||||
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
|
// Move everything to tmpfs to workaround Samsung bullshit
|
||||||
init64.renameTo(SuFile.open(installDir, "magiskinit"))
|
SuFile(Const.TMPDIR).also {
|
||||||
} else {
|
arrayOf(
|
||||||
init64.delete()
|
"rm -rf $it",
|
||||||
|
"mkdir -p $it",
|
||||||
|
"cp_readlink $installDir $it",
|
||||||
|
"rm -rf $installDir"
|
||||||
|
).sh()
|
||||||
|
installDir = it
|
||||||
}
|
}
|
||||||
"cd $installDir; chmod 755 *".sh()
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newEntry(name: String, size: Long): TarEntry {
|
// Optimization for SuFile I/O streams to skip an internal trial and error
|
||||||
|
private fun installDirFile(name: String): File {
|
||||||
|
return if (useRootDir)
|
||||||
|
SuFile(installDir, name)
|
||||||
|
else
|
||||||
|
File(installDir, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
||||||
|
src.copyTo(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newTarEntry(name: String, size: Long): TarEntry {
|
||||||
console.add("-- Writing: $name")
|
console.add("-- Writing: $name")
|
||||||
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun handleTar(input: InputStream, output: OutputStream): OutputStream {
|
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
|
||||||
console.add("- Processing tar file")
|
console.add("- Processing tar file")
|
||||||
val tarOut = TarOutputStream(output)
|
val tarOut = TarOutputStream(output)
|
||||||
TarInputStream(input).use { tarIn ->
|
TarInputStream(input).use { tarIn ->
|
||||||
lateinit var entry: TarEntry
|
lateinit var entry: TarEntry
|
||||||
|
|
||||||
fun decompressedStream() =
|
fun decompressedStream(): InputStream {
|
||||||
if (entry.name.contains(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
|
||||||
|
return object : FilterInputStream(src) {
|
||||||
|
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
|
||||||
|
override fun close() { /* Never close src stream */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||||
if (entry.name.contains("boot.img") ||
|
if (entry.name.startsWith("boot.img") ||
|
||||||
(Config.recovery && entry.name.contains("recovery.img"))) {
|
(Config.recovery && entry.name.contains("recovery.img"))) {
|
||||||
val name = entry.name.replace(".lz4", "")
|
val name = entry.name.replace(".lz4", "")
|
||||||
console.add("-- Extracting: $name")
|
console.add("-- Extracting: $name")
|
||||||
|
|
||||||
val extract = File(installDir, name)
|
val extract = installDirFile(name)
|
||||||
FileOutputStream(extract).use { decompressedStream().copyTo(it) }
|
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
||||||
} else if (entry.name.contains("vbmeta.img")) {
|
} else if (entry.name.contains("vbmeta.img")) {
|
||||||
val rawData = ByteArrayOutputStream().let {
|
val rawData = decompressedStream().readBytes()
|
||||||
decompressedStream().copyTo(it)
|
|
||||||
it.toByteArray()
|
|
||||||
}
|
|
||||||
// Valid vbmeta.img should be at least 256 bytes
|
// Valid vbmeta.img should be at least 256 bytes
|
||||||
if (rawData.size < 256)
|
if (rawData.size < 256)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||||
|
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||||
console.add("-- Patching: vbmeta.img")
|
console.add("-- Patching: vbmeta.img")
|
||||||
ByteBuffer.wrap(rawData).putInt(120, 2)
|
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||||
tarOut.putNextEntry(newEntry("vbmeta.img", rawData.size.toLong()))
|
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
|
||||||
tarOut.write(rawData)
|
tarOut.write(rawData)
|
||||||
} else {
|
} else {
|
||||||
console.add("-- Copying: ${entry.name}")
|
console.add("-- Copying: ${entry.name}")
|
||||||
@@ -196,19 +207,23 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val boot = SuFile.open(installDir, "boot.img")
|
}
|
||||||
val recovery = SuFile.open(installDir, "recovery.img")
|
val boot = installDirFile("boot.img")
|
||||||
if (recovery.exists() && boot.exists()) {
|
val recovery = installDirFile("recovery.img")
|
||||||
// Install Magisk to recovery
|
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||||
srcBoot = recovery.path
|
// Install to recovery
|
||||||
// Repack boot image to prevent restore
|
srcBoot = recovery
|
||||||
|
// Repack boot image to prevent auto restore
|
||||||
arrayOf(
|
arrayOf(
|
||||||
|
"cd $installDir",
|
||||||
"./magiskboot unpack boot.img",
|
"./magiskboot unpack boot.img",
|
||||||
"./magiskboot repack boot.img",
|
"./magiskboot repack boot.img",
|
||||||
|
"cat new-boot.img > boot.img",
|
||||||
"./magiskboot cleanup",
|
"./magiskboot cleanup",
|
||||||
"mv new-boot.img boot.img").sh()
|
"rm -f new-boot.img",
|
||||||
SuFileInputStream(boot).use {
|
"cd /").sh()
|
||||||
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
|
SuFileInputStream.open(boot).use {
|
||||||
|
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
||||||
it.copyTo(tarOut)
|
it.copyTo(tarOut)
|
||||||
}
|
}
|
||||||
boot.delete()
|
boot.delete()
|
||||||
@@ -217,15 +232,14 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
console.add("! No boot image found")
|
console.add("! No boot image found")
|
||||||
throw IOException()
|
throw IOException()
|
||||||
}
|
}
|
||||||
srcBoot = boot.path
|
srcBoot = boot
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return tarOut
|
return tarOut
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFile(uri: Uri): Boolean {
|
private fun handleFile(uri: Uri): Boolean {
|
||||||
val outStream: OutputStream
|
val outStream: OutputStream
|
||||||
val outFile: MediaStoreUtils.UriFile
|
var outFile: MediaStoreUtils.UriFile? = null
|
||||||
|
|
||||||
// Process input file
|
// Process input file
|
||||||
try {
|
try {
|
||||||
@@ -237,37 +251,52 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
src.reset()
|
src.reset()
|
||||||
|
|
||||||
|
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789"
|
||||||
|
val random = SecureRandom()
|
||||||
|
val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run {
|
||||||
|
for (i in 1..5) {
|
||||||
|
append(alphaNum[random.nextInt(alphaNum.length)])
|
||||||
|
}
|
||||||
|
toString()
|
||||||
|
}
|
||||||
|
|
||||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
||||||
outFile = MediaStoreUtils.getFile("magisk_patched.tar")
|
// tar file
|
||||||
handleTar(src, outFile.uri.outputStream())
|
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||||
|
processTar(src, outFile!!.uri.outputStream())
|
||||||
} else {
|
} else {
|
||||||
// Raw image
|
// raw image
|
||||||
srcBoot = File(installDir, "boot.img").path
|
srcBoot = installDirFile("boot.img")
|
||||||
console.add("- Copying image to cache")
|
console.add("- Copying image to cache")
|
||||||
FileOutputStream(srcBoot).use { src.copyTo(it) }
|
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
||||||
outFile = MediaStoreUtils.getFile("magisk_patched.img")
|
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||||
outFile.uri.outputStream()
|
outFile!!.uri.outputStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Process error")
|
console.add("! Process error")
|
||||||
|
outFile?.delete()
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch file
|
// Patch file
|
||||||
if (!patchBoot())
|
if (!patchBoot()) {
|
||||||
|
outFile!!.delete()
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Output file
|
// Output file
|
||||||
try {
|
try {
|
||||||
val patched = SuFile.open(installDir, "new-boot.img")
|
val newBoot = installDirFile("new-boot.img")
|
||||||
if (outStream is TarOutputStream) {
|
if (outStream is TarOutputStream) {
|
||||||
val name = if (srcBoot.contains("recovery")) "recovery.img" else "boot.img"
|
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
||||||
outStream.putNextEntry(newEntry(name, patched.length()))
|
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||||
}
|
}
|
||||||
withStreams(SuFileInputStream(patched), outStream) { src, out -> src.copyTo(out) }
|
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
||||||
patched.delete()
|
newBoot.delete()
|
||||||
|
|
||||||
console.add("")
|
console.add("")
|
||||||
console.add("****************************")
|
console.add("****************************")
|
||||||
@@ -276,126 +305,138 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
console.add("****************************")
|
console.add("****************************")
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Failed to output to $outFile")
|
console.add("! Failed to output to $outFile")
|
||||||
|
outFile!!.delete()
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix up binaries
|
||||||
|
srcBoot.delete()
|
||||||
|
if (shell.isRoot) {
|
||||||
|
"fix_env $installDir".sh()
|
||||||
|
} else {
|
||||||
|
"cp_readlink $installDir".sh()
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
private fun patchBoot(): Boolean {
|
||||||
var srcNand = ""
|
var isSigned = false
|
||||||
if ("[ -c $srcBoot ] && nanddump -f boot.img $srcBoot".sh().isSuccess) {
|
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
|
||||||
srcNand = srcBoot
|
|
||||||
srcBoot = File(installDir, "boot.img").path
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSigned: Boolean
|
|
||||||
try {
|
try {
|
||||||
SuFileInputStream(srcBoot).use {
|
SuFileInputStream.open(srcBoot).use {
|
||||||
isSigned = SignBoot.verifySignature(it, null)
|
if (SignBoot.verifySignature(it, null)) {
|
||||||
if (isSigned) {
|
isSigned = true
|
||||||
console.add("- Boot image is signed with AVB 1.0")
|
console.add("- Boot image is signed with AVB 1.0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Unable to check signature")
|
console.add("! Unable to check signature")
|
||||||
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!("KEEPFORCEENCRYPT=${Config.keepEnc} KEEPVERITY=${Config.keepVerity} " +
|
val newBoot = installDirFile("new-boot.img")
|
||||||
"RECOVERYMODE=${Config.recovery} sh update-binary " +
|
if (!useRootDir) {
|
||||||
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
|
// Create output files before hand
|
||||||
|
newBoot.createNewFile()
|
||||||
|
File(installDir, "stock_boot.img").createNewFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
val cmds = arrayOf(
|
||||||
|
"cd $installDir",
|
||||||
|
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
|
||||||
|
"KEEPVERITY=${Config.keepVerity} " +
|
||||||
|
"RECOVERYMODE=${Config.recovery} " +
|
||||||
|
"sh boot_patch.sh $srcBoot")
|
||||||
|
|
||||||
|
if (!cmds.sh().isSuccess)
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
|
|
||||||
if (srcNand.isNotEmpty()) {
|
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
|
||||||
srcBoot = srcNand
|
|
||||||
}
|
|
||||||
|
|
||||||
val job = Shell.sh(
|
|
||||||
"./magiskboot cleanup",
|
|
||||||
"mv bin/busybox busybox",
|
|
||||||
"rm -rf magisk.apk bin boot.img update-binary",
|
|
||||||
"cd /")
|
|
||||||
|
|
||||||
val patched = File(installDir, "new-boot.img")
|
|
||||||
if (isSigned) {
|
if (isSigned) {
|
||||||
console.add("- Signing boot image with verity keys")
|
console.add("- Signing boot image with verity keys")
|
||||||
val signed = File(installDir, "signed.img")
|
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||||
try {
|
try {
|
||||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
val src = SuFileInputStream.open(newBoot).buffered()
|
||||||
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
|
val out = signed.outputStream().buffered()
|
||||||
|
withStreams(src, out) { _, _ ->
|
||||||
|
SignBoot.doSignature(null, null, src, out, "/boot")
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Unable to sign image")
|
console.add("! Unable to sign image")
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
job.add("cat $signed > $newBoot", "rm -f $signed")
|
||||||
job.add("mv -f $signed $patched")
|
|
||||||
}
|
}
|
||||||
job.exec()
|
job.exec()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun flashBoot(): Boolean {
|
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
|
||||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
|
||||||
return false
|
|
||||||
"run_migrations".sh()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun postOTA(): Boolean {
|
private suspend fun postOTA(): Boolean {
|
||||||
val bootctl = SuFile("/data/adb/bootctl")
|
|
||||||
try {
|
try {
|
||||||
withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
|
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
|
||||||
it, out -> it.copyTo(out)
|
service.fetchBootctl().byteStream().writeTo(bootctl)
|
||||||
}
|
"post_ota $bootctl".sh()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Unable to download bootctl")
|
console.add("! Unable to download bootctl")
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
"post_ota ${bootctl.parent}".sh()
|
|
||||||
|
|
||||||
console.add("***************************************")
|
console.add("***************************************")
|
||||||
console.add(" Next reboot will boot to second slot!")
|
console.add(" Next reboot will boot to second slot!")
|
||||||
console.add("***************************************")
|
console.add("***************************************")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
|
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
|
||||||
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
|
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
|
||||||
private fun String.fsh() = ShellUtils.fastCmd(this)
|
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
|
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||||
|
|
||||||
protected fun doPatchFile(patchFile: Uri) = extractZip() && handleFile(patchFile)
|
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
|
||||||
|
|
||||||
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||||
|
|
||||||
protected suspend fun secondSlot() =
|
protected suspend fun secondSlot() =
|
||||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
|
||||||
protected fun fixEnv(zip: Uri): Boolean {
|
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||||
installDir = SuFile("/data/adb/magisk")
|
|
||||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
protected fun uninstall() = "run_uninstaller ${AssetHack.apk}".sh().isSuccess
|
||||||
zipUri = zip
|
|
||||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
protected abstract suspend fun operations(): Boolean
|
protected abstract suspend fun operations(): Boolean
|
||||||
|
|
||||||
open suspend fun exec() = withContext(Dispatchers.IO) { operations() }
|
open suspend fun exec(): Boolean {
|
||||||
|
synchronized(Companion) {
|
||||||
|
if (haveActiveSession)
|
||||||
|
return false
|
||||||
|
haveActiveSession = true
|
||||||
|
}
|
||||||
|
val result = withContext(Dispatchers.IO) { operations() }
|
||||||
|
synchronized(Companion) {
|
||||||
|
haveActiveSession = false
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class MagiskInstaller(
|
companion object {
|
||||||
file: Uri,
|
private var haveActiveSession = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MagiskInstaller(
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstallImpl(file, console, logs) {
|
) : MagiskInstallImpl(console, logs) {
|
||||||
|
|
||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
@@ -409,40 +450,57 @@ sealed class MagiskInstaller(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Patch(
|
class Patch(
|
||||||
file: Uri,
|
|
||||||
private val uri: Uri,
|
private val uri: Uri,
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(file, console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = doPatchFile(uri)
|
override suspend fun operations() = doPatchFile(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SecondSlot(
|
class SecondSlot(
|
||||||
file: Uri,
|
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(file, console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = secondSlot()
|
override suspend fun operations() = secondSlot()
|
||||||
}
|
}
|
||||||
|
|
||||||
class Direct(
|
class Direct(
|
||||||
file: Uri,
|
|
||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(file, console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = direct()
|
override suspend fun operations() = direct()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Emulator(
|
||||||
|
console: MutableList<String>,
|
||||||
|
logs: MutableList<String>
|
||||||
|
) : MagiskInstaller(console, logs) {
|
||||||
|
override suspend fun operations() = fixEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
class EnvFixTask(
|
class Uninstall(
|
||||||
private val zip: Uri
|
console: MutableList<String>,
|
||||||
) : MagiskInstallImpl() {
|
logs: MutableList<String>
|
||||||
override suspend fun operations() = fixEnv(zip)
|
) : MagiskInstallImpl(console, logs) {
|
||||||
|
override suspend fun operations() = uninstall()
|
||||||
|
|
||||||
override suspend fun exec(): Boolean {
|
override suspend fun exec(): Boolean {
|
||||||
val success = super.exec()
|
val success = super.exec()
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(EnvFixDialog.DISMISS))
|
if (success) {
|
||||||
|
UiThreadHandler.handler.postDelayed(3000) {
|
||||||
|
Shell.su("pm uninstall ${context.packageName}").exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FixEnv(private val callback: () -> Unit) : MagiskInstallImpl() {
|
||||||
|
override suspend fun operations() = fixEnv()
|
||||||
|
|
||||||
|
override suspend fun exec(): Boolean {
|
||||||
|
val success = super.exec()
|
||||||
|
callback()
|
||||||
Utils.toast(
|
Utils.toast(
|
||||||
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
@@ -452,3 +510,4 @@ class EnvFixTask(
|
|||||||
return success
|
return success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import android.util.Base64OutputStream
|
|||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||||
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||||
|
import com.topjohnwu.signing.KeyData
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
@@ -58,10 +60,10 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
|
|
||||||
class TestProvider : CertKeyProvider {
|
class TestProvider : CertKeyProvider {
|
||||||
override val cert by lazy {
|
override val cert by lazy {
|
||||||
readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem"))
|
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
||||||
}
|
}
|
||||||
override val key by lazy {
|
override val key by lazy {
|
||||||
readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8"))
|
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,16 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.AssetManager
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.AssetHack
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.ResMgr
|
|
||||||
import com.topjohnwu.magisk.core.addAssetPath
|
|
||||||
import com.topjohnwu.magisk.ktx.langTagToLocale
|
import com.topjohnwu.magisk.ktx.langTagToLocale
|
||||||
import com.topjohnwu.magisk.ktx.toLangTag
|
import com.topjohnwu.magisk.ktx.toLangTag
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.Comparator
|
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
var currentLocale: Locale = Locale.getDefault()
|
var currentLocale: Locale = Locale.getDefault()
|
||||||
@@ -30,11 +26,8 @@ suspend fun availableLocales() = cachedLocales ?:
|
|||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val compareId = R.string.app_changelog
|
val compareId = R.string.app_changelog
|
||||||
|
|
||||||
// Create a completely new resource to prevent cross talk over app's configs
|
// Create a completely new resource to prevent cross talk over active configs
|
||||||
val asset = AssetManager::class.java.newInstance().apply { addAssetPath(ResMgr.apk) }
|
val res = AssetHack.newResource()
|
||||||
val config = Configuration(ResMgr.resource.configuration)
|
|
||||||
val metrics = DisplayMetrics().apply { setTo(ResMgr.resource.displayMetrics) }
|
|
||||||
val res = Resources(asset, metrics, config)
|
|
||||||
|
|
||||||
val locales = ArrayList<String>().apply {
|
val locales = ArrayList<String>().apply {
|
||||||
// Add default locale
|
// Add default locale
|
||||||
@@ -45,19 +38,17 @@ withContext(Dispatchers.Default) {
|
|||||||
add("pt-BR")
|
add("pt-BR")
|
||||||
|
|
||||||
// Then add all supported locales
|
// Then add all supported locales
|
||||||
addAll(res.assets.locales)
|
addAll(Resources.getSystem().assets.locales)
|
||||||
}.map {
|
}.map {
|
||||||
it.langTagToLocale()
|
it.langTagToLocale()
|
||||||
}.distinctBy {
|
}.distinctBy {
|
||||||
config.setLocale(it)
|
res.updateLocale(it)
|
||||||
res.updateConfiguration(config, metrics)
|
|
||||||
res.getString(compareId)
|
res.getString(compareId)
|
||||||
}.sortedWith(Comparator { a, b ->
|
}.sortedWith { a, b ->
|
||||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||||
})
|
}
|
||||||
|
|
||||||
config.setLocale(defaultLocale)
|
res.updateLocale(defaultLocale)
|
||||||
res.updateConfiguration(config, metrics)
|
|
||||||
val defName = res.getString(R.string.system_default)
|
val defName = res.getString(R.string.system_default)
|
||||||
|
|
||||||
val names = ArrayList<String>(locales.size + 1)
|
val names = ArrayList<String>(locales.size + 1)
|
||||||
@@ -79,6 +70,11 @@ fun Resources.updateConfig(config: Configuration = configuration) {
|
|||||||
updateConfiguration(config, displayMetrics)
|
updateConfiguration(config, displayMetrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Resources.updateLocale(locale: Locale) {
|
||||||
|
configuration.setLocale(locale)
|
||||||
|
updateConfiguration(configuration, displayMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
fun refreshLocale() {
|
fun refreshLocale() {
|
||||||
val localeConfig = Config.locale
|
val localeConfig = Config.locale
|
||||||
currentLocale = when {
|
currentLocale = when {
|
||||||
@@ -86,5 +82,5 @@ fun refreshLocale() {
|
|||||||
else -> localeConfig.langTagToLocale()
|
else -> localeConfig.langTagToLocale()
|
||||||
}
|
}
|
||||||
Locale.setDefault(currentLocale)
|
Locale.setDefault(currentLocale)
|
||||||
ResMgr.resource.updateConfig()
|
AssetHack.resource.updateConfig()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
@@ -40,15 +39,17 @@ object MediaStoreUtils {
|
|||||||
|
|
||||||
private val relativePath get() = relativePath(Config.downloadDir)
|
private val relativePath get() = relativePath(Config.downloadDir)
|
||||||
|
|
||||||
@RequiresApi(api = 29)
|
@RequiresApi(api = 30)
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun insertFile(displayName: String): MediaStoreFile {
|
private fun insertFile(displayName: String): MediaStoreFile {
|
||||||
val values = ContentValues()
|
val values = ContentValues()
|
||||||
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||||
|
|
||||||
// before Android 11, MediaStore can not rename new file when file exists,
|
// When a file with the same name exists and was not created by us:
|
||||||
// insert will return null. use newFile() instead.
|
// - Before Android 11, insert will return null
|
||||||
|
// - On Android 11+, the system will automatically create a new name
|
||||||
|
// Thus the reason to restrict this method call to API 30+
|
||||||
val fileUri = cr.insert(tableUri, values) ?: throw IOException("Can't insert $displayName.")
|
val fileUri = cr.insert(tableUri, values) ?: throw IOException("Can't insert $displayName.")
|
||||||
|
|
||||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||||
@@ -65,14 +66,8 @@ object MediaStoreUtils {
|
|||||||
throw IOException("Can't insert $displayName.")
|
throw IOException("Can't insert $displayName.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = 29)
|
||||||
private fun queryFile(displayName: String): UriFile? {
|
private fun queryFile(displayName: String): UriFile? {
|
||||||
if (Build.VERSION.SDK_INT < 30) {
|
|
||||||
// Fallback to file based I/O pre Android 11
|
|
||||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
|
||||||
parent.mkdirs()
|
|
||||||
return LegacyUriFile(File(parent, displayName))
|
|
||||||
}
|
|
||||||
|
|
||||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||||
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
|
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
|
||||||
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
|
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
|
||||||
@@ -92,11 +87,17 @@ object MediaStoreUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun getFile(displayName: String): UriFile {
|
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
||||||
return queryFile(displayName) ?:
|
if (Build.VERSION.SDK_INT < 30) {
|
||||||
/* this code path will never happen pre 29 */ insertFile(displayName)
|
// Fallback to file based I/O pre Android 11
|
||||||
|
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||||
|
parent.mkdirs()
|
||||||
|
return LegacyUriFile(File(parent, displayName))
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (skipQuery) insertFile(displayName)
|
||||||
|
else queryFile(displayName) ?: insertFile(displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
|
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.core.wrap
|
|
||||||
import com.topjohnwu.magisk.ktx.rawResource
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
|
||||||
|
|
||||||
class RootInit : Shell.Initializer() {
|
|
||||||
|
|
||||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
|
||||||
return init(context.wrap(), shell)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun init(context: Context, shell: Shell): Boolean {
|
|
||||||
shell.newJob().apply {
|
|
||||||
add("export SDK_INT=${Build.VERSION.SDK_INT}")
|
|
||||||
if (Const.Version.atLeast_20_4()) {
|
|
||||||
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
|
||||||
} else {
|
|
||||||
add("export MAGISKTMP=/sbin/.magisk")
|
|
||||||
}
|
|
||||||
if (Const.Version.atLeast_21_0()) {
|
|
||||||
add("export ASH_STANDALONE=1")
|
|
||||||
add("[ -x /data/adb/magisk/busybox ] && exec /data/adb/magisk/busybox sh")
|
|
||||||
} else {
|
|
||||||
add("export PATH=\"\$MAGISKTMP/busybox:\$PATH\"")
|
|
||||||
}
|
|
||||||
add(context.rawResource(R.raw.manager))
|
|
||||||
if (shell.isRoot) {
|
|
||||||
add(context.rawResource(R.raw.util_functions))
|
|
||||||
}
|
|
||||||
add("mm_init")
|
|
||||||
}.exec()
|
|
||||||
|
|
||||||
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
|
|
||||||
fun getVar(name: String) = fastCmd("echo \$$name")
|
|
||||||
fun getBool(name: String) = getVar(name).toBoolean()
|
|
||||||
|
|
||||||
Const.MAGISKTMP = getVar("MAGISKTMP")
|
|
||||||
Info.isSAR = getBool("SYSTEM_ROOT")
|
|
||||||
Info.ramdisk = getBool("RAMDISKEXIST")
|
|
||||||
Info.isAB = getBool("ISAB")
|
|
||||||
Info.crypto = getVar("CRYPTOTYPE")
|
|
||||||
Info.isPixel = fastCmd("getprop ro.product.brand") == "google"
|
|
||||||
|
|
||||||
// Default presets
|
|
||||||
Config.recovery = getBool("RECOVERYMODE")
|
|
||||||
Config.keepVerity = getBool("KEEPVERITY")
|
|
||||||
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
101
app/src/main/java/com/topjohnwu/magisk/core/utils/ShellInit.kt
Normal file
101
app/src/main/java/com/topjohnwu/magisk/core/utils/ShellInit.kt
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import com.topjohnwu.magisk.DynAPK
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.*
|
||||||
|
import com.topjohnwu.magisk.ktx.cachedFile
|
||||||
|
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||||
|
import com.topjohnwu.magisk.ktx.rawResource
|
||||||
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
|
import java.io.File
|
||||||
|
import java.util.jar.JarFile
|
||||||
|
|
||||||
|
abstract class BaseShellInit : Shell.Initializer() {
|
||||||
|
final override fun onInit(context: Context, shell: Shell): Boolean {
|
||||||
|
return init(context.wrap(), shell)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun init(context: Context, shell: Shell): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BusyBoxInit : BaseShellInit() {
|
||||||
|
|
||||||
|
override fun init(context: Context, shell: Shell): Boolean {
|
||||||
|
shell.newJob().apply {
|
||||||
|
add("export ASH_STANDALONE=1")
|
||||||
|
|
||||||
|
val localBB: File
|
||||||
|
if (isRunningAsStub) {
|
||||||
|
if (!shell.isRoot)
|
||||||
|
return true
|
||||||
|
val jar = JarFile(DynAPK.current(context))
|
||||||
|
val bb = jar.getJarEntry("lib/${Const.CPU_ABI_32}/libbusybox.so")
|
||||||
|
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
||||||
|
localBB.delete()
|
||||||
|
jar.getInputStream(bb).writeTo(localBB)
|
||||||
|
localBB.setExecutable(true)
|
||||||
|
} else {
|
||||||
|
localBB = File(Const.NATIVE_LIB_DIR, "libbusybox.so")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shell.isRoot) {
|
||||||
|
add("export MAGISKTMP=\$(magisk --path)/.magisk")
|
||||||
|
// Test if we can properly execute stuff in /data
|
||||||
|
Info.noDataExec = !shell.newJob().add("$localBB true").exec().isSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Info.noDataExec) {
|
||||||
|
// Copy it out of /data to workaround Samsung bullshit
|
||||||
|
add(
|
||||||
|
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
|
||||||
|
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
|
||||||
|
" exec \$MAGISKTMP/busybox/busybox sh",
|
||||||
|
"else",
|
||||||
|
" cp -af $localBB /dev/.busybox",
|
||||||
|
" exec /dev/.busybox sh",
|
||||||
|
"fi"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Directly execute the file
|
||||||
|
add("exec $localBB sh")
|
||||||
|
}
|
||||||
|
}.exec()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppShellInit : BaseShellInit() {
|
||||||
|
|
||||||
|
override fun init(context: Context, shell: Shell): Boolean {
|
||||||
|
|
||||||
|
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
|
||||||
|
fun getVar(name: String) = fastCmd("echo \$$name")
|
||||||
|
fun getBool(name: String) = getVar(name).toBoolean()
|
||||||
|
|
||||||
|
shell.newJob().apply {
|
||||||
|
add(context.rawResource(R.raw.manager))
|
||||||
|
if (shell.isRoot) {
|
||||||
|
add(context.assets.open("util_functions.sh"))
|
||||||
|
}
|
||||||
|
add("app_init")
|
||||||
|
}.exec()
|
||||||
|
|
||||||
|
Const.MAGISKTMP = getVar("MAGISKTMP")
|
||||||
|
Info.isSAR = getBool("SYSTEM_ROOT")
|
||||||
|
Info.ramdisk = getBool("RAMDISKEXIST")
|
||||||
|
Info.isAB = getBool("ISAB")
|
||||||
|
Info.crypto = getVar("CRYPTOTYPE")
|
||||||
|
|
||||||
|
// Default presets
|
||||||
|
Config.recovery = getBool("RECOVERYMODE")
|
||||||
|
Config.keepVerity = getBool("KEEPVERITY")
|
||||||
|
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
|||||||
dest = SuFile(folder, name)
|
dest = SuFile(folder, name)
|
||||||
dest.parentFile!!.mkdirs()
|
dest.parentFile!!.mkdirs()
|
||||||
}
|
}
|
||||||
SuFileOutputStream(dest).use { out -> zin.copyTo(out) }
|
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ import retrofit2.http.*
|
|||||||
private const val REVISION = "revision"
|
private const val REVISION = "revision"
|
||||||
private const val BRANCH = "branch"
|
private const val BRANCH = "branch"
|
||||||
private const val REPO = "repo"
|
private const val REPO = "repo"
|
||||||
|
private const val FILE = "file"
|
||||||
|
|
||||||
const val MAGISK_FILES = "topjohnwu/magisk_files"
|
const val MAGISK_FILES = "topjohnwu/magisk-files"
|
||||||
const val MAGISK_MAIN = "topjohnwu/Magisk"
|
const val MAGISK_MAIN = "topjohnwu/Magisk"
|
||||||
|
|
||||||
interface GithubPageServices {
|
interface GithubPageServices {
|
||||||
|
|
||||||
@GET("stable.json")
|
@GET("{$FILE}")
|
||||||
suspend fun fetchStableUpdate(): UpdateInfo
|
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
|
||||||
|
|
||||||
@GET("beta.json")
|
|
||||||
suspend fun fetchBetaUpdate(): UpdateInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JSDelivrServices {
|
interface JSDelivrServices {
|
||||||
@@ -33,9 +31,6 @@ interface JSDelivrServices {
|
|||||||
@Streaming
|
@Streaming
|
||||||
suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody
|
suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody
|
||||||
|
|
||||||
@GET("$MAGISK_FILES@{$REVISION}/canary.json")
|
|
||||||
suspend fun fetchCanaryUpdate(@Path(REVISION) revision: String): UpdateInfo
|
|
||||||
|
|
||||||
@GET("$MAGISK_MAIN@{$REVISION}/scripts/module_installer.sh")
|
@GET("$MAGISK_MAIN@{$REVISION}/scripts/module_installer.sh")
|
||||||
@Streaming
|
@Streaming
|
||||||
suspend fun fetchInstaller(@Path(REVISION) revision: String): ResponseBody
|
suspend fun fetchInstaller(@Path(REVISION) revision: String): ResponseBody
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.data.repository
|
package com.topjohnwu.magisk.data.repository
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||||
@@ -8,7 +9,6 @@ import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
|||||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.model.*
|
|
||||||
import com.topjohnwu.magisk.data.network.*
|
import com.topjohnwu.magisk.data.network.*
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -21,6 +21,10 @@ class NetworkService(
|
|||||||
private val api: GithubApiServices
|
private val api: GithubApiServices
|
||||||
) {
|
) {
|
||||||
suspend fun fetchUpdate() = safe {
|
suspend fun fetchUpdate() = safe {
|
||||||
|
// Pre SDK 21 no longer receives any major updates
|
||||||
|
if (Build.VERSION.SDK_INT < 21)
|
||||||
|
return fetchStableUpdate()
|
||||||
|
|
||||||
var info = when (Config.updateChannel) {
|
var info = when (Config.updateChannel) {
|
||||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||||
BETA_CHANNEL -> fetchBetaUpdate()
|
BETA_CHANNEL -> fetchBetaUpdate()
|
||||||
@@ -34,31 +38,14 @@ class NetworkService(
|
|||||||
Config.updateChannel = BETA_CHANNEL
|
Config.updateChannel = BETA_CHANNEL
|
||||||
info = fetchBetaUpdate()
|
info = fetchBetaUpdate()
|
||||||
}
|
}
|
||||||
Info.remote = info
|
|
||||||
info
|
info
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateInfo
|
// UpdateInfo
|
||||||
private suspend fun fetchStableUpdate() = pages.fetchStableUpdate()
|
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||||
private suspend fun fetchBetaUpdate() = pages.fetchBetaUpdate()
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
|
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||||
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
||||||
private suspend fun fetchCanaryUpdate(): UpdateInfo {
|
|
||||||
val sha = fetchCanaryVersion()
|
|
||||||
val info = jsd.fetchCanaryUpdate(sha)
|
|
||||||
|
|
||||||
fun genCDNUrl(name: String) = "${Const.Url.JS_DELIVR_URL}${MAGISK_FILES}@${sha}/${name}"
|
|
||||||
fun ManagerJson.updateCopy() = copy(link = genCDNUrl(link), note = genCDNUrl(note))
|
|
||||||
fun MagiskJson.updateCopy() = copy(link = genCDNUrl(link), note = genCDNUrl(note))
|
|
||||||
fun StubJson.updateCopy() = copy(link = genCDNUrl(link))
|
|
||||||
fun UninstallerJson.updateCopy() = copy(link = genCDNUrl(link))
|
|
||||||
|
|
||||||
return info.copy(
|
|
||||||
app = info.app.updateCopy(),
|
|
||||||
magisk = info.magisk.updateCopy(),
|
|
||||||
stub = info.stub.updateCopy(),
|
|
||||||
uninstaller = info.uninstaller.updateCopy()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <T> safe(factory: () -> T): T? {
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
return try {
|
return try {
|
||||||
@@ -92,6 +79,5 @@ class NetworkService(
|
|||||||
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
|
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
|
||||||
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
||||||
|
|
||||||
private suspend fun fetchCanaryVersion() = api.fetchBranch(MAGISK_FILES, "canary").commit.sha
|
|
||||||
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
|
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.topjohnwu.magisk.core.ResMgr
|
import com.topjohnwu.magisk.core.AssetHack
|
||||||
|
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
@@ -11,15 +11,9 @@ val SUTimeout = named("su_timeout")
|
|||||||
val Protected = named("protected")
|
val Protected = named("protected")
|
||||||
|
|
||||||
val applicationModule = module {
|
val applicationModule = module {
|
||||||
factory { ResMgr.resource }
|
factory { AssetHack.resource }
|
||||||
factory { get<Context>().packageManager }
|
factory { get<Context>().packageManager }
|
||||||
factory(Protected) { createDEContext(get()) }
|
factory(Protected) { get<Context>().deviceProtectedContext }
|
||||||
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
||||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
single { PreferenceManager.getDefaultSharedPreferences(get(Protected)) }
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDEContext(context: Context): Context {
|
|
||||||
return if (Build.VERSION.SDK_INT >= 24)
|
|
||||||
context.createDeviceProtectedStorageContext()
|
|
||||||
else context
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ val networkingModule = module {
|
|||||||
single { createRetrofit(get()) }
|
single { createRetrofit(get()) }
|
||||||
single { createApiService<RawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
single { createApiService<RawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||||
single { createApiService<GithubPageServices>(get(), Const.Url.GITHUB_PAGE_URL) }
|
single { createApiService<GithubPageServices>(get(),
|
||||||
|
if (Build.VERSION.SDK_INT < 21) Const.Url.GITHUB_OLD_PAGE_URL
|
||||||
|
else Const.Url.GITHUB_PAGE_URL
|
||||||
|
) }
|
||||||
single { createApiService<JSDelivrServices>(get(), Const.Url.JS_DELIVR_URL) }
|
single { createApiService<JSDelivrServices>(get(), Const.Url.JS_DELIVR_URL) }
|
||||||
single { createMarkwon(get(), get()) }
|
single { createMarkwon(get(), get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,10 @@ val viewModelModules = module {
|
|||||||
viewModel { ModuleViewModel(get(), get()) }
|
viewModel { ModuleViewModel(get(), get()) }
|
||||||
viewModel { SafetynetViewModel() }
|
viewModel { SafetynetViewModel() }
|
||||||
viewModel { SettingsViewModel(get()) }
|
viewModel { SettingsViewModel(get()) }
|
||||||
viewModel { SuperuserViewModel(get(), get()) }
|
viewModel { SuperuserViewModel(get()) }
|
||||||
viewModel { ThemeViewModel() }
|
viewModel { ThemeViewModel() }
|
||||||
viewModel { InstallViewModel(get()) }
|
viewModel { InstallViewModel(get()) }
|
||||||
viewModel { MainViewModel() }
|
viewModel { MainViewModel() }
|
||||||
|
viewModel { (args: FlashFragmentArgs) -> FlashViewModel(args) }
|
||||||
// Legacy
|
|
||||||
viewModel { (args: FlashFragmentArgs) -> FlashViewModel(args, get()) }
|
|
||||||
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ object RebootEvent {
|
|||||||
fun inflateMenu(activity: BaseActivity): PopupMenu {
|
fun inflateMenu(activity: BaseActivity): PopupMenu {
|
||||||
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
|
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
|
||||||
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
|
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
|
||||||
|
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||||
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true)
|
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true)
|
||||||
menu.menu.getItem(R.id.action_reboot_userspace).isVisible = true
|
menu.menu.findItem(R.id.action_reboot_userspace).isVisible = true
|
||||||
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
|
|
||||||
menu.setOnMenuItemClickListener(::reboot)
|
menu.setOnMenuItemClickListener(::reboot)
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,32 +6,33 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||||
import com.topjohnwu.magisk.arch.BaseUIActivity
|
import com.topjohnwu.magisk.arch.BaseUIActivity
|
||||||
import com.topjohnwu.magisk.arch.ViewEvent
|
import com.topjohnwu.magisk.arch.ViewEvent
|
||||||
import com.topjohnwu.magisk.utils.TransitiveText
|
import com.topjohnwu.magisk.utils.TextHolder
|
||||||
|
import com.topjohnwu.magisk.utils.asText
|
||||||
|
|
||||||
class SnackbarEvent private constructor(
|
class SnackbarEvent constructor(
|
||||||
private val msg: TransitiveText,
|
private val msg: TextHolder,
|
||||||
private val length: Int,
|
private val length: Int = Snackbar.LENGTH_SHORT,
|
||||||
private val builder: Snackbar.() -> Unit
|
private val builder: Snackbar.() -> Unit = {}
|
||||||
) : ViewEvent(), ActivityExecutor {
|
) : ViewEvent(), ActivityExecutor {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@StringRes res: Int,
|
@StringRes res: Int,
|
||||||
length: Int = Snackbar.LENGTH_SHORT,
|
length: Int = Snackbar.LENGTH_SHORT,
|
||||||
builder: Snackbar.() -> Unit = {}
|
builder: Snackbar.() -> Unit = {}
|
||||||
) : this(TransitiveText.Res(res), length, builder)
|
) : this(res.asText(), length, builder)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
message: String,
|
msg: String,
|
||||||
length: Int = Snackbar.LENGTH_SHORT,
|
length: Int = Snackbar.LENGTH_SHORT,
|
||||||
builder: Snackbar.() -> Unit = {}
|
builder: Snackbar.() -> Unit = {}
|
||||||
) : this(TransitiveText.String(message), length, builder)
|
) : this(msg.asText(), length, builder)
|
||||||
|
|
||||||
|
|
||||||
private fun snackbar(
|
private fun snackbar(
|
||||||
view: View,
|
view: View,
|
||||||
message: String,
|
message: String,
|
||||||
length: Int = Snackbar.LENGTH_SHORT,
|
length: Int,
|
||||||
builder: Snackbar.() -> Unit = {}
|
builder: Snackbar.() -> Unit
|
||||||
) = Snackbar.make(view, message, length).apply(builder).show()
|
) = Snackbar.make(view, message, length).apply(builder).show()
|
||||||
|
|
||||||
override fun invoke(activity: BaseUIActivity<*, *>) {
|
override fun invoke(activity: BaseUIActivity<*, *>) {
|
||||||
@@ -39,5 +40,4 @@ class SnackbarEvent private constructor(
|
|||||||
msg.getText(activity.resources).toString(),
|
msg.getText(activity.resources).toString(),
|
||||||
length, builder)
|
length, builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,20 +14,22 @@ import com.topjohnwu.magisk.core.Const
|
|||||||
import com.topjohnwu.magisk.core.base.ActivityResultCallback
|
import com.topjohnwu.magisk.core.base.ActivityResultCallback
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
|
import com.topjohnwu.magisk.events.dialog.MarkDownDialog
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), ActivityExecutor {
|
class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), ActivityExecutor {
|
||||||
override fun invoke(activity: BaseUIActivity<*, *>) = action(activity)
|
override fun invoke(activity: BaseUIActivity<*, *>) = action(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
class OpenReadmeEvent(val item: OnlineModule) : ViewEventWithScope(), ContextExecutor {
|
class OpenReadmeEvent(private val item: OnlineModule) : MarkDownDialog() {
|
||||||
override fun invoke(context: Context) {
|
override suspend fun getMarkdownText() = item.notes()
|
||||||
scope.launch {
|
override fun build(dialog: MagiskDialog) {
|
||||||
MarkDownWindow.show(context, null, item::notes)
|
super.build(dialog)
|
||||||
}
|
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
|
titleRes = android.R.string.cancel
|
||||||
|
}.cancellable(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ class MagiskInstallFileEvent(private val callback: ActivityResultCallback)
|
|||||||
override fun invoke(activity: BaseUIActivity<*, *>) {
|
override fun invoke(activity: BaseUIActivity<*, *>) {
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("*/*")
|
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("*/*")
|
||||||
try {
|
try {
|
||||||
activity.startActivityForResult(intent, Const.ID.SELECT_FILE, callback)
|
activity.startActivityForResult(intent, callback)
|
||||||
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
|
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||||
@@ -107,10 +109,10 @@ class SelectModuleEvent : ViewEvent(), FragmentExecutor {
|
|||||||
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("application/zip")
|
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("application/zip")
|
||||||
try {
|
try {
|
||||||
fragment.apply {
|
fragment.apply {
|
||||||
activity.startActivityForResult(intent, Const.ID.FETCH_ZIP) { code, intent ->
|
activity.startActivityForResult(intent) { code, intent ->
|
||||||
if (code == Activity.RESULT_OK && intent != null) {
|
if (code == Activity.RESULT_OK && intent != null) {
|
||||||
intent.data?.also {
|
intent.data?.also {
|
||||||
MainDirections.actionFlashFragment(it, Const.Value.FLASH_ZIP).navigate()
|
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ abstract class DialogEvent : ViewEvent(), ActivityExecutor {
|
|||||||
protected lateinit var dialog: MagiskDialog
|
protected lateinit var dialog: MagiskDialog
|
||||||
|
|
||||||
override fun invoke(activity: BaseUIActivity<*, *>) {
|
override fun invoke(activity: BaseUIActivity<*, *>) {
|
||||||
dialog = MagiskDialog(activity).apply(this::build).reveal()
|
dialog = MagiskDialog(activity)
|
||||||
|
.apply { setOwnerActivity(activity) }
|
||||||
|
.apply(this::build).reveal()
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract fun build(dialog: MagiskDialog)
|
abstract fun build(dialog: MagiskDialog)
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.events.dialog
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import androidx.lifecycle.lifecycleScope
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.download.Action.EnvFix
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
import com.topjohnwu.magisk.core.download.Subject.Magisk
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class EnvFixDialog : DialogEvent() {
|
class EnvFixDialog : DialogEvent() {
|
||||||
|
|
||||||
@@ -24,20 +20,17 @@ class EnvFixDialog : DialogEvent() {
|
|||||||
.applyMessage(R.string.setup_msg)
|
.applyMessage(R.string.setup_msg)
|
||||||
.resetButtons()
|
.resetButtons()
|
||||||
.cancellable(false)
|
.cancellable(false)
|
||||||
val lbm = LocalBroadcastManager.getInstance(dialog.context)
|
(dialog.ownerActivity as BaseActivity).lifecycleScope.launch {
|
||||||
lbm.registerReceiver(object : BroadcastReceiver() {
|
MagiskInstaller.FixEnv {
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
lbm.unregisterReceiver(this)
|
}.exec()
|
||||||
}
|
}
|
||||||
}, IntentFilter(DISMISS))
|
|
||||||
DownloadService.start(dialog.context, Magisk(EnvFix))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
titleRes = android.R.string.cancel
|
titleRes = android.R.string.cancel
|
||||||
}
|
}
|
||||||
.let { Unit }
|
.let { }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val DISMISS = "com.topjohnwu.magisk.ENV_DONE"
|
const val DISMISS = "com.topjohnwu.magisk.ENV_DONE"
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.events.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.ktx.res
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
import org.koin.core.get
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.koin.core.inject
|
||||||
import kotlinx.coroutines.GlobalScope
|
import java.io.File
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class ManagerInstallDialog : DialogEvent() {
|
class ManagerInstallDialog : MarkDownDialog() {
|
||||||
|
|
||||||
|
private val svc: NetworkService by inject()
|
||||||
|
|
||||||
|
override suspend fun getMarkdownText(): String {
|
||||||
|
val text = svc.fetchString(Info.remote.magisk.note)
|
||||||
|
// Cache the changelog
|
||||||
|
val context = get<Context>()
|
||||||
|
context.cacheDir.listFiles { _, name -> name.endsWith(".md") }.orEmpty().forEach {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
File(context.cacheDir, "${Info.remote.magisk.versionCode}.md").writeText(text)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
override fun build(dialog: MagiskDialog) {
|
override fun build(dialog: MagiskDialog) {
|
||||||
|
super.build(dialog)
|
||||||
with(dialog) {
|
with(dialog) {
|
||||||
val subject = Subject.Manager()
|
|
||||||
|
|
||||||
applyTitle(R.string.repo_install_title.res(R.string.app_name.res()))
|
|
||||||
applyMessage(R.string.repo_install_msg.res(subject.title))
|
|
||||||
|
|
||||||
setCancelable(true)
|
setCancelable(true)
|
||||||
|
|
||||||
applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||||
titleRes = R.string.install
|
titleRes = R.string.install
|
||||||
onClick { DownloadService.start(context, subject) }
|
onClick { DownloadService.start(context, Subject.Manager()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Info.remote.app.note.isEmpty()) return
|
|
||||||
applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
titleRes = R.string.app_changelog
|
titleRes = android.R.string.cancel
|
||||||
onClick {
|
|
||||||
GlobalScope.launch(Dispatchers.Main.immediate) {
|
|
||||||
MarkDownWindow.show(context, null, Info.remote.app.note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.topjohnwu.magisk.events.dialog
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
import io.noties.markwon.Markwon
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
import org.koin.core.inject
|
||||||
|
import timber.log.Timber
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
abstract class MarkDownDialog : DialogEvent(), KoinComponent {
|
||||||
|
|
||||||
|
private val markwon: Markwon by inject()
|
||||||
|
|
||||||
|
abstract suspend fun getMarkdownText(): String
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun build(dialog: MagiskDialog) {
|
||||||
|
with(dialog) {
|
||||||
|
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
|
||||||
|
applyView(view)
|
||||||
|
(ownerActivity as BaseActivity).lifecycleScope.launch {
|
||||||
|
val tv = view.findViewById<TextView>(R.id.md_txt)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
markwon.setMarkdown(tv, getMarkdownText())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException)
|
||||||
|
throw e
|
||||||
|
Timber.e(e)
|
||||||
|
tv.post { tv.setText(R.string.download_file_error) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
|
|||||||
with(dialog) {
|
with(dialog) {
|
||||||
|
|
||||||
fun download(install: Boolean) {
|
fun download(install: Boolean) {
|
||||||
val config = if (install) Action.Flash.Primary else Action.Download
|
val config = if (install) Action.Flash else Action.Download
|
||||||
val subject = Subject.Module(item, config)
|
val subject = Subject.Module(item, config)
|
||||||
DownloadService.start(context, subject)
|
DownloadService.start(context, subject)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ package com.topjohnwu.magisk.events.dialog
|
|||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.core.download.Action
|
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@@ -20,13 +17,11 @@ class UninstallDialog : DialogEvent() {
|
|||||||
titleRes = R.string.restore_img
|
titleRes = R.string.restore_img
|
||||||
onClick { restore() }
|
onClick { restore() }
|
||||||
}
|
}
|
||||||
if (Info.remote.uninstaller.link.isNotEmpty()) {
|
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||||
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
|
||||||
titleRes = R.string.complete_uninstall
|
titleRes = R.string.complete_uninstall
|
||||||
onClick { completeUninstall() }
|
onClick { completeUninstall() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun restore() {
|
private fun restore() {
|
||||||
@@ -46,7 +41,7 @@ class UninstallDialog : DialogEvent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun completeUninstall() {
|
private fun completeUninstall() {
|
||||||
DownloadService.start(dialog.context, Subject.Magisk(Action.Uninstall))
|
FlashFragment.uninstall()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
193
app/src/main/java/com/topjohnwu/magisk/ktx/RecyclerView.kt
Normal file
193
app/src/main/java/com/topjohnwu/magisk/ktx/RecyclerView.kt
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package com.topjohnwu.magisk.ktx
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.EdgeEffect
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
|
||||||
|
fun RecyclerView.addInvalidateItemDecorationsObserver() {
|
||||||
|
|
||||||
|
adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||||
|
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||||
|
invalidateItemDecorations()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||||
|
invalidateItemDecorations()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||||
|
invalidateItemDecorations()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecyclerView.addVerticalPadding(paddingTop: Int = 0, paddingBottom: Int = 0) {
|
||||||
|
addItemDecoration(VerticalPaddingDecoration(paddingTop, paddingBottom))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class VerticalPaddingDecoration(private val paddingTop: Int = 0, private val paddingBottom: Int = 0) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
private var allowTop: Boolean = true
|
||||||
|
private var allowBottom: Boolean = true
|
||||||
|
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
val adapter = parent.adapter ?: return
|
||||||
|
val position = parent.getChildAdapterPosition(view)
|
||||||
|
val count = adapter.itemCount
|
||||||
|
if (position == 0 && allowTop) {
|
||||||
|
outRect.top = paddingTop
|
||||||
|
} else if (position == count - 1 && allowBottom) {
|
||||||
|
outRect.bottom = paddingBottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecyclerView.addSimpleItemDecoration(
|
||||||
|
left: Int = 0,
|
||||||
|
top: Int = 0,
|
||||||
|
right: Int = 0,
|
||||||
|
bottom: Int = 0,
|
||||||
|
) {
|
||||||
|
addItemDecoration(SimpleItemDecoration(left, top, right, bottom))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SimpleItemDecoration(
|
||||||
|
private val left: Int = 0,
|
||||||
|
private val top: Int = 0,
|
||||||
|
private val right: Int = 0,
|
||||||
|
private val bottom: Int = 0
|
||||||
|
) : RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
private var allowLeft: Boolean = true
|
||||||
|
private var allowTop: Boolean = true
|
||||||
|
private var allowRight: Boolean = true
|
||||||
|
private var allowBottom: Boolean = true
|
||||||
|
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
if (parent.adapter == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (allowLeft) {
|
||||||
|
outRect.left = left
|
||||||
|
}
|
||||||
|
if (allowTop) {
|
||||||
|
outRect.top = top
|
||||||
|
}
|
||||||
|
if (allowRight) {
|
||||||
|
outRect.right = right
|
||||||
|
}
|
||||||
|
if (allowBottom) {
|
||||||
|
outRect.top = bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RecyclerView.fixEdgeEffect(overScrollIfContentScrolls: Boolean = true, alwaysClipToPadding: Boolean = true) {
|
||||||
|
if (overScrollIfContentScrolls) {
|
||||||
|
val listener = OverScrollIfContentScrollsListener()
|
||||||
|
addOnLayoutChangeListener(listener)
|
||||||
|
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, listener)
|
||||||
|
} else {
|
||||||
|
val listener = getTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener) as? OverScrollIfContentScrollsListener
|
||||||
|
if (listener != null) {
|
||||||
|
removeOnLayoutChangeListener(listener)
|
||||||
|
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeEffectFactory = if (alwaysClipToPadding && !clipToPadding) {
|
||||||
|
AlwaysClipToPaddingEdgeEffectFactory()
|
||||||
|
} else {
|
||||||
|
RecyclerView.EdgeEffectFactory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OverScrollIfContentScrollsListener : View.OnLayoutChangeListener {
|
||||||
|
private var show = true
|
||||||
|
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||||
|
if (shouldDrawOverScroll(v as RecyclerView) != show) {
|
||||||
|
show = !show
|
||||||
|
if (show) {
|
||||||
|
v.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS)
|
||||||
|
} else {
|
||||||
|
v.setOverScrollMode(View.OVER_SCROLL_NEVER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldDrawOverScroll(recyclerView: RecyclerView): Boolean {
|
||||||
|
if (recyclerView.layoutManager == null || recyclerView.adapter == null || recyclerView.adapter!!.itemCount == 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (recyclerView.layoutManager is LinearLayoutManager) {
|
||||||
|
val itemCount = recyclerView.layoutManager!!.itemCount
|
||||||
|
val firstPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findFirstCompletelyVisibleItemPosition()
|
||||||
|
val lastPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition()
|
||||||
|
return firstPosition != 0 || lastPosition != itemCount - 1
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AlwaysClipToPaddingEdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
|
||||||
|
|
||||||
|
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||||
|
|
||||||
|
return object : EdgeEffect(view.context) {
|
||||||
|
private var ensureSize = false
|
||||||
|
|
||||||
|
private fun ensureSize() {
|
||||||
|
if (ensureSize) return
|
||||||
|
ensureSize = true
|
||||||
|
|
||||||
|
when (direction) {
|
||||||
|
DIRECTION_LEFT -> {
|
||||||
|
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
|
||||||
|
view.measuredWidth - view.paddingLeft - view.paddingRight)
|
||||||
|
}
|
||||||
|
DIRECTION_TOP -> {
|
||||||
|
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
|
||||||
|
view.measuredHeight - view.paddingTop - view.paddingBottom)
|
||||||
|
}
|
||||||
|
DIRECTION_RIGHT -> {
|
||||||
|
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
|
||||||
|
view.measuredWidth - view.paddingLeft - view.paddingRight)
|
||||||
|
}
|
||||||
|
DIRECTION_BOTTOM -> {
|
||||||
|
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
|
||||||
|
view.measuredHeight - view.paddingTop - view.paddingBottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(c: Canvas): Boolean {
|
||||||
|
ensureSize()
|
||||||
|
|
||||||
|
val restore = c.save()
|
||||||
|
when (direction) {
|
||||||
|
DIRECTION_LEFT -> {
|
||||||
|
c.translate(view.paddingBottom.toFloat(), 0f)
|
||||||
|
}
|
||||||
|
DIRECTION_TOP -> {
|
||||||
|
c.translate(view.paddingLeft.toFloat(), view.paddingTop.toFloat())
|
||||||
|
}
|
||||||
|
DIRECTION_RIGHT -> {
|
||||||
|
c.translate(-view.paddingTop.toFloat(), 0f)
|
||||||
|
}
|
||||||
|
DIRECTION_BOTTOM -> {
|
||||||
|
c.translate(view.paddingRight.toFloat(), view.paddingBottom.toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val res = super.draw(c)
|
||||||
|
c.restoreToCount(restore)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,11 @@ import android.content.Context
|
|||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.ComponentInfo
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.*
|
import android.content.pm.PackageManager.*
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS
|
||||||
|
import android.content.pm.ServiceInfo.FLAG_USE_APP_ZYGOTE
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
@@ -19,8 +21,8 @@ import android.graphics.drawable.AdaptiveIconDrawable
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import android.system.Os
|
||||||
import android.text.PrecomputedText
|
import android.text.PrecomputedText
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@@ -39,11 +41,13 @@ import androidx.core.widget.TextViewCompat
|
|||||||
import androidx.databinding.BindingAdapter
|
import androidx.databinding.BindingAdapter
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.transition.AutoTransition
|
import androidx.transition.AutoTransition
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.AssetHack
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.ResMgr
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
@@ -53,37 +57,31 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.lang.reflect.Method
|
||||||
import java.lang.reflect.Array as JArray
|
import java.lang.reflect.Array as JArray
|
||||||
|
|
||||||
val packageName: String get() = get<Context>().packageName
|
val packageName: String get() = get<Context>().packageName
|
||||||
|
|
||||||
val ApplicationInfo.processes: List<String> @SuppressLint("InlinedApi") get() {
|
private lateinit var osSymlink: Method
|
||||||
val pm = get<PackageManager>()
|
private lateinit var os: Any
|
||||||
val appProcessName = processName ?: packageName
|
|
||||||
val baseFlag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES
|
fun symlink(oldPath: String, newPath: String) {
|
||||||
val packageInfo = try {
|
if (SDK_INT >= 21) {
|
||||||
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
Os.symlink(oldPath, newPath)
|
||||||
pm.getPackageInfo(packageName, baseFlag or request)
|
} else {
|
||||||
} catch (e: NameNotFoundException) { // EdXposed hooked, issue#3276
|
if (!::osSymlink.isInitialized) {
|
||||||
return listOf(appProcessName)
|
os = Class.forName("libcore.io.Libcore").getField("os").get(null)!!
|
||||||
} catch (e: Exception) {
|
osSymlink = os.javaClass.getMethod("symlink", String::class.java, String::class.java)
|
||||||
// Exceed binder data transfer limit, fetch each component type separately
|
}
|
||||||
pm.getPackageInfo(packageName, baseFlag).apply {
|
osSymlink.invoke(os, oldPath, newPath)
|
||||||
runCatching { activities = pm.getPackageInfo(packageName, GET_ACTIVITIES).activities }
|
|
||||||
runCatching { services = pm.getPackageInfo(packageName, GET_SERVICES).services }
|
|
||||||
runCatching { receivers = pm.getPackageInfo(packageName, GET_RECEIVERS).receivers }
|
|
||||||
runCatching { providers = pm.getPackageInfo(packageName, GET_PROVIDERS).providers }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fun Array<out ComponentInfo>.processNames() = map { it.processName ?: appProcessName }
|
|
||||||
return with(packageInfo) {
|
|
||||||
activities?.processNames().orEmpty() +
|
|
||||||
services?.processNames().orEmpty() +
|
|
||||||
receivers?.processNames().orEmpty() +
|
|
||||||
providers?.processNames().orEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val ServiceInfo.isIsolated get() = (flags and FLAG_ISOLATED_PROCESS) != 0
|
||||||
|
|
||||||
|
@get:SuppressLint("InlinedApi")
|
||||||
|
val ServiceInfo.useAppZygote get() = (flags and FLAG_USE_APP_ZYGOTE) != 0
|
||||||
|
|
||||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||||
|
|
||||||
fun Context.getBitmap(id: Int): Bitmap {
|
fun Context.getBitmap(id: Int): Bitmap {
|
||||||
@@ -103,6 +101,11 @@ fun Context.getBitmap(id: Int): Bitmap {
|
|||||||
return bitmap
|
return bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val Context.deviceProtectedContext: Context get() =
|
||||||
|
if (SDK_INT >= 24) {
|
||||||
|
createDeviceProtectedStorageContext()
|
||||||
|
} else { this }
|
||||||
|
|
||||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||||
|
|
||||||
fun Intent.startActivityWithRoot() {
|
fun Intent.startActivityWithRoot() {
|
||||||
@@ -247,15 +250,13 @@ fun Context.colorStateListCompat(@ColorRes id: Int) = try {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id)
|
fun Context.drawableCompat(@DrawableRes id: Int) = AppCompatResources.getDrawable(this, id)
|
||||||
/**
|
/**
|
||||||
* Pass [start] and [end] dimensions, function will return left and right
|
* Pass [start] and [end] dimensions, function will return left and right
|
||||||
* with respect to RTL layout direction
|
* with respect to RTL layout direction
|
||||||
*/
|
*/
|
||||||
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
||||||
if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||||
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
|
||||||
) {
|
|
||||||
return end to start
|
return end to start
|
||||||
}
|
}
|
||||||
return start to end
|
return start to end
|
||||||
@@ -315,8 +316,21 @@ fun ViewGroup.startAnimations() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val View.activity: Activity get() {
|
||||||
|
var context = context
|
||||||
|
while(true) {
|
||||||
|
if (context !is ContextWrapper)
|
||||||
|
error("View is not attached to activity")
|
||||||
|
if (context is Activity)
|
||||||
|
return context
|
||||||
|
context = context.baseContext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var View.coroutineScope: CoroutineScope
|
var View.coroutineScope: CoroutineScope
|
||||||
get() = getTag(R.id.coroutineScope) as? CoroutineScope ?: GlobalScope
|
get() = getTag(R.id.coroutineScope) as? CoroutineScope
|
||||||
|
?: (activity as? BaseActivity)?.lifecycleScope
|
||||||
|
?: GlobalScope
|
||||||
set(value) = setTag(R.id.coroutineScope, value)
|
set(value) = setTag(R.id.coroutineScope, value)
|
||||||
|
|
||||||
@set:BindingAdapter("precomputedText")
|
@set:BindingAdapter("precomputedText")
|
||||||
@@ -366,6 +380,16 @@ var TextView.precomputedText: CharSequence
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Int.dpInPx(): Int {
|
fun Int.dpInPx(): Int {
|
||||||
val scale = ResMgr.resource.displayMetrics.density
|
val scale = AssetHack.resource.displayMetrics.density
|
||||||
return (this * scale + 0.5).toInt()
|
return (this * scale + 0.5).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("PrivateApi")
|
||||||
|
fun getProperty(key: String, def: String): String {
|
||||||
|
runCatching {
|
||||||
|
val clazz = Class.forName("android.os.SystemProperties")
|
||||||
|
val get = clazz.getMethod("get", String::class.java, String::class.java)
|
||||||
|
return get.invoke(clazz, key, def) as String
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,14 +6,12 @@ import timber.log.Timber
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
|
||||||
import java.lang.reflect.Method
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||||
var entry: ZipEntry? = nextEntry
|
var entry: ZipEntry? = nextEntry
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
callback(entry)
|
callback(entry)
|
||||||
@@ -53,7 +51,7 @@ fun String.langTagToLocale(): Locale {
|
|||||||
if (Build.VERSION.SDK_INT >= 21) {
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
return Locale.forLanguageTag(this)
|
return Locale.forLanguageTag(this)
|
||||||
} else {
|
} else {
|
||||||
val tok = split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
val tok = split("[-_]".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
if (tok.isEmpty()) {
|
if (tok.isEmpty()) {
|
||||||
return Locale("")
|
return Locale("")
|
||||||
}
|
}
|
||||||
@@ -110,32 +108,3 @@ fun Locale.toLangTag(): String {
|
|||||||
|
|
||||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
fun SimpleDateFormat.parseOrNull(date: String) =
|
||||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||||
|
|
||||||
// Reflection hacks
|
|
||||||
|
|
||||||
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
|
|
||||||
private val getDeclaredMethod = Class::class.java.getMethod("getDeclaredMethod",
|
|
||||||
String::class.java, arrayOf<Class<*>>()::class.java)
|
|
||||||
private val getDeclaredField = Class::class.java.getMethod("getDeclaredField", String::class.java)
|
|
||||||
|
|
||||||
fun ClassLoader.forceLoadClass(name: String) =
|
|
||||||
runCatching { loadClass.invoke(this, name) }.getOrNull() as Class<*>?
|
|
||||||
|
|
||||||
fun Class<*>.forceGetDeclaredMethod(name: String, vararg types: Class<*>) =
|
|
||||||
(runCatching { getDeclaredMethod.invoke(this, name, types) }.getOrNull() as Method?)?.also {
|
|
||||||
it.isAccessible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Class<*>.forceGetDeclaredField(name: String) =
|
|
||||||
(runCatching { getDeclaredField.invoke(this, name) }.getOrNull() as Field?)?.also {
|
|
||||||
it.isAccessible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> T.forceGetClass(name: String) =
|
|
||||||
T::class.java.classLoader?.forceLoadClass(name)
|
|
||||||
|
|
||||||
fun Class<*>.forceGetField(name: String): Field? =
|
|
||||||
forceGetDeclaredField(name) ?: superclass?.forceGetField(name)
|
|
||||||
|
|
||||||
fun Class<*>.forceGetMethod(name: String, vararg types: Class<*>): Method? =
|
|
||||||
forceGetDeclaredMethod(name, *types) ?: superclass?.forceGetMethod(name, *types)
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.ui
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
@@ -10,10 +11,7 @@ import android.view.WindowManager
|
|||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.setPadding
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.google.android.material.card.MaterialCardView
|
|
||||||
import com.topjohnwu.magisk.MainDirections
|
import com.topjohnwu.magisk.MainDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIActivity
|
import com.topjohnwu.magisk.arch.BaseUIActivity
|
||||||
@@ -23,13 +21,12 @@ import com.topjohnwu.magisk.core.*
|
|||||||
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
import com.topjohnwu.magisk.ktx.startAnimations
|
||||||
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||||
import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior
|
|
||||||
import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior
|
|
||||||
import com.topjohnwu.magisk.utils.HideableBehavior
|
import com.topjohnwu.magisk.utils.HideableBehavior
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class MainViewModel : BaseViewModel()
|
class MainViewModel : BaseViewModel()
|
||||||
|
|
||||||
@@ -37,15 +34,7 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
|||||||
|
|
||||||
override val layoutRes = R.layout.activity_main_md2
|
override val layoutRes = R.layout.activity_main_md2
|
||||||
override val viewModel by viewModel<MainViewModel>()
|
override val viewModel by viewModel<MainViewModel>()
|
||||||
override val navHost: Int = R.id.main_nav_host
|
override val navHostId: Int = R.id.main_nav_host
|
||||||
|
|
||||||
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
|
|
||||||
//padding on itself given insets are not consumed beforehand. Unfortunately the listener
|
|
||||||
//implementation doesn't favor us against the design library, so on re-create it's often given
|
|
||||||
//upper hand.
|
|
||||||
private val navObserver = ViewTreeObserver.OnGlobalLayoutListener {
|
|
||||||
binding.mainNavigation.setPadding(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isRootFragment = true
|
private var isRootFragment = true
|
||||||
|
|
||||||
@@ -86,12 +75,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
|||||||
|
|
||||||
setSupportActionBar(binding.mainToolbar)
|
setSupportActionBar(binding.mainToolbar)
|
||||||
|
|
||||||
binding.mainToolbarWrapper.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
|
||||||
behavior = HideTopViewOnScrollBehavior<MaterialCardView>()
|
|
||||||
}
|
|
||||||
binding.mainBottomBar.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
|
||||||
behavior = HideBottomViewOnScrollBehavior<MaterialCardView>()
|
|
||||||
}
|
|
||||||
binding.mainNavigation.setOnNavigationItemSelectedListener {
|
binding.mainNavigation.setOnNavigationItemSelectedListener {
|
||||||
getScreen(it.itemId)?.navigate()
|
getScreen(it.itemId)?.navigate()
|
||||||
true
|
true
|
||||||
@@ -100,9 +83,7 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
|||||||
(currentFragment as? ReselectionTarget)?.onReselected()
|
(currentFragment as? ReselectionTarget)?.onReselected()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver)
|
val section = if (intent.action == Intent.ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS
|
||||||
|
|
||||||
val section = if (intent.action == ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS
|
|
||||||
else intent.getStringExtra(Const.Key.OPEN_SECTION)
|
else intent.getStringExtra(Const.Key.OPEN_SECTION)
|
||||||
getScreen(section)?.navigate()
|
getScreen(section)?.navigate()
|
||||||
|
|
||||||
@@ -116,16 +97,11 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
binding.mainNavigation.menu.apply {
|
binding.mainNavigation.menu.apply {
|
||||||
findItem(R.id.superuserFragment)?.isEnabled = Info.env.isActive
|
findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
|
||||||
findItem(R.id.logFragment)?.isEnabled = Info.env.isActive
|
findItem(R.id.logFragment)?.isEnabled = Info.env.isActive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver)
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
android.R.id.home -> onBackPressed()
|
android.R.id.home -> onBackPressed()
|
||||||
@@ -190,9 +166,9 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
|||||||
|
|
||||||
private fun getScreen(name: String?): NavDirections? {
|
private fun getScreen(name: String?): NavDirections? {
|
||||||
return when (name) {
|
return when (name) {
|
||||||
Const.Nav.SUPERUSER -> HomeFragmentDirections.actionSuperuserFragment()
|
Const.Nav.SUPERUSER -> MainDirections.actionSuperuserFragment()
|
||||||
Const.Nav.HIDE -> HomeFragmentDirections.actionHideFragment()
|
Const.Nav.HIDE -> MainDirections.actionHideFragment()
|
||||||
Const.Nav.MODULES -> HomeFragmentDirections.actionModuleFragment()
|
Const.Nav.MODULES -> MainDirections.actionModuleFragment()
|
||||||
Const.Nav.SETTINGS -> HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
|
Const.Nav.SETTINGS -> HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
@@ -214,7 +190,37 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
|||||||
.applyTitle(R.string.unsupport_magisk_title)
|
.applyTitle(R.string.unsupport_magisk_title)
|
||||||
.applyMessage(R.string.unsupport_magisk_msg, Const.Version.MIN_VERSION)
|
.applyMessage(R.string.unsupport_magisk_msg, Const.Version.MIN_VERSION)
|
||||||
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
|
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
|
||||||
.cancellable(true)
|
.cancellable(false)
|
||||||
|
.reveal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
|
||||||
|
?.split(':')
|
||||||
|
?.filterNot { File("$it/magisk").exists() }
|
||||||
|
?.any { File("$it/su").exists() } == true) {
|
||||||
|
MagiskDialog(this)
|
||||||
|
.applyTitle(R.string.unsupport_general_title)
|
||||||
|
.applyMessage(R.string.unsupport_other_su_msg)
|
||||||
|
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
|
||||||
|
.cancellable(false)
|
||||||
|
.reveal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
|
||||||
|
MagiskDialog(this)
|
||||||
|
.applyTitle(R.string.unsupport_general_title)
|
||||||
|
.applyMessage(R.string.unsupport_system_app_msg)
|
||||||
|
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
|
||||||
|
.cancellable(false)
|
||||||
|
.reveal()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0) {
|
||||||
|
MagiskDialog(this)
|
||||||
|
.applyTitle(R.string.unsupport_general_title)
|
||||||
|
.applyMessage(R.string.unsupport_external_storage_msg)
|
||||||
|
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
|
||||||
|
.cancellable(false)
|
||||||
.reveal()
|
.reveal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,11 +244,4 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
|||||||
.reveal()
|
.reveal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val ACTION_APPLICATION_PREFERENCES get() =
|
|
||||||
if (Build.VERSION.SDK_INT >= 24) Intent.ACTION_APPLICATION_PREFERENCES
|
|
||||||
else "???"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.navigation.NavDeepLinkBuilder
|
import androidx.navigation.NavDeepLinkBuilder
|
||||||
|
import com.topjohnwu.magisk.MainDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIActivity
|
|
||||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.cmp
|
import com.topjohnwu.magisk.core.cmp
|
||||||
@@ -16,14 +16,12 @@ import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
|
|||||||
import com.topjohnwu.magisk.ui.MainActivity
|
import com.topjohnwu.magisk.ui.MainActivity
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import com.topjohnwu.magisk.MainDirections.Companion.actionFlashFragment as toFlash
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragmentArgs as args
|
|
||||||
|
|
||||||
class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>() {
|
class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>() {
|
||||||
|
|
||||||
override val layoutRes = R.layout.fragment_flash_md2
|
override val layoutRes = R.layout.fragment_flash_md2
|
||||||
override val viewModel by viewModel<FlashViewModel> {
|
override val viewModel by viewModel<FlashViewModel> {
|
||||||
parametersOf(args.fromBundle(requireArguments()))
|
parametersOf(FlashFragmentArgs.fromBundle(requireArguments()))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var defaultOrientation = -1
|
private var defaultOrientation = -1
|
||||||
@@ -32,6 +30,10 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
|
|||||||
super.onStart()
|
super.onStart()
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
activity.setTitle(R.string.flash_screen_title)
|
activity.setTitle(R.string.flash_screen_title)
|
||||||
|
|
||||||
|
viewModel.subtitle.observe(this) {
|
||||||
|
activity.supportActionBar?.setSubtitle(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
@@ -47,6 +49,7 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
|
|||||||
|
|
||||||
defaultOrientation = activity.requestedOrientation
|
defaultOrientation = activity.requestedOrientation
|
||||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||||
|
viewModel.startFlashing()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
@@ -74,7 +77,7 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private fun createIntent(context: Context, args: args) =
|
private fun createIntent(context: Context, args: FlashFragmentArgs) =
|
||||||
NavDeepLinkBuilder(context)
|
NavDeepLinkBuilder(context)
|
||||||
.setGraph(R.navigation.main)
|
.setGraph(R.navigation.main)
|
||||||
.setComponentName(MainActivity::class.java.cmp(context.packageName))
|
.setComponentName(MainActivity::class.java.cmp(context.packageName))
|
||||||
@@ -87,61 +90,36 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
|
|||||||
|
|
||||||
/* Flashing is understood as installing / flashing magisk itself */
|
/* Flashing is understood as installing / flashing magisk itself */
|
||||||
|
|
||||||
fun flashIntent(context: Context, file: Uri, isSecondSlot: Boolean, id: Int = -1) = args(
|
fun flash(isSecondSlot: Boolean) = MainDirections.actionFlashFragment(
|
||||||
installer = file,
|
action = flashType(isSecondSlot)
|
||||||
action = flashType(isSecondSlot),
|
)
|
||||||
dismissId = id
|
|
||||||
).let { createIntent(context, it) }
|
|
||||||
|
|
||||||
fun flash(file: Uri, isSecondSlot: Boolean, id: Int) = toFlash(
|
|
||||||
installer = file,
|
|
||||||
action = flashType(isSecondSlot),
|
|
||||||
dismissId = id
|
|
||||||
).let { BaseUIActivity.postDirections(it) }
|
|
||||||
|
|
||||||
/* Patching is understood as injecting img files with magisk */
|
/* Patching is understood as injecting img files with magisk */
|
||||||
|
|
||||||
fun patchIntent(context: Context, file: Uri, uri: Uri, id: Int = -1) = args(
|
fun patch(uri: Uri) = MainDirections.actionFlashFragment(
|
||||||
installer = file,
|
|
||||||
action = Const.Value.PATCH_FILE,
|
action = Const.Value.PATCH_FILE,
|
||||||
additionalData = uri,
|
additionalData = uri
|
||||||
dismissId = id
|
)
|
||||||
).let { createIntent(context, it) }
|
|
||||||
|
|
||||||
fun patch(file: Uri, uri: Uri, id: Int) = toFlash(
|
|
||||||
installer = file,
|
|
||||||
action = Const.Value.PATCH_FILE,
|
|
||||||
additionalData = uri,
|
|
||||||
dismissId = id
|
|
||||||
).let { BaseUIActivity.postDirections(it) }
|
|
||||||
|
|
||||||
/* Uninstalling is understood as removing magisk entirely */
|
/* Uninstalling is understood as removing magisk entirely */
|
||||||
|
|
||||||
fun uninstallIntent(context: Context, file: Uri, id: Int = -1) = args(
|
fun uninstall() = MainDirections.actionFlashFragment(
|
||||||
installer = file,
|
action = Const.Value.UNINSTALL
|
||||||
action = Const.Value.UNINSTALL,
|
)
|
||||||
dismissId = id
|
|
||||||
).let { createIntent(context, it) }
|
|
||||||
|
|
||||||
fun uninstall(file: Uri, id: Int) = toFlash(
|
|
||||||
installer = file,
|
|
||||||
action = Const.Value.UNINSTALL,
|
|
||||||
dismissId = id
|
|
||||||
).let { BaseUIActivity.postDirections(it) }
|
|
||||||
|
|
||||||
/* Installing is understood as flashing modules / zips */
|
/* Installing is understood as flashing modules / zips */
|
||||||
|
|
||||||
fun installIntent(context: Context, file: Uri, id: Int = -1) = args(
|
fun installIntent(context: Context, file: Uri, id: Int = -1) = FlashFragmentArgs(
|
||||||
installer = file,
|
|
||||||
action = Const.Value.FLASH_ZIP,
|
action = Const.Value.FLASH_ZIP,
|
||||||
|
additionalData = file,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { createIntent(context, it) }
|
).let { createIntent(context, it) }
|
||||||
|
|
||||||
fun install(file: Uri, id: Int) = toFlash(
|
fun install(file: Uri, id: Int) = MainDirections.actionFlashFragment(
|
||||||
installer = file,
|
|
||||||
action = Const.Value.FLASH_ZIP,
|
action = Const.Value.FLASH_ZIP,
|
||||||
|
additionalData = file,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { BaseUIActivity.postDirections(it) }
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.topjohnwu.magisk.ui.flash
|
package com.topjohnwu.magisk.ui.flash
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
@@ -11,33 +11,29 @@ import com.topjohnwu.magisk.arch.BaseViewModel
|
|||||||
import com.topjohnwu.magisk.arch.diffListOf
|
import com.topjohnwu.magisk.arch.diffListOf
|
||||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.tasks.FlashZip
|
import com.topjohnwu.magisk.core.tasks.FlashZip
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.databinding.RvBindingAdapter
|
import com.topjohnwu.magisk.databinding.RvBindingAdapter
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.ktx.*
|
import com.topjohnwu.magisk.ktx.*
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class FlashViewModel(
|
class FlashViewModel(private val args: FlashFragmentArgs) : BaseViewModel() {
|
||||||
args: FlashFragmentArgs,
|
|
||||||
private val resources: Resources
|
|
||||||
) : BaseViewModel() {
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var showReboot = Shell.rootAccess()
|
var showReboot = Shell.rootAccess()
|
||||||
set(value) = set(value, field, { field = it }, BR.showReboot)
|
set(value) = set(value, field, { field = it }, BR.showReboot)
|
||||||
|
|
||||||
@get:Bindable
|
private val _subtitle = MutableLiveData(R.string.flashing)
|
||||||
var behaviorText = resources.getString(R.string.flashing)
|
val subtitle get() = _subtitle as LiveData<Int>
|
||||||
set(value) = set(value, field, { field = it }, BR.behaviorText)
|
|
||||||
|
|
||||||
val adapter = RvBindingAdapter<ConsoleItem>()
|
val adapter = RvBindingAdapter<ConsoleItem>()
|
||||||
val items = diffListOf<ConsoleItem>()
|
val items = diffListOf<ConsoleItem>()
|
||||||
@@ -52,34 +48,33 @@ class FlashViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
fun startFlashing() {
|
||||||
args.dismissId.takeIf { it != -1 }?.also {
|
val (action, uri, id) = args
|
||||||
Notifications.mgr.cancel(it)
|
if (id != -1)
|
||||||
}
|
Notifications.mgr.cancel(id)
|
||||||
val (installer, action, uri) = args
|
|
||||||
startFlashing(installer, uri, action)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startFlashing(installer: Uri, uri: Uri?, action: String) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = when (action) {
|
val result = when (action) {
|
||||||
Const.Value.FLASH_ZIP -> {
|
Const.Value.FLASH_ZIP -> {
|
||||||
FlashZip(installer, outItems, logItems).exec()
|
FlashZip(uri!!, outItems, logItems).exec()
|
||||||
}
|
}
|
||||||
Const.Value.UNINSTALL -> {
|
Const.Value.UNINSTALL -> {
|
||||||
showReboot = false
|
showReboot = false
|
||||||
FlashZip.Uninstall(installer, outItems, logItems).exec()
|
MagiskInstaller.Uninstall(outItems, logItems).exec()
|
||||||
}
|
}
|
||||||
Const.Value.FLASH_MAGISK -> {
|
Const.Value.FLASH_MAGISK -> {
|
||||||
MagiskInstaller.Direct(installer, outItems, logItems).exec()
|
if (Info.isEmulator)
|
||||||
|
MagiskInstaller.Emulator(outItems, logItems).exec()
|
||||||
|
else
|
||||||
|
MagiskInstaller.Direct(outItems, logItems).exec()
|
||||||
}
|
}
|
||||||
Const.Value.FLASH_INACTIVE_SLOT -> {
|
Const.Value.FLASH_INACTIVE_SLOT -> {
|
||||||
MagiskInstaller.SecondSlot(installer, outItems, logItems).exec()
|
MagiskInstaller.SecondSlot(outItems, logItems).exec()
|
||||||
}
|
}
|
||||||
Const.Value.PATCH_FILE -> {
|
Const.Value.PATCH_FILE -> {
|
||||||
uri ?: return@launch
|
uri ?: return@launch
|
||||||
showReboot = false
|
showReboot = false
|
||||||
MagiskInstaller.Patch(installer, uri, outItems, logItems).exec()
|
MagiskInstaller.Patch(uri, outItems, logItems).exec()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
back()
|
back()
|
||||||
@@ -92,9 +87,9 @@ class FlashViewModel(
|
|||||||
|
|
||||||
private fun onResult(success: Boolean) {
|
private fun onResult(success: Boolean) {
|
||||||
state = if (success) State.LOADED else State.LOADING_FAILED
|
state = if (success) State.LOADED else State.LOADING_FAILED
|
||||||
behaviorText = when {
|
when {
|
||||||
success -> resources.getString(R.string.done)
|
success -> _subtitle.postValue(R.string.done)
|
||||||
else -> resources.getString(R.string.failure)
|
else -> _subtitle.postValue(R.string.failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,10 +101,9 @@ class FlashViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun savePressed() = withExternalRW {
|
private fun savePressed() = withExternalRW {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
val name = "magisk_install_log_%s.log".format(now.toTime(timeFormatStandard))
|
||||||
val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
|
val file = MediaStoreUtils.getFile(name, true)
|
||||||
val file = MediaStoreUtils.getFile(name)
|
|
||||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||||
logItems.forEach {
|
logItems.forEach {
|
||||||
writer.write(it)
|
writer.write(it)
|
||||||
@@ -119,7 +113,6 @@ class FlashViewModel(
|
|||||||
SnackbarEvent(file.toString()).publish()
|
SnackbarEvent(file.toString()).publish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun restartPressed() = reboot()
|
fun restartPressed() = reboot()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.hide
|
|
||||||
|
|
||||||
import android.content.pm.ApplicationInfo
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
|
||||||
import com.topjohnwu.magisk.ktx.getLabel
|
|
||||||
|
|
||||||
class HideTarget(line: String) {
|
|
||||||
val packageName: String
|
|
||||||
val process: String
|
|
||||||
|
|
||||||
init {
|
|
||||||
val split = line.split(Regex("\\|"), 2)
|
|
||||||
packageName = split[0]
|
|
||||||
process = split.getOrElse(1) { packageName }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HideAppInfo(info: ApplicationInfo, pm: PackageManager)
|
|
||||||
: ApplicationInfo(info), Comparable<HideAppInfo> {
|
|
||||||
|
|
||||||
val label = info.getLabel(pm)
|
|
||||||
val iconImage: Drawable = info.loadIcon(pm)
|
|
||||||
|
|
||||||
override fun compareTo(other: HideAppInfo) = comparator.compare(this, other)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val comparator = compareBy<HideAppInfo>(
|
|
||||||
{ it.label.toLowerCase(currentLocale) },
|
|
||||||
{ it.packageName }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HideProcessInfo(
|
|
||||||
val name: String,
|
|
||||||
val packageName: String,
|
|
||||||
val isHidden: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
class HideAppTarget(
|
|
||||||
val info: HideAppInfo,
|
|
||||||
val processes: List<HideProcessInfo>
|
|
||||||
) : Comparable<HideAppTarget> {
|
|
||||||
override fun compareTo(other: HideAppTarget) = compareValuesBy(this, other) { it.info }
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
|
||||||
|
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||||
|
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||||
|
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
import com.topjohnwu.magisk.ktx.hideKeyboard
|
||||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
@@ -49,6 +52,21 @@ class HideFragment : BaseUIFragment<HideViewModel, FragmentHideMd2Binding>() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
val resource = requireContext().resources
|
||||||
|
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||||
|
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||||
|
binding.hideContent.addVerticalPadding(
|
||||||
|
l_50,
|
||||||
|
l1 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
|
||||||
|
)
|
||||||
|
binding.hideContent.addSimpleItemDecoration(
|
||||||
|
left = l1,
|
||||||
|
top = l_50,
|
||||||
|
right = l1,
|
||||||
|
bottom = l_50,
|
||||||
|
)
|
||||||
|
binding.hideContent.fixEdgeEffect()
|
||||||
|
|
||||||
val lama = binding.hideContent.layoutManager ?: return
|
val lama = binding.hideContent.layoutManager ?: return
|
||||||
lama.isAutoMeasureEnabled = false
|
lama.isAutoMeasureEnabled = false
|
||||||
}
|
}
|
||||||
|
|||||||
107
app/src/main/java/com/topjohnwu/magisk/ui/hide/HideInfo.kt
Normal file
107
app/src/main/java/com/topjohnwu/magisk/ui/hide/HideInfo.kt
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.hide
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.ComponentInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.PackageManager.*
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
|
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||||
|
import com.topjohnwu.magisk.ktx.getLabel
|
||||||
|
import com.topjohnwu.magisk.ktx.isIsolated
|
||||||
|
import com.topjohnwu.magisk.ktx.useAppZygote
|
||||||
|
|
||||||
|
class CmdlineHiddenItem(line: String) {
|
||||||
|
val packageName: String
|
||||||
|
val process: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
val split = line.split(Regex("\\|"), 2)
|
||||||
|
packageName = split[0]
|
||||||
|
process = split.getOrElse(1) { packageName }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val ISOLATED_MAGIC = "isolated"
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
class HideAppInfo(info: ApplicationInfo, pm: PackageManager, hideList: List<CmdlineHiddenItem>)
|
||||||
|
: ApplicationInfo(info), Comparable<HideAppInfo> {
|
||||||
|
|
||||||
|
val label = info.getLabel(pm)
|
||||||
|
val iconImage: Drawable = info.loadIcon(pm)
|
||||||
|
val processes = fetchProcesses(pm, hideList)
|
||||||
|
|
||||||
|
override fun compareTo(other: HideAppInfo) = comparator.compare(this, other)
|
||||||
|
|
||||||
|
private fun fetchProcesses(
|
||||||
|
pm: PackageManager,
|
||||||
|
hideList: List<CmdlineHiddenItem>
|
||||||
|
): List<HideProcessInfo> {
|
||||||
|
// Fetch full PackageInfo
|
||||||
|
val baseFlag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES
|
||||||
|
val packageInfo = try {
|
||||||
|
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
||||||
|
pm.getPackageInfo(packageName, baseFlag or request)
|
||||||
|
} catch (e: NameNotFoundException) {
|
||||||
|
// EdXposed hooked, issue#3276
|
||||||
|
return emptyList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Exceed binder data transfer limit, fetch each component type separately
|
||||||
|
pm.getPackageInfo(packageName, baseFlag).apply {
|
||||||
|
runCatching { activities = pm.getPackageInfo(packageName, baseFlag or GET_ACTIVITIES).activities }
|
||||||
|
runCatching { services = pm.getPackageInfo(packageName, baseFlag or GET_SERVICES).services }
|
||||||
|
runCatching { receivers = pm.getPackageInfo(packageName, baseFlag or GET_RECEIVERS).receivers }
|
||||||
|
runCatching { providers = pm.getPackageInfo(packageName, baseFlag or GET_PROVIDERS).providers }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hidden = hideList.filter { it.packageName == packageName || it.packageName == ISOLATED_MAGIC }
|
||||||
|
fun createProcess(name: String, pkg: String = packageName): HideProcessInfo {
|
||||||
|
return HideProcessInfo(name, pkg, hidden.any { it.process == name && it.packageName == pkg })
|
||||||
|
}
|
||||||
|
|
||||||
|
var haveAppZygote = false
|
||||||
|
fun Array<out ComponentInfo>.processes() = map { createProcess(it.processName) }
|
||||||
|
fun Array<ServiceInfo>.processes() = map {
|
||||||
|
if (it.isIsolated) {
|
||||||
|
if (it.useAppZygote) {
|
||||||
|
haveAppZygote = true
|
||||||
|
// Using app zygote, don't need to track the process
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val proc = if (SDK_INT >= 29) "${it.processName}:${it.name}" else it.processName
|
||||||
|
createProcess(proc, ISOLATED_MAGIC)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createProcess(it.processName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return with(packageInfo) {
|
||||||
|
activities?.processes().orEmpty() +
|
||||||
|
services?.processes().orEmpty() +
|
||||||
|
receivers?.processes().orEmpty() +
|
||||||
|
providers?.processes().orEmpty() +
|
||||||
|
listOf(if (haveAppZygote) createProcess("${processName}_zygote") else null)
|
||||||
|
}.filterNotNull().distinct().sortedBy { it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val comparator = compareBy<HideAppInfo>(
|
||||||
|
{ it.label.toLowerCase(currentLocale) },
|
||||||
|
{ it.packageName }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HideProcessInfo(
|
||||||
|
val name: String,
|
||||||
|
val packageName: String,
|
||||||
|
var isHidden: Boolean
|
||||||
|
) {
|
||||||
|
val isIsolated get() = packageName == ISOLATED_MAGIC
|
||||||
|
val isAppZygote get() = name.endsWith("_zygote")
|
||||||
|
}
|
||||||
@@ -12,14 +12,13 @@ import com.topjohnwu.magisk.utils.set
|
|||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class HideItem(
|
class HideRvItem(
|
||||||
app: HideAppTarget
|
val info: HideAppInfo
|
||||||
) : ObservableItem<HideItem>(), Comparable<HideItem> {
|
) : ObservableItem<HideRvItem>(), Comparable<HideRvItem> {
|
||||||
|
|
||||||
override val layoutRes = R.layout.item_hide_md2
|
override val layoutRes get() = R.layout.item_hide_md2
|
||||||
|
|
||||||
val info = app.info
|
val processes = info.processes.map { HideProcessRvItem(it) }
|
||||||
val processes = app.processes.map { HideProcessItem(it) }
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var isExpanded = false
|
var isExpanded = false
|
||||||
@@ -41,11 +40,10 @@ class HideItem(
|
|||||||
if (value == true) {
|
if (value == true) {
|
||||||
processes
|
processes
|
||||||
.filterNot { it.isHidden }
|
.filterNot { it.isHidden }
|
||||||
.filter { isExpanded || it.process.name == it.process.packageName }
|
.filter { isExpanded || it.defaultSelection }
|
||||||
} else {
|
} else {
|
||||||
processes
|
processes
|
||||||
.filter { it.isHidden }
|
.filter { it.isHidden }
|
||||||
.filter { isExpanded || it.process.name == it.process.packageName }
|
|
||||||
}.forEach { it.toggle() }
|
}.forEach { it.toggle() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,14 +67,19 @@ class HideItem(
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
processes.find { it.isHidden && it.process.name == it.process.packageName } != null
|
val defaultProcesses = processes.filter { it.defaultSelection }
|
||||||
|
when (defaultProcesses.count { it.isHidden }) {
|
||||||
|
0 -> false
|
||||||
|
defaultProcesses.size -> true
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun compareTo(other: HideItem) = comparator.compare(this, other)
|
override fun compareTo(other: HideRvItem) = comparator.compare(this, other)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val comparator = compareBy<HideItem>(
|
private val comparator = compareBy<HideRvItem>(
|
||||||
{ it.itemsChecked == 0 },
|
{ it.itemsChecked == 0 },
|
||||||
{ it.info }
|
{ it.info }
|
||||||
)
|
)
|
||||||
@@ -84,25 +87,34 @@ class HideItem(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HideProcessItem(
|
class HideProcessRvItem(
|
||||||
val process: HideProcessInfo
|
val process: HideProcessInfo
|
||||||
) : ObservableItem<HideProcessItem>() {
|
) : ObservableItem<HideProcessRvItem>() {
|
||||||
|
|
||||||
override val layoutRes = R.layout.item_hide_process_md2
|
override val layoutRes get() = R.layout.item_hide_process_md2
|
||||||
|
|
||||||
|
val displayName = if (process.isIsolated) "(isolated) ${process.name}" else process.name
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var isHidden = process.isHidden
|
var isHidden
|
||||||
set(value) = set(value, field, { field = it }, BR.hidden) {
|
get() = process.isHidden
|
||||||
val arg = if (isHidden) "add" else "rm"
|
set(value) = set(value, process.isHidden, { process.isHidden = it }, BR.hidden) {
|
||||||
|
val arg = if (it) "add" else "rm"
|
||||||
val (name, pkg) = process
|
val (name, pkg) = process
|
||||||
Shell.su("magiskhide --$arg $pkg $name").submit()
|
Shell.su("magiskhide $arg $pkg \'$name\'").submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggle() {
|
fun toggle() {
|
||||||
isHidden = !isHidden
|
isHidden = !isHidden
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun contentSameAs(other: HideProcessItem) = process == other.process
|
val defaultSelection get() =
|
||||||
override fun itemSameAs(other: HideProcessItem) = process.name == other.process.name
|
process.isIsolated || process.isAppZygote || process.name == process.packageName
|
||||||
|
|
||||||
|
override fun contentSameAs(other: HideProcessRvItem) =
|
||||||
|
process.isHidden == other.process.isHidden
|
||||||
|
|
||||||
|
override fun itemSameAs(other: HideProcessRvItem) =
|
||||||
|
process.name == other.process.name && process.packageName == other.process.packageName
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.topjohnwu.magisk.ui.hide
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
import android.os.Process
|
import android.os.Process
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -14,7 +15,6 @@ import com.topjohnwu.magisk.arch.itemBindingOf
|
|||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.magisk.ktx.packageName
|
import com.topjohnwu.magisk.ktx.packageName
|
||||||
import com.topjohnwu.magisk.ktx.processes
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@@ -45,11 +45,11 @@ class HideViewModel : BaseViewModel(), Queryable {
|
|||||||
submitQuery()
|
submitQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = filterableListOf<HideItem>()
|
val items = filterableListOf<HideRvItem>()
|
||||||
val itemBinding = itemBindingOf<HideItem> {
|
val itemBinding = itemBindingOf<HideRvItem> {
|
||||||
it.bindExtra(BR.viewModel, this)
|
it.bindExtra(BR.viewModel, this)
|
||||||
}
|
}
|
||||||
val itemInternalBinding = itemBindingOf<HideProcessItem> {
|
val itemInternalBinding = itemBindingOf<HideProcessRvItem> {
|
||||||
it.bindExtra(BR.viewModel, this)
|
it.bindExtra(BR.viewModel, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,14 +62,14 @@ class HideViewModel : BaseViewModel(), Queryable {
|
|||||||
state = State.LOADING
|
state = State.LOADING
|
||||||
val (apps, diff) = withContext(Dispatchers.Default) {
|
val (apps, diff) = withContext(Dispatchers.Default) {
|
||||||
val pm = get<PackageManager>()
|
val pm = get<PackageManager>()
|
||||||
val hides = Shell.su("magiskhide --ls").exec().out.map { HideTarget(it) }
|
val hideList = Shell.su("magiskhide ls").exec().out.map { CmdlineHiddenItem(it) }
|
||||||
val apps = pm.getInstalledApplications(PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES)
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
.filterNot { blacklist.contains(it.packageName) }
|
||||||
.map { HideAppInfo(it, pm) }
|
.map { HideAppInfo(it, pm, hideList) }
|
||||||
.map { createTarget(it, hides) }
|
|
||||||
.filter { it.processes.isNotEmpty() }
|
.filter { it.processes.isNotEmpty() }
|
||||||
.map { HideItem(it) }
|
.filter { info -> info.enabled || info.processes.any { it.isHidden } }
|
||||||
|
.map { HideRvItem(it) }
|
||||||
.toList()
|
.toList()
|
||||||
.sorted()
|
.sorted()
|
||||||
apps to items.calculateDiff(apps)
|
apps to items.calculateDiff(apps)
|
||||||
@@ -80,18 +80,6 @@ class HideViewModel : BaseViewModel(), Queryable {
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
private fun createTarget(info: HideAppInfo, hideList: List<HideTarget>): HideAppTarget {
|
|
||||||
val pkg = info.packageName
|
|
||||||
val hidden = hideList.filter { it.packageName == pkg }
|
|
||||||
val processNames = info.processes.distinct()
|
|
||||||
val processes = processNames.map { name ->
|
|
||||||
HideProcessInfo(name, pkg, hidden.any { name == it.process })
|
|
||||||
}
|
|
||||||
return HideAppTarget(info, processes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
override fun query() {
|
override fun query() {
|
||||||
items.filter {
|
items.filter {
|
||||||
fun showHidden() = it.itemsChecked != 0
|
fun showHidden() = it.itemsChecked != 0
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.topjohnwu.magisk.ui.home
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||||
import com.topjohnwu.magisk.core.download.BaseDownloader
|
import com.topjohnwu.magisk.core.download.BaseDownloader
|
||||||
@@ -22,20 +24,32 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
|
|||||||
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
|
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkTitle(text: TextView, icon: ImageView) {
|
||||||
|
text.post {
|
||||||
|
if (text.layout.getEllipsisCount(0) != 0) {
|
||||||
|
with (icon) {
|
||||||
|
layoutParams.width = 0
|
||||||
|
layoutParams.height = 0
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
// Set barrier reference IDs in code, since resource IDs will be stripped in release mode
|
// If titles are squished, hide icons
|
||||||
binding.homeMagiskWrapper.homeMagiskTitleBarrier.referencedIds =
|
with(binding.homeMagiskWrapper) {
|
||||||
intArrayOf(R.id.home_magisk_button, R.id.home_magisk_title, R.id.home_magisk_icon)
|
checkTitle(homeMagiskTitle, homeMagiskIcon)
|
||||||
binding.homeMagiskWrapper.homeMagiskBarrier.referencedIds =
|
}
|
||||||
intArrayOf(R.id.home_magisk_latest_version, R.id.home_magisk_installed_version)
|
with(binding.homeManagerWrapper) {
|
||||||
binding.homeManagerWrapper.homeManagerTitleBarrier.referencedIds =
|
checkTitle(homeManagerTitle, homeManagerIcon)
|
||||||
intArrayOf(R.id.home_manager_button, R.id.home_manager_title, R.id.home_manager_icon)
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
@@ -46,11 +60,14 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
|
|||||||
menu.removeItem(R.id.action_reboot)
|
menu.removeItem(R.id.action_reboot)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
R.id.action_settings -> HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
|
when (item.itemId) {
|
||||||
.navigate()
|
R.id.action_settings ->
|
||||||
|
HomeFragmentDirections.actionHomeFragmentToSettingsFragment().navigate()
|
||||||
R.id.action_reboot -> RebootEvent.inflateMenu(activity).show()
|
R.id.action_reboot -> RebootEvent.inflateMenu(activity).show()
|
||||||
else -> null
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}?.let { true } ?: super.onOptionsItemSelected(item)
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.ui.home
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
@@ -10,8 +9,6 @@ import com.topjohnwu.magisk.core.Config
|
|||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.download.Subject.Manager
|
import com.topjohnwu.magisk.core.download.Subject.Manager
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
|
||||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.events.OpenInappLinkEvent
|
import com.topjohnwu.magisk.events.OpenInappLinkEvent
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
@@ -19,8 +16,7 @@ import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
|||||||
import com.topjohnwu.magisk.events.dialog.ManagerInstallDialog
|
import com.topjohnwu.magisk.events.dialog.ManagerInstallDialog
|
||||||
import com.topjohnwu.magisk.events.dialog.UninstallDialog
|
import com.topjohnwu.magisk.events.dialog.UninstallDialog
|
||||||
import com.topjohnwu.magisk.ktx.await
|
import com.topjohnwu.magisk.ktx.await
|
||||||
import com.topjohnwu.magisk.ktx.packageName
|
import com.topjohnwu.magisk.utils.asText
|
||||||
import com.topjohnwu.magisk.ktx.res
|
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -35,42 +31,46 @@ class HomeViewModel(
|
|||||||
private val svc: NetworkService
|
private val svc: NetworkService
|
||||||
) : BaseViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
val magiskTitleBarrierIds =
|
||||||
|
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
|
||||||
|
val magiskDetailBarrierIds =
|
||||||
|
intArrayOf(R.id.home_magisk_installed_version, R.id.home_device_details_ramdisk)
|
||||||
|
val appTitleBarrierIds =
|
||||||
|
intArrayOf(R.id.home_manager_icon, R.id.home_manager_title, R.id.home_manager_button)
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var isNoticeVisible = Config.safetyNotice
|
var isNoticeVisible = Config.safetyNotice
|
||||||
set(value) = set(value, field, { field = it }, BR.noticeVisible)
|
set(value) = set(value, field, { field = it }, BR.noticeVisible)
|
||||||
|
|
||||||
@get:Bindable
|
val stateMagisk = when {
|
||||||
var stateMagisk = MagiskState.LOADING
|
!Info.env.isActive -> MagiskState.NOT_INSTALLED
|
||||||
set(value) = set(value, field, { field = it }, BR.stateMagisk, BR.showUninstall)
|
Info.env.magiskVersionCode < BuildConfig.VERSION_CODE -> MagiskState.OBSOLETE
|
||||||
|
else -> MagiskState.UP_TO_DATE
|
||||||
|
}
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var stateManager = MagiskState.LOADING
|
var stateManager = MagiskState.LOADING
|
||||||
set(value) = set(value, field, { field = it }, BR.stateManager)
|
set(value) = set(value, field, { field = it }, BR.stateManager)
|
||||||
|
|
||||||
@get:Bindable
|
val magiskInstalledVersion get() = Info.env.run {
|
||||||
var magiskRemoteVersion = R.string.loading.res()
|
if (isActive)
|
||||||
set(value) = set(value, field, { field = it }, BR.magiskRemoteVersion)
|
"$magiskVersionString ($magiskVersionCode)".asText()
|
||||||
|
else
|
||||||
val magiskInstalledVersion get() =
|
R.string.not_available.asText()
|
||||||
"${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})"
|
}
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var managerRemoteVersion = R.string.loading.res()
|
var managerRemoteVersion = R.string.loading.asText()
|
||||||
set(value) = set(value, field, { field = it }, BR.managerRemoteVersion)
|
set(value) = set(value, field, { field = it }, BR.managerRemoteVersion)
|
||||||
|
|
||||||
val managerInstalledVersion = Info.stub?.let {
|
val managerInstalledVersion = Info.stub?.let {
|
||||||
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) (${it.version})"
|
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) (${it.version})"
|
||||||
} ?: "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
} ?: "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
|
||||||
val statePackageName = packageName
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var stateManagerProgress = 0
|
var stateManagerProgress = 0
|
||||||
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
|
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
val showUninstall get() =
|
|
||||||
Info.env.magiskVersionCode > 0 && stateMagisk != MagiskState.LOADING && isConnected.get()
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
val showSafetyNet get() = Info.hasGMS && isConnected.get()
|
val showSafetyNet get() = Info.hasGMS && isConnected.get()
|
||||||
|
|
||||||
@@ -82,31 +82,25 @@ class HomeViewModel(
|
|||||||
|
|
||||||
override fun refresh() = viewModelScope.launch {
|
override fun refresh() = viewModelScope.launch {
|
||||||
state = State.LOADING
|
state = State.LOADING
|
||||||
notifyPropertyChanged(BR.showUninstall)
|
|
||||||
notifyPropertyChanged(BR.showSafetyNet)
|
notifyPropertyChanged(BR.showSafetyNet)
|
||||||
svc.fetchUpdate()?.apply {
|
Info.getRemote(svc)?.apply {
|
||||||
state = State.LOADED
|
state = State.LOADED
|
||||||
stateMagisk = when {
|
|
||||||
!Info.env.isActive -> MagiskState.NOT_INSTALLED
|
|
||||||
magisk.isObsolete -> MagiskState.OBSOLETE
|
|
||||||
else -> MagiskState.UP_TO_DATE
|
|
||||||
}
|
|
||||||
|
|
||||||
stateManager = when {
|
stateManager = when {
|
||||||
!app.isUpdateChannelCorrect && isConnected.get() -> MagiskState.NOT_INSTALLED
|
BuildConfig.VERSION_CODE < magisk.versionCode -> MagiskState.OBSOLETE
|
||||||
app.isObsolete -> MagiskState.OBSOLETE
|
|
||||||
else -> MagiskState.UP_TO_DATE
|
else -> MagiskState.UP_TO_DATE
|
||||||
}
|
}
|
||||||
|
|
||||||
magiskRemoteVersion =
|
|
||||||
"${magisk.version} (${magisk.versionCode})"
|
|
||||||
managerRemoteVersion =
|
managerRemoteVersion =
|
||||||
"${app.version} (${app.versionCode}) (${stub.versionCode})"
|
"${magisk.version} (${magisk.versionCode}) (${stub.versionCode})".asText()
|
||||||
|
|
||||||
launch {
|
launch {
|
||||||
ensureEnv()
|
ensureEnv()
|
||||||
}
|
}
|
||||||
} ?: apply { state = State.LOADING_FAILED }
|
} ?: {
|
||||||
|
state = State.LOADING_FAILED
|
||||||
|
managerRemoteVersion = R.string.not_available.asText()
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
val showTest = false
|
val showTest = false
|
||||||
@@ -118,27 +112,26 @@ class HomeViewModel(
|
|||||||
}.publish()
|
}.publish()
|
||||||
|
|
||||||
fun onProgressUpdate(progress: Float, subject: Subject) {
|
fun onProgressUpdate(progress: Float, subject: Subject) {
|
||||||
when (subject) {
|
if (subject is Manager)
|
||||||
is Manager -> stateManagerProgress = progress.times(100f).roundToInt()
|
stateManagerProgress = progress.times(100f).roundToInt()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish()
|
fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish()
|
||||||
|
|
||||||
fun onDeletePressed() = UninstallDialog().publish()
|
fun onDeletePressed() = UninstallDialog().publish()
|
||||||
|
|
||||||
fun onManagerPressed() =
|
fun onManagerPressed() = when (state) {
|
||||||
if (isConnected.get()) ManagerInstallDialog().publish()
|
State.LOADED -> withExternalRW { ManagerInstallDialog().publish() }
|
||||||
else SnackbarEvent(R.string.no_connection).publish()
|
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
||||||
|
else -> SnackbarEvent(R.string.no_connection).publish()
|
||||||
|
}
|
||||||
|
|
||||||
fun onMagiskPressed() = if (isConnected.get()) withExternalRW {
|
fun onMagiskPressed() = withExternalRW {
|
||||||
HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish()
|
HomeFragmentDirections.actionHomeFragmentToInstallFragment().navigate()
|
||||||
} else {
|
|
||||||
SnackbarEvent(R.string.no_connection).publish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSafetyNetPressed() =
|
fun onSafetyNetPressed() =
|
||||||
HomeFragmentDirections.actionHomeFragmentToSafetynetFragment().publish()
|
HomeFragmentDirections.actionHomeFragmentToSafetynetFragment().navigate()
|
||||||
|
|
||||||
fun hideNotice() {
|
fun hideNotice() {
|
||||||
Config.safetyNotice = false
|
Config.safetyNotice = false
|
||||||
@@ -150,17 +143,7 @@ class HomeViewModel(
|
|||||||
MagiskState.NOT_INSTALLED,
|
MagiskState.NOT_INSTALLED,
|
||||||
MagiskState.LOADING
|
MagiskState.LOADING
|
||||||
)
|
)
|
||||||
|
if (invalidStates.any { it == stateMagisk } || shownDialog) return
|
||||||
// Don't bother checking env when magisk is not installed, loading or already has been shown
|
|
||||||
if (
|
|
||||||
invalidStates.any { it == stateMagisk } ||
|
|
||||||
shownDialog ||
|
|
||||||
// don't care for emulators either
|
|
||||||
Build.DEVICE.orEmpty().contains("generic") ||
|
|
||||||
Build.PRODUCT.orEmpty().contains("generic")
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = Shell.su("env_check").await()
|
val result = Shell.su("env_check").await()
|
||||||
if (!result.isSuccess) {
|
if (!result.isSuccess) {
|
||||||
@@ -169,11 +152,4 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val MagiskJson.isObsolete
|
|
||||||
get() = Info.env.isActive && Info.env.magiskVersionCode < versionCode
|
|
||||||
private val ManagerJson.isUpdateChannelCorrect
|
|
||||||
get() = versionCode > 0
|
|
||||||
private val ManagerJson.isObsolete
|
|
||||||
get() = BuildConfig.VERSION_CODE < versionCode
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.topjohnwu.magisk.ui.inflater
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.InflateException
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.collection.SimpleArrayMap
|
||||||
|
import java.lang.reflect.Constructor
|
||||||
|
|
||||||
|
open class LayoutInflaterFactory(private val delegate: AppCompatDelegate) : LayoutInflater.Factory2 {
|
||||||
|
|
||||||
|
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||||
|
return onCreateView(null, name, context, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
|
||||||
|
val view = delegate.createView(parent, name, context, attrs)
|
||||||
|
?: LayoutInflaterFactoryDefaultImpl.createViewFromTag(context, name, attrs)
|
||||||
|
onViewCreated(view, parent, name, context, attrs)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onViewCreated(view: View?, parent: View?, name: String, context: Context, attrs: AttributeSet) {
|
||||||
|
if (view == null) return
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
WindowInsetsHelper.attach(view, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object LayoutInflaterFactoryDefaultImpl {
|
||||||
|
|
||||||
|
private val constructorSignature = arrayOf(
|
||||||
|
Context::class.java, AttributeSet::class.java)
|
||||||
|
|
||||||
|
private val classPrefixList = arrayOf(
|
||||||
|
"android.widget.",
|
||||||
|
"android.view.",
|
||||||
|
"android.webkit."
|
||||||
|
)
|
||||||
|
|
||||||
|
private val constructorMap = SimpleArrayMap<String, Constructor<out View?>>()
|
||||||
|
|
||||||
|
fun createViewFromTag(context: Context, name: String, attrs: AttributeSet): View? {
|
||||||
|
var name = name
|
||||||
|
if (name == "view") {
|
||||||
|
name = attrs.getAttributeValue(null, "class")
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
if (-1 == name.indexOf('.')) {
|
||||||
|
for (prefix in classPrefixList) {
|
||||||
|
val view: View? = createViewByPrefix(context, name, attrs, prefix)
|
||||||
|
if (view != null) {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
createViewByPrefix(context, name, attrs, null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(ClassNotFoundException::class, InflateException::class)
|
||||||
|
private fun createViewByPrefix(context: Context, name: String, attrs: AttributeSet, prefix: String?): View? {
|
||||||
|
var constructor = constructorMap[name]
|
||||||
|
return try {
|
||||||
|
if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it
|
||||||
|
val clazz = Class.forName(
|
||||||
|
if (prefix != null) prefix + name else name,
|
||||||
|
false,
|
||||||
|
context.classLoader).asSubclass(View::class.java)
|
||||||
|
constructor = clazz.getConstructor(*constructorSignature)
|
||||||
|
constructorMap.put(name, constructor)
|
||||||
|
}
|
||||||
|
constructor!!.isAccessible = true
|
||||||
|
constructor.newInstance(context, attrs)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
@file:Suppress("unused")
|
||||||
|
|
||||||
|
package com.topjohnwu.magisk.ui.inflater
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity.*
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.Insets
|
||||||
|
import androidx.core.view.OnApplyWindowInsetsListener
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
|
|
||||||
|
private typealias ApplyInsetsCallback<T> = (insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) -> T
|
||||||
|
|
||||||
|
private class ApplyInsets(private val out: Rect) : ApplyInsetsCallback<Unit> {
|
||||||
|
|
||||||
|
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) {
|
||||||
|
out.left += if (left) insets.left else 0
|
||||||
|
out.top += if (top) insets.top else 0
|
||||||
|
out.right += if (right) insets.right else 0
|
||||||
|
out.bottom += if (bottom) insets.bottom else 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ConsumeInsets : ApplyInsetsCallback<Insets> {
|
||||||
|
|
||||||
|
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean): Insets {
|
||||||
|
val insetsLeft = if (left) 0 else insets.left
|
||||||
|
val insetsTop = if (top) 0 else insets.top
|
||||||
|
val insetsRight = if (right) 0 else insets.right
|
||||||
|
val insetsBottom = if (bottom) 0 else insets.bottom
|
||||||
|
return Insets.of(insetsLeft, insetsTop, insetsRight, insetsBottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
open class WindowInsetsHelper private constructor(
|
||||||
|
private val view: View,
|
||||||
|
private val fitSystemWindows: Int,
|
||||||
|
private val layout_fitsSystemWindowsInsets: Int,
|
||||||
|
private val consumeSystemWindows: Int) : OnApplyWindowInsetsListener {
|
||||||
|
|
||||||
|
internal var initialPaddingLeft: Int = view.paddingLeft
|
||||||
|
internal var initialPaddingTop: Int = view.paddingTop
|
||||||
|
internal var initialPaddingRight: Int = view.paddingRight
|
||||||
|
internal var initialPaddingBottom: Int = view.paddingBottom
|
||||||
|
|
||||||
|
private var initialMargin = false
|
||||||
|
internal var initialMarginLeft: Int = 0
|
||||||
|
internal var initialMarginTop: Int = 0
|
||||||
|
internal var initialMarginRight: Int = 0
|
||||||
|
internal var initialMarginBottom: Int = 0
|
||||||
|
internal var initialMarginStart: Int = 0
|
||||||
|
internal var initialMarginEnd: Int = 0
|
||||||
|
|
||||||
|
private var lastInsets: WindowInsetsCompat? = null
|
||||||
|
|
||||||
|
open fun setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
initialPaddingLeft = left
|
||||||
|
initialPaddingTop = top
|
||||||
|
initialPaddingRight = right
|
||||||
|
initialPaddingBottom = bottom
|
||||||
|
|
||||||
|
lastInsets?.let { applyWindowInsets(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||||
|
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||||
|
if (isRTL) {
|
||||||
|
setInitialPadding(start, top, end, bottom)
|
||||||
|
} else {
|
||||||
|
setInitialPadding(start, top, end, bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
initialPaddingLeft = left
|
||||||
|
initialPaddingTop = top
|
||||||
|
initialPaddingRight = right
|
||||||
|
initialPaddingBottom = bottom
|
||||||
|
|
||||||
|
lastInsets?.let { applyWindowInsets(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||||
|
initialMarginStart = start
|
||||||
|
initialMarginTop = top
|
||||||
|
initialMarginEnd = end
|
||||||
|
initialMarginBottom = bottom
|
||||||
|
|
||||||
|
lastInsets?.let { applyWindowInsets(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RtlHardcoded")
|
||||||
|
private fun <T> applyInsets(insets: Insets, fit: Int, callback: ApplyInsetsCallback<T>): T {
|
||||||
|
val relativeMode = (fit and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION
|
||||||
|
|
||||||
|
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||||
|
|
||||||
|
val left: Boolean
|
||||||
|
val top = fit and TOP == TOP
|
||||||
|
val right: Boolean
|
||||||
|
val bottom = fit and BOTTOM == BOTTOM
|
||||||
|
|
||||||
|
if (relativeMode) {
|
||||||
|
val start = fit and START == START
|
||||||
|
val end = fit and END == END
|
||||||
|
left = (!isRTL && start) || (isRTL && end)
|
||||||
|
right = (!isRTL && end) || (isRTL && start)
|
||||||
|
} else {
|
||||||
|
left = fit and LEFT == LEFT
|
||||||
|
right = fit and RIGHT == RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback.invoke(insets, left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyWindowInsets(windowInsets: WindowInsetsCompat): WindowInsetsCompat {
|
||||||
|
if (fitSystemWindows != 0) {
|
||||||
|
val padding = Rect(initialPaddingLeft, initialPaddingTop, initialPaddingRight, initialPaddingBottom)
|
||||||
|
applyInsets(windowInsets.systemWindowInsets, fitSystemWindows, ApplyInsets(padding))
|
||||||
|
view.setPadding(padding.left, padding.top, padding.right, padding.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout_fitsSystemWindowsInsets != 0) {
|
||||||
|
if (!initialMargin) {
|
||||||
|
initialMarginLeft = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.leftMargin ?: 0
|
||||||
|
initialMarginTop = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin ?: 0
|
||||||
|
initialMarginRight = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.rightMargin ?: 0
|
||||||
|
initialMarginBottom = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin ?: 0
|
||||||
|
initialMarginStart = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart ?: 0
|
||||||
|
initialMarginEnd = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginEnd ?: 0
|
||||||
|
initialMargin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val margin = if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION)
|
||||||
|
Rect(initialMarginLeft, initialMarginTop, initialMarginRight, initialMarginBottom)
|
||||||
|
else
|
||||||
|
Rect(initialMarginStart, initialMarginTop, initialMarginEnd, initialMarginBottom)
|
||||||
|
|
||||||
|
applyInsets(windowInsets.systemWindowInsets, layout_fitsSystemWindowsInsets, ApplyInsets(margin))
|
||||||
|
|
||||||
|
val lp = view.layoutParams
|
||||||
|
if (lp is ViewGroup.MarginLayoutParams) {
|
||||||
|
lp.topMargin = margin.top
|
||||||
|
lp.bottomMargin = margin.bottom
|
||||||
|
|
||||||
|
if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION) {
|
||||||
|
lp.marginStart = margin.left
|
||||||
|
lp.marginEnd = margin.right
|
||||||
|
} else {
|
||||||
|
lp.leftMargin = margin.left
|
||||||
|
lp.rightMargin = margin.right
|
||||||
|
}
|
||||||
|
|
||||||
|
view.layoutParams = lp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val systemWindowInsets = if (consumeSystemWindows != 0) applyInsets(windowInsets.systemWindowInsets, consumeSystemWindows, ConsumeInsets()) else windowInsets.systemWindowInsets
|
||||||
|
|
||||||
|
return WindowInsetsCompat.Builder(windowInsets)
|
||||||
|
.setSystemWindowInsets(systemWindowInsets)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||||
|
if (lastInsets == insets) {
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
|
||||||
|
lastInsets = insets
|
||||||
|
|
||||||
|
return applyWindowInsets(insets)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun attach(view: View, attrs: AttributeSet) {
|
||||||
|
val a = view.context.obtainStyledAttributes(attrs, R.styleable.WindowInsetsHelper, 0, 0)
|
||||||
|
val edgeToEdge = a.getBoolean(R.styleable.WindowInsetsHelper_edgeToEdge, false)
|
||||||
|
val fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_fitsSystemWindowsInsets, 0)
|
||||||
|
val layout_fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_layout_fitsSystemWindowsInsets, 0)
|
||||||
|
val consumeSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_consumeSystemWindowsInsets, 0)
|
||||||
|
a.recycle()
|
||||||
|
|
||||||
|
attach(view, edgeToEdge, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun attach(view: View, edgeToEdge: Boolean, fitsSystemWindowsInsets: Int, layout_fitsSystemWindowsInsets: Int, consumeSystemWindowsInsets: Int) {
|
||||||
|
if (edgeToEdge) {
|
||||||
|
view.systemUiVisibility = (view.systemUiVisibility
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fitsSystemWindowsInsets == 0 && layout_fitsSystemWindowsInsets == 0 && consumeSystemWindowsInsets == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val listener = WindowInsetsHelper(view, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(view, listener)
|
||||||
|
view.setTag(R.id.tag_rikka_material_WindowInsetsHelper, listener)
|
||||||
|
|
||||||
|
if (!view.isAttachedToWindow) {
|
||||||
|
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||||
|
override fun onViewAttachedToWindow(v: View) {
|
||||||
|
v.removeOnAttachStateChangeListener(this)
|
||||||
|
v.requestApplyInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val View.windowInsetsHelper: WindowInsetsHelper?
|
||||||
|
get() {
|
||||||
|
val value = getTag(R.id.tag_rikka_material_WindowInsetsHelper)
|
||||||
|
return if (value is WindowInsetsHelper) value else null
|
||||||
|
}
|
||||||
|
|
||||||
|
val View.initialPaddingLeft: Int
|
||||||
|
get() = windowInsetsHelper?.initialPaddingLeft ?: 0
|
||||||
|
|
||||||
|
val View.initialPaddingTop: Int
|
||||||
|
get() = windowInsetsHelper?.initialPaddingTop ?: 0
|
||||||
|
|
||||||
|
val View.initialPaddingRight: Int
|
||||||
|
get() = windowInsetsHelper?.initialPaddingRight ?: 0
|
||||||
|
|
||||||
|
val View.initialPaddingBottom: Int
|
||||||
|
get() = windowInsetsHelper?.initialPaddingBottom ?: 0
|
||||||
|
|
||||||
|
val View.initialPaddingStart: Int
|
||||||
|
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingRight else initialPaddingLeft
|
||||||
|
|
||||||
|
val View.initialPaddingEnd: Int
|
||||||
|
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingLeft else initialPaddingRight
|
||||||
|
|
||||||
|
fun View.setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
windowInsetsHelper?.setInitialPadding(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||||
|
windowInsetsHelper?.setInitialPaddingRelative(start, top, end, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
val View.initialMarginLeft: Int
|
||||||
|
get() = windowInsetsHelper?.initialMarginLeft ?: 0
|
||||||
|
|
||||||
|
val View.initialMarginTop: Int
|
||||||
|
get() = windowInsetsHelper?.initialMarginTop ?: 0
|
||||||
|
|
||||||
|
val View.initialMarginRight: Int
|
||||||
|
get() = windowInsetsHelper?.initialMarginRight ?: 0
|
||||||
|
|
||||||
|
val View.initialMarginBottom: Int
|
||||||
|
get() = windowInsetsHelper?.initialMarginBottom ?: 0
|
||||||
|
|
||||||
|
val View.initialMarginStart: Int
|
||||||
|
get() = windowInsetsHelper?.initialMarginStart ?: 0
|
||||||
|
|
||||||
|
val View.initialMarginEnd: Int
|
||||||
|
get() = windowInsetsHelper?.initialMarginEnd ?: 0
|
||||||
|
|
||||||
|
fun View.setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
windowInsetsHelper?.setInitialMargin(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||||
|
windowInsetsHelper?.setInitialMarginRelative(start, top, end, bottom)
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import android.view.ViewGroup
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||||
import com.topjohnwu.magisk.core.download.BaseDownloader
|
|
||||||
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
|
||||||
import com.topjohnwu.magisk.ktx.coroutineScope
|
import com.topjohnwu.magisk.ktx.coroutineScope
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
@@ -23,7 +22,6 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
|
|||||||
|
|
||||||
// Allow markwon to run in viewmodel scope
|
// Allow markwon to run in viewmodel scope
|
||||||
binding.releaseNotes.coroutineScope = viewModel.viewModelScope
|
binding.releaseNotes.coroutineScope = viewModel.viewModelScope
|
||||||
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
package com.topjohnwu.magisk.ui.install
|
package com.topjohnwu.magisk.ui.install
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.Action
|
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.events.MagiskInstallFileEvent
|
import com.topjohnwu.magisk.events.MagiskInstallFileEvent
|
||||||
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
|
import com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
|
||||||
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.get
|
import org.koin.core.get
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
class InstallViewModel(
|
class InstallViewModel(
|
||||||
svc: NetworkService
|
svc: NetworkService
|
||||||
) : BaseViewModel(State.LOADED) {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val isRooted = Shell.rootAccess()
|
val isRooted = Shell.rootAccess()
|
||||||
val skipOptions = Info.ramdisk && !Info.isFDE && Info.isSAR
|
val skipOptions = Info.isEmulator || (Info.ramdisk && !Info.isFDE && Info.isSAR)
|
||||||
|
val noSecondSlot = !isRooted || Info.isPixel || Info.isVirtualAB || !Info.isAB || Info.isEmulator
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var step = if (skipOptions) 1 else 0
|
var step = if (skipOptions) 1 else 0
|
||||||
@@ -52,10 +54,6 @@ class InstallViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Bindable
|
|
||||||
var progress = 0
|
|
||||||
set(value) = set(value, field, { field = it }, BR.progress)
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var data: Uri? = null
|
var data: Uri? = null
|
||||||
set(value) = set(value, field, { field = it }, BR.data)
|
set(value) = set(value, field, { field = it }, BR.data)
|
||||||
@@ -67,41 +65,35 @@ class InstallViewModel(
|
|||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
notes = svc.fetchString(Info.remote.magisk.note)
|
val context = get<Context>()
|
||||||
|
File(context.cacheDir, "${BuildConfig.VERSION_CODE}.md").run {
|
||||||
|
notes = when {
|
||||||
|
exists() -> readText()
|
||||||
|
Const.Url.CHANGELOG_URL.isEmpty() -> ""
|
||||||
|
else -> {
|
||||||
|
val text = svc.fetchString(Const.Url.CHANGELOG_URL)
|
||||||
|
writeText(text)
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onProgressUpdate(progress: Float, subject: Subject) {
|
|
||||||
if (subject !is Subject.Magisk) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.progress = progress.times(100).roundToInt()
|
|
||||||
if (this.progress >= 100) {
|
|
||||||
state = State.LOADED
|
|
||||||
} else if (this.progress < -150) {
|
|
||||||
state = State.LOADING_FAILED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun step(nextStep: Int) {
|
fun step(nextStep: Int) {
|
||||||
step = nextStep
|
step = nextStep
|
||||||
}
|
}
|
||||||
|
|
||||||
fun install() {
|
fun install() {
|
||||||
DownloadService.start(get(), Subject.Magisk(resolveAction()))
|
when (method) {
|
||||||
state = State.LOADING
|
R.id.method_patch -> FlashFragment.patch(data!!).navigate()
|
||||||
}
|
R.id.method_direct -> FlashFragment.flash(false).navigate()
|
||||||
|
R.id.method_inactive_slot -> FlashFragment.flash(true).navigate()
|
||||||
// ---
|
|
||||||
|
|
||||||
private fun resolveAction() = when (method) {
|
|
||||||
R.id.method_download -> Action.Download
|
|
||||||
R.id.method_patch -> Action.Patch(data!!)
|
|
||||||
R.id.method_direct -> Action.Flash.Primary
|
|
||||||
R.id.method_inactive_slot -> Action.Flash.Secondary
|
|
||||||
else -> error("Unknown value")
|
else -> error("Unknown value")
|
||||||
}
|
}
|
||||||
|
state = State.LOADING
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import androidx.core.view.isVisible
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
|
||||||
|
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||||
|
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||||
|
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
import com.topjohnwu.magisk.ui.MainActivity
|
||||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
@@ -42,6 +45,21 @@ class LogFragment : BaseUIFragment<LogViewModel, FragmentLogMd2Binding>() {
|
|||||||
binding.logFilterToggle.setOnClickListener {
|
binding.logFilterToggle.setOnClickListener {
|
||||||
isMagiskLogVisible = true
|
isMagiskLogVisible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val resource = requireContext().resources
|
||||||
|
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||||
|
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||||
|
binding.logFilterSuperuser.logSuperuser.addVerticalPadding(
|
||||||
|
0,
|
||||||
|
l1
|
||||||
|
)
|
||||||
|
binding.logFilterSuperuser.logSuperuser.addSimpleItemDecoration(
|
||||||
|
left = l1,
|
||||||
|
top = l_50,
|
||||||
|
right = l1,
|
||||||
|
bottom = l_50,
|
||||||
|
)
|
||||||
|
binding.logFilterSuperuser.logSuperuser.fixEdgeEffect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,24 @@ package com.topjohnwu.magisk.ui.log
|
|||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
import com.topjohnwu.magisk.arch.diffListOf
|
import com.topjohnwu.magisk.arch.diffListOf
|
||||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||||
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.ktx.now
|
||||||
|
import com.topjohnwu.magisk.ktx.timeFormatStandard
|
||||||
|
import com.topjohnwu.magisk.ktx.toTime
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.magisk.view.TextItem
|
import com.topjohnwu.magisk.view.TextItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class LogViewModel(
|
class LogViewModel(
|
||||||
private val repo: LogRepository
|
private val repo: LogRepository
|
||||||
@@ -54,14 +58,23 @@ class LogViewModel(
|
|||||||
|
|
||||||
fun saveMagiskLog() = withExternalRW {
|
fun saveMagiskLog() = withExternalRW {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val now = Calendar.getInstance()
|
val filename = "magisk_log_%s.log".format(now.toTime(timeFormatStandard))
|
||||||
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
|
val logFile = MediaStoreUtils.getFile(filename, true)
|
||||||
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
logFile.uri.outputStream().bufferedWriter().use { file ->
|
||||||
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
file.write("---System Properties---\n\n")
|
||||||
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
|
|
||||||
)
|
ProcessBuilder("getprop").start()
|
||||||
val logFile = MediaStoreUtils.getFile(filename)
|
.inputStream.reader().use { it.copyTo(file) }
|
||||||
logFile.uri.outputStream().writer().use { it.write(consoleText) }
|
|
||||||
|
file.write("\n---Magisk Logs---\n")
|
||||||
|
file.write("${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})\n\n")
|
||||||
|
file.write(consoleText)
|
||||||
|
|
||||||
|
file.write("\n---Manager Logs---\n")
|
||||||
|
file.write("${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\n\n")
|
||||||
|
ProcessBuilder("logcat", "-d").start()
|
||||||
|
.inputStream.reader().use { it.copyTo(file) }
|
||||||
|
}
|
||||||
SnackbarEvent(logFile.toString()).publish()
|
SnackbarEvent(logFile.toString()).publish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import com.topjohnwu.magisk.arch.ReselectionTarget
|
|||||||
import com.topjohnwu.magisk.arch.ViewEvent
|
import com.topjohnwu.magisk.arch.ViewEvent
|
||||||
import com.topjohnwu.magisk.core.download.BaseDownloader
|
import com.topjohnwu.magisk.core.download.BaseDownloader
|
||||||
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
||||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
import com.topjohnwu.magisk.ktx.*
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
import com.topjohnwu.magisk.ui.MainActivity
|
||||||
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
|
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
|
||||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||||
@@ -62,6 +62,40 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
|
|||||||
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
|
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
val resource = requireContext().resources
|
||||||
|
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||||
|
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||||
|
binding.moduleList.apply {
|
||||||
|
addVerticalPadding(
|
||||||
|
l_50,
|
||||||
|
l1 + l_50 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
|
||||||
|
)
|
||||||
|
addSimpleItemDecoration(
|
||||||
|
left = l1,
|
||||||
|
top = l_50,
|
||||||
|
right = l1,
|
||||||
|
bottom = l_50,
|
||||||
|
)
|
||||||
|
fixEdgeEffect()
|
||||||
|
post {
|
||||||
|
addInvalidateItemDecorationsObserver()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.moduleFilterInclude.moduleFilterList.apply {
|
||||||
|
addVerticalPadding(
|
||||||
|
l_50,
|
||||||
|
l_50
|
||||||
|
)
|
||||||
|
addSimpleItemDecoration(
|
||||||
|
left = l1,
|
||||||
|
top = l_50,
|
||||||
|
right = l1,
|
||||||
|
bottom = l_50,
|
||||||
|
)
|
||||||
|
fixEdgeEffect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
|
|||||||
@@ -313,10 +313,8 @@ class ModuleViewModel(
|
|||||||
|
|
||||||
fun infoPressed(item: ModuleItem) {
|
fun infoPressed(item: ModuleItem) {
|
||||||
item.repo?.also {
|
item.repo?.also {
|
||||||
if (isConnected.get())
|
if (isConnected.get()) OpenReadmeEvent(it).publish()
|
||||||
OpenReadmeEvent(it).publish()
|
else SnackbarEvent(R.string.no_connection).publish()
|
||||||
else
|
|
||||||
SnackbarEvent(R.string.no_connection).publish()
|
|
||||||
} ?: return
|
} ?: return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import androidx.databinding.Bindable
|
|||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.ObservableItem
|
import com.topjohnwu.magisk.databinding.ObservableItem
|
||||||
import com.topjohnwu.magisk.utils.TransitiveText
|
import com.topjohnwu.magisk.utils.TextHolder
|
||||||
import com.topjohnwu.magisk.utils.asTransitive
|
import com.topjohnwu.magisk.utils.asText
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
@@ -21,9 +21,9 @@ sealed class BaseSettingsItem : ObservableItem<BaseSettingsItem>() {
|
|||||||
override val layoutRes get() = R.layout.item_settings
|
override val layoutRes get() = R.layout.item_settings
|
||||||
|
|
||||||
open val icon: Int get() = 0
|
open val icon: Int get() = 0
|
||||||
open val title: TransitiveText get() = TransitiveText.EMPTY
|
open val title: TextHolder get() = TextHolder.EMPTY
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
open val description: TransitiveText get() = TransitiveText.EMPTY
|
open val description: TextHolder get() = TextHolder.EMPTY
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
@@ -151,14 +151,14 @@ sealed class BaseSettingsItem : ObservableItem<BaseSettingsItem>() {
|
|||||||
open val entries get() = resources.getArrayOrEmpty(entryRes)
|
open val entries get() = resources.getArrayOrEmpty(entryRes)
|
||||||
open val entryValues get() = resources.getArrayOrEmpty(entryValRes)
|
open val entryValues get() = resources.getArrayOrEmpty(entryValRes)
|
||||||
|
|
||||||
override val description: TransitiveText
|
override val description: TextHolder
|
||||||
get() = entries.getOrNull(value)?.asTransitive() ?: TransitiveText.EMPTY
|
get() = entries.getOrNull(value)?.asText() ?: TextHolder.EMPTY
|
||||||
|
|
||||||
private fun Resources.getArrayOrEmpty(id: Int): Array<String> =
|
private fun Resources.getArrayOrEmpty(id: Int): Array<String> =
|
||||||
runCatching { getStringArray(id) }.getOrDefault(emptyArray())
|
runCatching { getStringArray(id) }.getOrDefault(emptyArray())
|
||||||
|
|
||||||
override fun onPressed(view: View, callback: Callback) {
|
override fun onPressed(view: View, callback: Callback) {
|
||||||
if (entries.isEmpty() || entryValues.isEmpty()) return
|
if (entries.isEmpty()) return
|
||||||
super.onPressed(view, callback)
|
super.onPressed(view, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.view.View
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
|
||||||
|
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||||
|
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||||
|
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||||
import com.topjohnwu.magisk.ktx.setOnViewReadyListener
|
import com.topjohnwu.magisk.ktx.setOnViewReadyListener
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
@@ -24,6 +27,21 @@ class SettingsFragment : BaseUIFragment<SettingsViewModel, FragmentSettingsMd2Bi
|
|||||||
binding.settingsList.setOnViewReadyListener {
|
binding.settingsList.setOnViewReadyListener {
|
||||||
binding.settingsList.scrollToPosition(0)
|
binding.settingsList.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val resource = requireContext().resources
|
||||||
|
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||||
|
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||||
|
binding.settingsList.addVerticalPadding(
|
||||||
|
0,
|
||||||
|
l1
|
||||||
|
)
|
||||||
|
binding.settingsList.addSimpleItemDecoration(
|
||||||
|
left = l1,
|
||||||
|
top = l_50,
|
||||||
|
right = l1,
|
||||||
|
bottom = l_50,
|
||||||
|
)
|
||||||
|
binding.settingsList.fixEdgeEffect()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
|
|||||||
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
|
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
|
||||||
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
|
import com.topjohnwu.magisk.utils.TextHolder
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.utils.asTransitive
|
import com.topjohnwu.magisk.utils.asText
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@@ -30,7 +31,7 @@ import kotlinx.coroutines.launch
|
|||||||
// --- Customization
|
// --- Customization
|
||||||
|
|
||||||
object Customization : BaseSettingsItem.Section() {
|
object Customization : BaseSettingsItem.Section() {
|
||||||
override val title = R.string.settings_customization.asTransitive()
|
override val title = R.string.settings_customization.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
object Language : BaseSettingsItem.Selector() {
|
object Language : BaseSettingsItem.Selector() {
|
||||||
@@ -39,7 +40,7 @@ object Language : BaseSettingsItem.Selector() {
|
|||||||
Config.locale = entryValues[it]
|
Config.locale = entryValues[it]
|
||||||
}
|
}
|
||||||
|
|
||||||
override val title = R.string.language.asTransitive()
|
override val title = R.string.language.asText()
|
||||||
override var entries = emptyArray<String>()
|
override var entries = emptyArray<String>()
|
||||||
override var entryValues = emptyArray<String>()
|
override var entryValues = emptyArray<String>()
|
||||||
|
|
||||||
@@ -57,18 +58,18 @@ object Language : BaseSettingsItem.Selector() {
|
|||||||
|
|
||||||
object Theme : BaseSettingsItem.Blank() {
|
object Theme : BaseSettingsItem.Blank() {
|
||||||
override val icon = R.drawable.ic_paint
|
override val icon = R.drawable.ic_paint
|
||||||
override val title = R.string.section_theme.asTransitive()
|
override val title = R.string.section_theme.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Manager
|
// --- App
|
||||||
|
|
||||||
object Manager : BaseSettingsItem.Section() {
|
object AppSettings : BaseSettingsItem.Section() {
|
||||||
override val title = R.string.manager.asTransitive()
|
override val title = R.string.home_app_title.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
object ClearRepoCache : BaseSettingsItem.Blank() {
|
object ClearRepoCache : BaseSettingsItem.Blank() {
|
||||||
override val title = R.string.settings_clear_cache_title.asTransitive()
|
override val title = R.string.settings_clear_cache_title.asText()
|
||||||
override val description = R.string.settings_clear_cache_summary.asTransitive()
|
override val description = R.string.settings_clear_cache_summary.asText()
|
||||||
|
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
isEnabled = Info.env.isActive
|
isEnabled = Info.env.isActive
|
||||||
@@ -76,8 +77,8 @@ object ClearRepoCache : BaseSettingsItem.Blank() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Hide : BaseSettingsItem.Input() {
|
object Hide : BaseSettingsItem.Input() {
|
||||||
override val title = R.string.settings_hide_manager_title.asTransitive()
|
override val title = R.string.settings_hide_app_title.asText()
|
||||||
override val description = R.string.settings_hide_manager_summary.asTransitive()
|
override val description = R.string.settings_hide_app_summary.asText()
|
||||||
|
|
||||||
override var value = ""
|
override var value = ""
|
||||||
set(value) = setV(value, field, { field = it })
|
set(value) = setV(value, field, { field = it })
|
||||||
@@ -86,7 +87,7 @@ object Hide : BaseSettingsItem.Input() {
|
|||||||
get() = if (isError) null else result
|
get() = if (isError) null else result
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var result = "Manager"
|
var result = "Settings"
|
||||||
set(value) = set(value, field, { field = it }, BR.result, BR.error)
|
set(value) = set(value, field, { field = it }, BR.result, BR.error)
|
||||||
|
|
||||||
val maxLength
|
val maxLength
|
||||||
@@ -105,24 +106,21 @@ object Hide : BaseSettingsItem.Input() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Restore : BaseSettingsItem.Blank() {
|
object Restore : BaseSettingsItem.Blank() {
|
||||||
override val title = R.string.settings_restore_manager_title.asTransitive()
|
override val title = R.string.settings_restore_app_title.asText()
|
||||||
override val description = R.string.settings_restore_manager_summary.asTransitive()
|
override val description = R.string.settings_restore_app_summary.asText()
|
||||||
override fun refresh() {
|
|
||||||
isEnabled = Info.remote.app.versionCode > 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object AddShortcut : BaseSettingsItem.Blank() {
|
object AddShortcut : BaseSettingsItem.Blank() {
|
||||||
override val title = R.string.add_shortcut_title.asTransitive()
|
override val title = R.string.add_shortcut_title.asText()
|
||||||
override val description = R.string.setting_add_shortcut_summary.asTransitive()
|
override val description = R.string.setting_add_shortcut_summary.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
object DownloadPath : BaseSettingsItem.Input() {
|
object DownloadPath : BaseSettingsItem.Input() {
|
||||||
override var value = Config.downloadDir
|
override var value = Config.downloadDir
|
||||||
set(value) = setV(value, field, { field = it }) { Config.downloadDir = it }
|
set(value) = setV(value, field, { field = it }) { Config.downloadDir = it }
|
||||||
|
|
||||||
override val title = R.string.settings_download_path_title.asTransitive()
|
override val title = R.string.settings_download_path_title.asText()
|
||||||
override val description get() = path.asTransitive()
|
override val description get() = path.asText()
|
||||||
|
|
||||||
override val inputResult: String get() = result
|
override val inputResult: String get() = result
|
||||||
|
|
||||||
@@ -140,21 +138,29 @@ object DownloadPath : BaseSettingsItem.Input() {
|
|||||||
|
|
||||||
object UpdateChannel : BaseSettingsItem.Selector() {
|
object UpdateChannel : BaseSettingsItem.Selector() {
|
||||||
override var value = Config.updateChannel
|
override var value = Config.updateChannel
|
||||||
set(value) = setV(value, field, { field = it }) { Config.updateChannel = it }
|
set(value) = setV(value, field, { field = it }) {
|
||||||
|
Config.updateChannel = it
|
||||||
override val title = R.string.settings_update_channel_title.asTransitive()
|
Info.remote = Info.EMPTY_REMOTE
|
||||||
override val entries
|
|
||||||
get() = resources.getStringArray(R.array.update_channel).let {
|
|
||||||
if (BuildConfig.DEBUG) it.toMutableList().apply { add("Canary") }.toTypedArray() else it
|
|
||||||
}
|
}
|
||||||
override val entryValRes = R.array.value_array
|
|
||||||
|
override val title = R.string.settings_update_channel_title.asText()
|
||||||
|
override val entries: Array<String> = resources.getStringArray(R.array.update_channel).let {
|
||||||
|
if (BuildConfig.VERSION_CODE % 100 == 0)
|
||||||
|
it.toMutableList().apply { removeAt(Config.Value.CANARY_CHANNEL) }.toTypedArray()
|
||||||
|
else it
|
||||||
|
}
|
||||||
|
override val description
|
||||||
|
get() = entries.getOrNull(value)?.asText() ?: TextHolder.String(entries[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
object UpdateChannelUrl : BaseSettingsItem.Input() {
|
object UpdateChannelUrl : BaseSettingsItem.Input() {
|
||||||
override val title = R.string.settings_update_custom.asTransitive()
|
override val title = R.string.settings_update_custom.asText()
|
||||||
override var value = Config.customChannelUrl
|
override var value = Config.customChannelUrl
|
||||||
set(value) = setV(value, field, { field = it }) { Config.customChannelUrl = it }
|
set(value) = setV(value, field, { field = it }) {
|
||||||
override val description get() = value.asTransitive()
|
Config.customChannelUrl = it
|
||||||
|
Info.remote = Info.EMPTY_REMOTE
|
||||||
|
}
|
||||||
|
override val description get() = value.asText()
|
||||||
|
|
||||||
override val inputResult get() = result
|
override val inputResult get() = result
|
||||||
|
|
||||||
@@ -171,8 +177,8 @@ object UpdateChannelUrl : BaseSettingsItem.Input() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object UpdateChecker : BaseSettingsItem.Toggle() {
|
object UpdateChecker : BaseSettingsItem.Toggle() {
|
||||||
override val title = R.string.settings_check_update_title.asTransitive()
|
override val title = R.string.settings_check_update_title.asText()
|
||||||
override val description = R.string.settings_check_update_summary.asTransitive()
|
override val description = R.string.settings_check_update_summary.asText()
|
||||||
override var value = Config.checkUpdate
|
override var value = Config.checkUpdate
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
Config.checkUpdate = it
|
Config.checkUpdate = it
|
||||||
@@ -181,8 +187,8 @@ object UpdateChecker : BaseSettingsItem.Toggle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object DoHToggle : BaseSettingsItem.Toggle() {
|
object DoHToggle : BaseSettingsItem.Toggle() {
|
||||||
override val title = R.string.settings_doh_title.asTransitive()
|
override val title = R.string.settings_doh_title.asText()
|
||||||
override val description = R.string.settings_doh_description.asTransitive()
|
override val description = R.string.settings_doh_description.asText()
|
||||||
override var value = Config.doh
|
override var value = Config.doh
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
Config.doh = it
|
Config.doh = it
|
||||||
@@ -191,35 +197,35 @@ object DoHToggle : BaseSettingsItem.Toggle() {
|
|||||||
|
|
||||||
// check whether is module already installed beforehand?
|
// check whether is module already installed beforehand?
|
||||||
object SystemlessHosts : BaseSettingsItem.Blank() {
|
object SystemlessHosts : BaseSettingsItem.Blank() {
|
||||||
override val title = R.string.settings_hosts_title.asTransitive()
|
override val title = R.string.settings_hosts_title.asText()
|
||||||
override val description = R.string.settings_hosts_summary.asTransitive()
|
override val description = R.string.settings_hosts_summary.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
object Tapjack : BaseSettingsItem.Toggle() {
|
object Tapjack : BaseSettingsItem.Toggle() {
|
||||||
override val title = R.string.settings_su_tapjack_title.asTransitive()
|
override val title = R.string.settings_su_tapjack_title.asText()
|
||||||
override var description = R.string.settings_su_tapjack_summary.asTransitive()
|
override var description = R.string.settings_su_tapjack_summary.asText()
|
||||||
override var value = Config.suTapjack
|
override var value = Config.suTapjack
|
||||||
set(value) = setV(value, field, { field = it }) { Config.suTapjack = it }
|
set(value) = setV(value, field, { field = it }) { Config.suTapjack = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
object Biometrics : BaseSettingsItem.Toggle() {
|
object Biometrics : BaseSettingsItem.Toggle() {
|
||||||
override val title = R.string.settings_su_biometric_title.asTransitive()
|
override val title = R.string.settings_su_biometric_title.asText()
|
||||||
override var value = Config.suBiometric
|
override var value = Config.suBiometric
|
||||||
set(value) = setV(value, field, { field = it }) { Config.suBiometric = it }
|
set(value) = setV(value, field, { field = it }) { Config.suBiometric = it }
|
||||||
override var description = R.string.settings_su_biometric_summary.asTransitive()
|
override var description = R.string.settings_su_biometric_summary.asText()
|
||||||
|
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
isEnabled = BiometricHelper.isSupported
|
isEnabled = BiometricHelper.isSupported
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
value = false
|
value = false
|
||||||
description = R.string.no_biometric.asTransitive()
|
description = R.string.no_biometric.asText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Reauthenticate : BaseSettingsItem.Toggle() {
|
object Reauthenticate : BaseSettingsItem.Toggle() {
|
||||||
override val title = R.string.settings_su_reauth_title.asTransitive()
|
override val title = R.string.settings_su_reauth_title.asText()
|
||||||
override val description = R.string.settings_su_reauth_summary.asTransitive()
|
override val description = R.string.settings_su_reauth_summary.asText()
|
||||||
override var value = Config.suReAuth
|
override var value = Config.suReAuth
|
||||||
set(value) = setV(value, field, { field = it }) { Config.suReAuth = it }
|
set(value) = setV(value, field, { field = it }) { Config.suReAuth = it }
|
||||||
|
|
||||||
@@ -231,16 +237,16 @@ object Reauthenticate : BaseSettingsItem.Toggle() {
|
|||||||
// --- Magisk
|
// --- Magisk
|
||||||
|
|
||||||
object Magisk : BaseSettingsItem.Section() {
|
object Magisk : BaseSettingsItem.Section() {
|
||||||
override val title = R.string.magisk.asTransitive()
|
override val title = R.string.magisk.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
object MagiskHide : BaseSettingsItem.Toggle() {
|
object MagiskHide : BaseSettingsItem.Toggle() {
|
||||||
override val title = R.string.magiskhide.asTransitive()
|
override val title = R.string.magiskhide.asText()
|
||||||
override val description = R.string.settings_magiskhide_summary.asTransitive()
|
override val description = R.string.settings_magiskhide_summary.asText()
|
||||||
override var value = Config.magiskHide
|
override var value = Config.magiskHide
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
val cmd = if (it) "enable" else "disable"
|
val cmd = if (it) "enable" else "disable"
|
||||||
Shell.su("magiskhide --$cmd").submit { cb ->
|
Shell.su("magiskhide $cmd").submit { cb ->
|
||||||
if (cb.isSuccess) Config.magiskHide = it
|
if (cb.isSuccess) Config.magiskHide = it
|
||||||
else field = !it
|
else field = !it
|
||||||
}
|
}
|
||||||
@@ -250,32 +256,30 @@ object MagiskHide : BaseSettingsItem.Toggle() {
|
|||||||
// --- Superuser
|
// --- Superuser
|
||||||
|
|
||||||
object Superuser : BaseSettingsItem.Section() {
|
object Superuser : BaseSettingsItem.Section() {
|
||||||
override val title = R.string.superuser.asTransitive()
|
override val title = R.string.superuser.asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
object AccessMode : BaseSettingsItem.Selector() {
|
object AccessMode : BaseSettingsItem.Selector() {
|
||||||
override val title = R.string.superuser_access.asTransitive()
|
override val title = R.string.superuser_access.asText()
|
||||||
override val entryRes = R.array.su_access
|
override val entryRes = R.array.su_access
|
||||||
override val entryValRes = R.array.value_array
|
|
||||||
|
|
||||||
override var value = Config.rootMode
|
override var value = Config.rootMode
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
Config.rootMode = entryValues[it].toInt()
|
Config.rootMode = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object MultiuserMode : BaseSettingsItem.Selector() {
|
object MultiuserMode : BaseSettingsItem.Selector() {
|
||||||
override val title = R.string.multiuser_mode.asTransitive()
|
override val title = R.string.multiuser_mode.asText()
|
||||||
override val entryRes = R.array.multiuser_mode
|
override val entryRes = R.array.multiuser_mode
|
||||||
override val entryValRes = R.array.value_array
|
|
||||||
|
|
||||||
override var value = Config.suMultiuserMode
|
override var value = Config.suMultiuserMode
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
Config.suMultiuserMode = entryValues[it].toInt()
|
Config.suMultiuserMode = it
|
||||||
}
|
}
|
||||||
|
|
||||||
override val description
|
override val description
|
||||||
get() = resources.getStringArray(R.array.multiuser_summary)[value].asTransitive()
|
get() = resources.getStringArray(R.array.multiuser_summary)[value].asText()
|
||||||
|
|
||||||
override fun refresh() {
|
override fun refresh() {
|
||||||
isEnabled = Const.USER_ID == 0
|
isEnabled = Const.USER_ID == 0
|
||||||
@@ -283,32 +287,30 @@ object MultiuserMode : BaseSettingsItem.Selector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object MountNamespaceMode : BaseSettingsItem.Selector() {
|
object MountNamespaceMode : BaseSettingsItem.Selector() {
|
||||||
override val title = R.string.mount_namespace_mode.asTransitive()
|
override val title = R.string.mount_namespace_mode.asText()
|
||||||
override val entryRes = R.array.namespace
|
override val entryRes = R.array.namespace
|
||||||
override val entryValRes = R.array.value_array
|
|
||||||
|
|
||||||
override var value = Config.suMntNamespaceMode
|
override var value = Config.suMntNamespaceMode
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
Config.suMntNamespaceMode = entryValues[it].toInt()
|
Config.suMntNamespaceMode = it
|
||||||
}
|
}
|
||||||
|
|
||||||
override val description
|
override val description
|
||||||
get() = resources.getStringArray(R.array.namespace_summary)[value].asTransitive()
|
get() = resources.getStringArray(R.array.namespace_summary)[value].asText()
|
||||||
}
|
}
|
||||||
|
|
||||||
object AutomaticResponse : BaseSettingsItem.Selector() {
|
object AutomaticResponse : BaseSettingsItem.Selector() {
|
||||||
override val title = R.string.auto_response.asTransitive()
|
override val title = R.string.auto_response.asText()
|
||||||
override val entryRes = R.array.auto_response
|
override val entryRes = R.array.auto_response
|
||||||
override val entryValRes = R.array.value_array
|
|
||||||
|
|
||||||
override var value = Config.suAutoResponse
|
override var value = Config.suAutoResponse
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
Config.suAutoResponse = entryValues[it].toInt()
|
Config.suAutoResponse = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object RequestTimeout : BaseSettingsItem.Selector() {
|
object RequestTimeout : BaseSettingsItem.Selector() {
|
||||||
override val title = R.string.request_timeout.asTransitive()
|
override val title = R.string.request_timeout.asText()
|
||||||
override val entryRes = R.array.request_timeout
|
override val entryRes = R.array.request_timeout
|
||||||
override val entryValRes = R.array.request_timeout_value
|
override val entryValRes = R.array.request_timeout_value
|
||||||
|
|
||||||
@@ -322,12 +324,11 @@ object RequestTimeout : BaseSettingsItem.Selector() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object SUNotification : BaseSettingsItem.Selector() {
|
object SUNotification : BaseSettingsItem.Selector() {
|
||||||
override val title = R.string.superuser_notification.asTransitive()
|
override val title = R.string.superuser_notification.asText()
|
||||||
override val entryRes = R.array.su_notification
|
override val entryRes = R.array.su_notification
|
||||||
override val entryValRes = R.array.value_array
|
|
||||||
|
|
||||||
override var value = Config.suNotification
|
override var value = Config.suNotification
|
||||||
set(value) = setV(value, field, { field = it }) {
|
set(value) = setV(value, field, { field = it }) {
|
||||||
Config.suNotification = entryValues[it].toInt()
|
Config.suNotification = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.topjohnwu.magisk.data.database.RepoDao
|
|||||||
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
||||||
import com.topjohnwu.magisk.events.RecreateEvent
|
import com.topjohnwu.magisk.events.RecreateEvent
|
||||||
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
||||||
|
import com.topjohnwu.magisk.ktx.activity
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -54,13 +55,17 @@ class SettingsViewModel(
|
|||||||
|
|
||||||
// Manager
|
// Manager
|
||||||
list.addAll(listOf(
|
list.addAll(listOf(
|
||||||
Manager,
|
AppSettings,
|
||||||
UpdateChannel, UpdateChannelUrl, DoHToggle, UpdateChecker, DownloadPath
|
UpdateChannel, UpdateChannelUrl, DoHToggle, UpdateChecker, DownloadPath
|
||||||
))
|
))
|
||||||
if (Info.env.isActive) {
|
if (Info.env.isActive) {
|
||||||
list.add(ClearRepoCache)
|
list.add(ClearRepoCache)
|
||||||
if (Const.USER_ID == 0 && Info.isConnected.get())
|
if (Build.VERSION.SDK_INT >= 21 && Const.USER_ID == 0) {
|
||||||
list.add(if (hidden) Restore else Hide)
|
if (hidden)
|
||||||
|
list.add(Restore)
|
||||||
|
else if (Info.isConnected.get())
|
||||||
|
list.add(Hide)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Magisk
|
// Magisk
|
||||||
@@ -98,20 +103,22 @@ class SettingsViewModel(
|
|||||||
override fun onItemPressed(view: View, item: BaseSettingsItem, callback: () -> Unit) = when (item) {
|
override fun onItemPressed(view: View, item: BaseSettingsItem, callback: () -> Unit) = when (item) {
|
||||||
is DownloadPath -> withExternalRW(callback)
|
is DownloadPath -> withExternalRW(callback)
|
||||||
is Biometrics -> authenticate(callback)
|
is Biometrics -> authenticate(callback)
|
||||||
is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish()
|
is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().navigate()
|
||||||
is ClearRepoCache -> clearRepoCache()
|
is ClearRepoCache -> clearRepoCache()
|
||||||
is SystemlessHosts -> createHosts()
|
is SystemlessHosts -> createHosts()
|
||||||
is Restore -> HideAPK.restore(view.context)
|
is Restore -> HideAPK.restore(view.activity)
|
||||||
is AddShortcut -> AddHomeIconEvent().publish()
|
is AddShortcut -> AddHomeIconEvent().publish()
|
||||||
else -> callback()
|
else -> callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemChanged(view: View, item: BaseSettingsItem) = when (item) {
|
override fun onItemChanged(view: View, item: BaseSettingsItem) {
|
||||||
|
when (item) {
|
||||||
is Language -> RecreateEvent().publish()
|
is Language -> RecreateEvent().publish()
|
||||||
is UpdateChannel -> openUrlIfNecessary(view)
|
is UpdateChannel -> openUrlIfNecessary(view)
|
||||||
is Hide -> HideAPK.hide(view.context, item.value)
|
is Hide -> viewModelScope.launch { HideAPK.hide(view.activity, item.value) }
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun openUrlIfNecessary(view: View) {
|
private fun openUrlIfNecessary(view: View) {
|
||||||
UpdateChannelUrl.refresh()
|
UpdateChannelUrl.refresh()
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.ui.superuser
|
package com.topjohnwu.magisk.ui.superuser
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
|
||||||
|
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||||
|
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||||
|
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
|
class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
|
||||||
@@ -15,6 +20,25 @@ class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd
|
|||||||
activity.title = resources.getString(R.string.superuser)
|
activity.title = resources.getString(R.string.superuser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val resource = requireContext().resources
|
||||||
|
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||||
|
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||||
|
binding.superuserList.addVerticalPadding(
|
||||||
|
l_50,
|
||||||
|
l1
|
||||||
|
)
|
||||||
|
binding.superuserList.addSimpleItemDecoration(
|
||||||
|
left = l1,
|
||||||
|
top = l_50,
|
||||||
|
right = l1,
|
||||||
|
bottom = l_50,
|
||||||
|
)
|
||||||
|
binding.superuserList.fixEdgeEffect()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPreBind(binding: FragmentSuperuserMd2Binding) {}
|
override fun onPreBind(binding: FragmentSuperuserMd2Binding) {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.ui.superuser
|
package com.topjohnwu.magisk.ui.superuser
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import androidx.databinding.ObservableArrayList
|
import androidx.databinding.ObservableArrayList
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
@@ -19,6 +18,7 @@ import com.topjohnwu.magisk.events.SnackbarEvent
|
|||||||
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
||||||
import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog
|
import com.topjohnwu.magisk.events.dialog.SuperuserRevokeDialog
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
|
import com.topjohnwu.magisk.utils.asText
|
||||||
import com.topjohnwu.magisk.view.TappableHeadlineItem
|
import com.topjohnwu.magisk.view.TappableHeadlineItem
|
||||||
import com.topjohnwu.magisk.view.TextItem
|
import com.topjohnwu.magisk.view.TextItem
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,8 +27,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
|
import me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
|
||||||
|
|
||||||
class SuperuserViewModel(
|
class SuperuserViewModel(
|
||||||
private val db: PolicyDao,
|
private val db: PolicyDao
|
||||||
private val resources: Resources
|
|
||||||
) : BaseViewModel(), TappableHeadlineItem.Listener {
|
) : BaseViewModel(), TappableHeadlineItem.Listener {
|
||||||
|
|
||||||
private val itemNoData = TextItem(R.string.superuser_policy_none)
|
private val itemNoData = TextItem(R.string.superuser_policy_none)
|
||||||
@@ -55,6 +54,7 @@ class SuperuserViewModel(
|
|||||||
}
|
}
|
||||||
state = State.LOADING
|
state = State.LOADING
|
||||||
val (policies, diff) = withContext(Dispatchers.Default) {
|
val (policies, diff) = withContext(Dispatchers.Default) {
|
||||||
|
db.deleteOutdated()
|
||||||
val policies = db.fetchAll {
|
val policies = db.fetchAll {
|
||||||
PolicyRvItem(it, it.icon, this@SuperuserViewModel)
|
PolicyRvItem(it, it.icon, this@SuperuserViewModel)
|
||||||
}.sortedWith(compareBy(
|
}.sortedWith(compareBy(
|
||||||
@@ -79,7 +79,7 @@ class SuperuserViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun hidePressed() =
|
private fun hidePressed() =
|
||||||
SuperuserFragmentDirections.actionSuperuserFragmentToHideFragment().publish()
|
SuperuserFragmentDirections.actionSuperuserFragmentToHideFragment().navigate()
|
||||||
|
|
||||||
fun deletePressed(item: PolicyRvItem) {
|
fun deletePressed(item: PolicyRvItem) {
|
||||||
fun updateState() = viewModelScope.launch {
|
fun updateState() = viewModelScope.launch {
|
||||||
@@ -106,7 +106,7 @@ class SuperuserViewModel(
|
|||||||
|
|
||||||
fun updatePolicy(policy: SuPolicy, isLogging: Boolean) = viewModelScope.launch {
|
fun updatePolicy(policy: SuPolicy, isLogging: Boolean) = viewModelScope.launch {
|
||||||
db.update(policy)
|
db.update(policy)
|
||||||
val str = when {
|
val res = when {
|
||||||
isLogging -> when {
|
isLogging -> when {
|
||||||
policy.logging -> R.string.su_snack_log_on
|
policy.logging -> R.string.su_snack_log_on
|
||||||
else -> R.string.su_snack_log_off
|
else -> R.string.su_snack_log_off
|
||||||
@@ -116,7 +116,7 @@ class SuperuserViewModel(
|
|||||||
else -> R.string.su_snack_notif_off
|
else -> R.string.su_snack_notif_off
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SnackbarEvent(resources.getString(str, policy.appName)).publish()
|
SnackbarEvent(res.asText(policy.appName)).publish()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
|
fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
|
||||||
@@ -130,7 +130,7 @@ class SuperuserViewModel(
|
|||||||
db.update(app)
|
db.update(app)
|
||||||
val res = if (app.policy == SuPolicy.ALLOW) R.string.su_snack_grant
|
val res = if (app.policy == SuPolicy.ALLOW) R.string.su_snack_grant
|
||||||
else R.string.su_snack_deny
|
else R.string.su_snack_deny
|
||||||
SnackbarEvent(resources.getString(res).format(item.item.appName)).publish()
|
SnackbarEvent(res.asText(item.item.appName)).publish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.utils
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
@@ -13,8 +15,8 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class HideBottomViewOnScrollBehavior<V : View> : HideBottomViewOnScrollBehavior<V>(),
|
class HideBottomViewOnScrollBehavior<V : View>(context: Context, attrs: AttributeSet) :
|
||||||
HideableBehavior<V> {
|
HideBottomViewOnScrollBehavior<V>(), HideableBehavior<V> {
|
||||||
|
|
||||||
private var lockState: Boolean = false
|
private var lockState: Boolean = false
|
||||||
private var isLaidOut = false
|
private var isLaidOut = false
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.topjohnwu.magisk.utils
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
import android.animation.TimeInterpolator
|
import android.animation.TimeInterpolator
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewPropertyAnimator
|
import android.view.ViewPropertyAnimator
|
||||||
@@ -8,9 +10,8 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
|
|||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import com.google.android.material.animation.AnimationUtils
|
import com.google.android.material.animation.AnimationUtils
|
||||||
|
|
||||||
class HideTopViewOnScrollBehavior<V : View> :
|
class HideTopViewOnScrollBehavior<V : View>(context: Context, attrs: AttributeSet) :
|
||||||
CoordinatorLayout.Behavior<V>(),
|
CoordinatorLayout.Behavior<V>(), HideableBehavior<V> {
|
||||||
HideableBehavior<V> {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STATE_SCROLLED_DOWN = 1
|
private const val STATE_SCROLLED_DOWN = 1
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import android.widget.TextView
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.caverock.androidsvg.SVG
|
import com.caverock.androidsvg.SVG
|
||||||
import com.caverock.androidsvg.SVGParseException
|
import com.caverock.androidsvg.SVGParseException
|
||||||
import com.topjohnwu.magisk.core.ResMgr
|
import com.topjohnwu.magisk.core.AssetHack
|
||||||
import com.topjohnwu.superuser.internal.WaitRunnable
|
import com.topjohnwu.superuser.internal.WaitRunnable
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin
|
import io.noties.markwon.AbstractMarkwonPlugin
|
||||||
import io.noties.markwon.MarkwonSpansFactory
|
import io.noties.markwon.MarkwonSpansFactory
|
||||||
@@ -216,7 +216,7 @@ class MarkwonImagePlugin(okHttp: OkHttpClient) : AbstractMarkwonPlugin() {
|
|||||||
return PictureDrawable(picture)
|
return PictureDrawable(picture)
|
||||||
}
|
}
|
||||||
|
|
||||||
val density: Float = ResMgr.resource.displayMetrics.density
|
val density: Float = AssetHack.resource.displayMetrics.density
|
||||||
|
|
||||||
val width = (w * density + .5f).toInt()
|
val width = (w * density + .5f).toInt()
|
||||||
val height = (h * density + .5f).toInt()
|
val height = (h * density + .5f).toInt()
|
||||||
@@ -226,7 +226,7 @@ class MarkwonImagePlugin(okHttp: OkHttpClient) : AbstractMarkwonPlugin() {
|
|||||||
canvas.scale(density, density)
|
canvas.scale(density, density)
|
||||||
svg.renderToCanvas(canvas)
|
svg.renderToCanvas(canvas)
|
||||||
|
|
||||||
return BitmapDrawable(ResMgr.resource, bitmap)
|
return BitmapDrawable(AssetHack.resource, bitmap)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
47
app/src/main/java/com/topjohnwu/magisk/utils/TextHolder.kt
Normal file
47
app/src/main/java/com/topjohnwu/magisk/utils/TextHolder.kt
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.databinding.BindingAdapter
|
||||||
|
|
||||||
|
sealed class TextHolder {
|
||||||
|
|
||||||
|
abstract val isEmpty: Boolean
|
||||||
|
abstract fun getText(resources: Resources): CharSequence
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
class String(
|
||||||
|
private val value: CharSequence
|
||||||
|
) : TextHolder() {
|
||||||
|
|
||||||
|
override val isEmpty get() = value.isEmpty()
|
||||||
|
override fun getText(resources: Resources) = value
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Resource(
|
||||||
|
private val value: Int,
|
||||||
|
private vararg val params: Any
|
||||||
|
) : TextHolder() {
|
||||||
|
|
||||||
|
override val isEmpty get() = value == 0
|
||||||
|
override fun getText(resources: Resources) = resources.getString(value, *params)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val EMPTY = String("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.asText(vararg params: Any): TextHolder = TextHolder.Resource(this, *params)
|
||||||
|
fun CharSequence.asText(): TextHolder = TextHolder.String(this)
|
||||||
|
|
||||||
|
|
||||||
|
@BindingAdapter("android:text")
|
||||||
|
fun TextView.setText(text: TextHolder) {
|
||||||
|
this.text = text.getText(context.resources)
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.databinding.BindingAdapter
|
|
||||||
import androidx.databinding.InverseBindingAdapter
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
|
||||||
|
|
||||||
sealed class TransitiveText {
|
|
||||||
|
|
||||||
abstract val isEmpty: Boolean
|
|
||||||
abstract fun getText(resources: Resources): CharSequence
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
class String(
|
|
||||||
private val value: CharSequence
|
|
||||||
) : TransitiveText() {
|
|
||||||
|
|
||||||
override val isEmpty = value.isEmpty()
|
|
||||||
override fun getText(resources: Resources) = value
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Res(
|
|
||||||
private val value: Int,
|
|
||||||
private vararg val params: Any
|
|
||||||
) : TransitiveText() {
|
|
||||||
|
|
||||||
override val isEmpty = value == 0
|
|
||||||
override fun getText(resources: Resources) =
|
|
||||||
resources.getString(value, *params)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val EMPTY = String("")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun Int.asTransitive(vararg params: Any) = TransitiveText.Res(this, *params)
|
|
||||||
fun CharSequence.asTransitive() = TransitiveText.String(this)
|
|
||||||
|
|
||||||
|
|
||||||
@BindingAdapter("android:text")
|
|
||||||
fun TextView.setText(text: TransitiveText) {
|
|
||||||
this.text = text.getText(get())
|
|
||||||
}
|
|
||||||
|
|
||||||
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
|
|
||||||
fun TextView.getTransitiveText() = text.asTransitive()
|
|
||||||
@@ -2,25 +2,24 @@ package com.topjohnwu.magisk.view
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.graphics.Color
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.InsetDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatDialog
|
import androidx.appcompat.app.AppCompatDialog
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||||
@@ -47,33 +46,20 @@ class MagiskDialog(
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
super.setContentView(binding.root)
|
super.setContentView(binding.root)
|
||||||
|
|
||||||
|
val default = MaterialColors.getColor(context, R.attr.colorSurface, javaClass.canonicalName)
|
||||||
|
val surfaceColor = MaterialColors.getColor(context, R.attr.colorSurfaceSurfaceVariant, default)
|
||||||
|
val materialShapeDrawable = MaterialShapeDrawable(context, null, R.attr.alertDialogStyle, R.style.MaterialAlertDialog_MaterialComponents)
|
||||||
|
materialShapeDrawable.initializeElevationOverlay(context)
|
||||||
|
materialShapeDrawable.fillColor = ColorStateList.valueOf(surfaceColor)
|
||||||
|
materialShapeDrawable.elevation = context.resources.getDimension(R.dimen.margin_generic)
|
||||||
|
materialShapeDrawable.setCornerSize(context.resources.getDimension(R.dimen.l_50))
|
||||||
|
|
||||||
|
val inset = context.resources.getDimensionPixelSize(R.dimen.appcompat_dialog_background_inset)
|
||||||
window?.apply {
|
window?.apply {
|
||||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
setBackgroundDrawable(InsetDrawable(materialShapeDrawable, inset, inset, inset, inset))
|
||||||
setLayout(
|
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
WindowManager.LayoutParams.MATCH_PARENT,
|
|
||||||
WindowManager.LayoutParams.MATCH_PARENT
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val paddingTop = binding.root.paddingTop
|
|
||||||
val paddingBottom = binding.root.paddingBottom
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
|
|
||||||
view.updatePadding(
|
|
||||||
top = paddingTop + insets.systemWindowInsetTop,
|
|
||||||
bottom = paddingBottom + insets.systemWindowInsetBottom
|
|
||||||
)
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setCancelable(flag: Boolean) {
|
|
||||||
val listener = if (!flag) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
setCanceledOnTouchOutside(true)
|
|
||||||
View.OnClickListener { dismiss() }
|
|
||||||
}
|
|
||||||
binding.dialogBaseOutsideContainer.setOnClickListener(listener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Data : ObservableHost {
|
inner class Data : ObservableHost {
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.view
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
|
||||||
import com.topjohnwu.magisk.ktx.coroutineScope
|
|
||||||
import io.noties.markwon.Markwon
|
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
|
|
||||||
object MarkDownWindow : KoinComponent {
|
|
||||||
|
|
||||||
private val svc: NetworkService by inject()
|
|
||||||
private val markwon: Markwon by inject()
|
|
||||||
|
|
||||||
suspend fun show(activity: Context, title: String?, url: String) {
|
|
||||||
show(activity, title) {
|
|
||||||
svc.fetchString(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun show(activity: Context, title: String?, input: suspend () -> String) {
|
|
||||||
val view = LayoutInflater.from(activity).inflate(R.layout.markdown_window_md2, null)
|
|
||||||
|
|
||||||
MagiskDialog(activity)
|
|
||||||
.applyTitle(title ?: "")
|
|
||||||
.applyView(view)
|
|
||||||
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
|
||||||
titleRes = android.R.string.cancel
|
|
||||||
}
|
|
||||||
.reveal()
|
|
||||||
|
|
||||||
val tv = view.findViewById<TextView>(R.id.md_txt)
|
|
||||||
tv.coroutineScope = CoroutineScope(coroutineContext)
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
markwon.setMarkdown(tv, input())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
if (e is CancellationException)
|
|
||||||
throw e
|
|
||||||
Timber.e(e)
|
|
||||||
tv.post { tv.setText(R.string.download_file_error) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,24 +3,20 @@ package com.topjohnwu.magisk.view
|
|||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import androidx.core.app.TaskStackBuilder
|
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.graphics.drawable.toIcon
|
import androidx.core.graphics.drawable.toIcon
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.SplashActivity
|
|
||||||
import com.topjohnwu.magisk.core.cmp
|
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.intent
|
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.magisk.ktx.getBitmap
|
import com.topjohnwu.magisk.ktx.getBitmap
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
object Notifications {
|
object Notifications {
|
||||||
|
|
||||||
val mgr by lazy { get<Context>().getSystemService<NotificationManager>()!! }
|
val mgr by lazy { get<Context>().getSystemService<NotificationManager>()!! }
|
||||||
@@ -49,29 +45,11 @@ object Notifications {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun magiskUpdate(context: Context) {
|
|
||||||
val intent = context.intent<SplashActivity>()
|
|
||||||
.putExtra(Const.Key.OPEN_SECTION, "magisk")
|
|
||||||
val stackBuilder = TaskStackBuilder.create(context)
|
|
||||||
stackBuilder.addParentStack(SplashActivity::class.java.cmp(context.packageName))
|
|
||||||
stackBuilder.addNextIntent(intent)
|
|
||||||
val pendingIntent = stackBuilder.getPendingIntent(
|
|
||||||
Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
|
|
||||||
val builder = updateBuilder(context)
|
|
||||||
.setContentTitle(context.getString(R.string.magisk_update_title))
|
|
||||||
.setContentText(context.getString(R.string.manager_download_install))
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
|
|
||||||
mgr.notify(Const.ID.MAGISK_UPDATE_NOTIFICATION_ID, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun managerUpdate(context: Context) {
|
fun managerUpdate(context: Context) {
|
||||||
val intent = DownloadService.pendingIntent(context, Subject.Manager())
|
val intent = DownloadService.pendingIntent(context, Subject.Manager())
|
||||||
|
|
||||||
val builder = updateBuilder(context)
|
val builder = updateBuilder(context)
|
||||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
.setContentTitle(context.getString(R.string.magisk_update_title))
|
||||||
.setContentText(context.getString(R.string.manager_download_install))
|
.setContentText(context.getString(R.string.manager_download_install))
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setContentIntent(intent)
|
.setContentIntent(intent)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.view
|
package com.topjohnwu.magisk.view
|
||||||
|
|
||||||
import android.annotation.TargetApi
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ShortcutInfo
|
import android.content.pm.ShortcutInfo
|
||||||
@@ -8,6 +7,8 @@ import android.content.pm.ShortcutManager
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
@@ -24,16 +25,14 @@ object Shortcuts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(26)
|
|
||||||
fun addHomeIcon(context: Context) {
|
fun addHomeIcon(context: Context) {
|
||||||
val manager = context.getSystemService<ShortcutManager>() ?: return
|
|
||||||
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
||||||
val info = ShortcutInfo.Builder(context, Const.Nav.HOME)
|
val info = ShortcutInfoCompat.Builder(context, Const.Nav.HOME)
|
||||||
.setShortLabel(context.getString(R.string.app_name))
|
.setShortLabel(context.getString(R.string.magisk))
|
||||||
.setIntent(intent)
|
.setIntent(intent)
|
||||||
.setIcon(context.getIcon(R.drawable.ic_launcher))
|
.setIcon(context.getIconCompat(R.drawable.ic_launcher))
|
||||||
.build()
|
.build()
|
||||||
manager.requestPinShortcut(info, null)
|
ShortcutManagerCompat.requestPinShortcut(context, info, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.getIconCompat(id: Int): IconCompat {
|
private fun Context.getIconCompat(id: Int): IconCompat {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user