mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 11:37:36 +00:00
Compare commits
371 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6066b5cf86 | ||
![]() |
5cdf95a4d0 | ||
![]() |
910a36fdc1 | ||
![]() |
8331206acb | ||
![]() |
8423dc8d63 | ||
![]() |
6077c989a7 | ||
![]() |
c97d1044fa | ||
![]() |
f42c089b26 | ||
![]() |
1f8c063dc6 | ||
![]() |
4874520d65 | ||
![]() |
5e53639969 | ||
![]() |
83ab0ca6cd | ||
![]() |
70fd03d5fc | ||
![]() |
2e52875b50 | ||
![]() |
fd9b990ad7 | ||
![]() |
69978a9442 | ||
![]() |
d155da52ce | ||
![]() |
9c5b131913 | ||
![]() |
9d740cec1a | ||
![]() |
c2978eb9c3 | ||
![]() |
38abad1e44 | ||
![]() |
b4863eb51b | ||
![]() |
3817167ba1 | ||
![]() |
d1a35dd2ba | ||
![]() |
26116ac414 | ||
![]() |
0b26882fce | ||
![]() |
a2495fb5fb | ||
![]() |
0beb3bf16a | ||
![]() |
b68658e974 | ||
![]() |
3ae7344747 | ||
![]() |
4eb71830b3 | ||
![]() |
9183a0a6ea | ||
![]() |
bb64ba0ef6 | ||
![]() |
d89a568897 | ||
![]() |
9fd1f41e8b | ||
![]() |
c1ab348673 | ||
![]() |
00247c7901 | ||
![]() |
3c75f474c6 | ||
![]() |
db1f5b0397 | ||
![]() |
db277c3e55 | ||
![]() |
b9c93c66f6 | ||
![]() |
a250e2b56c | ||
![]() |
cd96454886 | ||
![]() |
741b679306 | ||
![]() |
90013e486d | ||
![]() |
4e2ecdb920 | ||
![]() |
6e5df1f06b | ||
![]() |
9469e79e3c | ||
![]() |
db78c20161 | ||
![]() |
1699da1754 | ||
![]() |
754e690274 | ||
![]() |
6f74ed6ceb | ||
![]() |
71205bc530 | ||
![]() |
10e236abdf | ||
![]() |
2248af00f3 | ||
![]() |
7e61716277 | ||
![]() |
50edb8d072 | ||
![]() |
515f81944c | ||
![]() |
46d4708386 | ||
![]() |
aabc36f86b | ||
![]() |
e0d5d90267 | ||
![]() |
482a5b991b | ||
![]() |
20124fe410 | ||
![]() |
f8dcec116a | ||
![]() |
343a339aae | ||
![]() |
42606efe56 | ||
![]() |
cae58c8790 | ||
![]() |
3a39dd4049 | ||
![]() |
89ff3c6572 | ||
![]() |
7bf9c74216 | ||
![]() |
e2f3753551 | ||
![]() |
cacf873645 | ||
![]() |
11e1e7ee36 | ||
![]() |
87801b6f23 | ||
![]() |
7ce4789e17 | ||
![]() |
9dc6d9afce | ||
![]() |
d6a5354bff | ||
![]() |
07af37475b | ||
![]() |
1b9c273b10 | ||
![]() |
262c52db56 | ||
![]() |
eb777296d4 | ||
![]() |
fc70a384d3 | ||
![]() |
34b2f525a3 | ||
![]() |
569e9ad937 | ||
![]() |
c495b3d183 | ||
![]() |
8b16bfbb54 | ||
![]() |
b2f1fd9966 | ||
![]() |
317153c53a | ||
![]() |
fa60daf9b5 | ||
![]() |
aadb2d825c | ||
![]() |
0e7fe537e3 | ||
![]() |
409de3ac44 | ||
![]() |
759055eaa5 | ||
![]() |
9016e6727d | ||
![]() |
a3381da7ed | ||
![]() |
351e094440 | ||
![]() |
2106751ea4 | ||
![]() |
7ae3cd1c43 | ||
![]() |
edfd4dcddf | ||
![]() |
fb89cf1367 | ||
![]() |
b7b345cf8a | ||
![]() |
0be487e47e | ||
![]() |
5471147422 | ||
![]() |
6305159c5e | ||
![]() |
2ed092c9db | ||
![]() |
5c6a7ffa6f | ||
![]() |
9ab7550970 | ||
![]() |
47e7a0a434 | ||
![]() |
4cc5e9f986 | ||
![]() |
6a2ae89846 | ||
![]() |
3c93539e02 | ||
![]() |
05e5ac2ad2 | ||
![]() |
10b1782732 | ||
![]() |
e029994ef8 | ||
![]() |
9679874874 | ||
![]() |
8186f253e8 | ||
![]() |
d4fe8632ec | ||
![]() |
d7776f6597 | ||
![]() |
3219d945f5 | ||
![]() |
8a73a16029 | ||
![]() |
ce90f9b60d | ||
![]() |
bdf54d562f | ||
![]() |
e744cc8ea6 | ||
![]() |
babcf36495 | ||
![]() |
e4094c0caa | ||
![]() |
2e51fe20a1 | ||
![]() |
c29636c452 | ||
![]() |
22017a5543 | ||
![]() |
50e2f33d1c | ||
![]() |
5e6eb8dd01 | ||
![]() |
18acb97dfe | ||
![]() |
bf2f823b8c | ||
![]() |
d0c4226997 | ||
![]() |
4ea8bd0229 | ||
![]() |
ee0d58a9b8 | ||
![]() |
bf04fa134b | ||
![]() |
297662cafb | ||
![]() |
f464a9b269 | ||
![]() |
d19fcd5e21 | ||
![]() |
c0981174a8 | ||
![]() |
0b5f973b31 | ||
![]() |
4159b3871c | ||
![]() |
580c993c0b | ||
![]() |
0cc29350a0 | ||
![]() |
490a784993 | ||
![]() |
9c774f96db | ||
![]() |
99afe7ac07 | ||
![]() |
b3f05fd925 | ||
![]() |
683cfee88b | ||
![]() |
3bcaf0ed5b | ||
![]() |
edb76503d3 | ||
![]() |
484038638f | ||
![]() |
8dfb30fefe | ||
![]() |
2a252d13b8 | ||
![]() |
afa364cfc3 | ||
![]() |
dfa36fb25d | ||
![]() |
c8492b0c58 | ||
![]() |
083ef803fe | ||
![]() |
351f0269ae | ||
![]() |
a29ae15ff7 | ||
![]() |
34dded3b25 | ||
![]() |
975b1a5e36 | ||
![]() |
e11508f84d | ||
![]() |
0772f6dcaf | ||
![]() |
d3fe3a711a | ||
![]() |
756d8356ca | ||
![]() |
42003b4006 | ||
![]() |
dc65a2b884 | ||
![]() |
071ae79fa8 | ||
![]() |
c11ccbae2d | ||
![]() |
6ef86d8d20 | ||
![]() |
985249c3d0 | ||
![]() |
622e09862a | ||
![]() |
7505599ea0 | ||
![]() |
575c417403 | ||
![]() |
9f7a3db8be | ||
![]() |
029422679c | ||
![]() |
05d6d2b51b | ||
![]() |
4cff0384f7 | ||
![]() |
68db366696 | ||
![]() |
358538717c | ||
![]() |
24603b3cef | ||
![]() |
4eb9240806 | ||
![]() |
0469f0b5ae | ||
![]() |
0b8577d02b | ||
![]() |
97135879a1 | ||
![]() |
fef41f68c0 | ||
![]() |
0ac19e3a4e | ||
![]() |
2793d209a4 | ||
![]() |
71e9c044e6 | ||
![]() |
42e5f5150a | ||
![]() |
90545057e9 | ||
![]() |
cffd024e9e | ||
![]() |
8c858592c4 | ||
![]() |
4f1a1879e5 | ||
![]() |
e88eed9a8d | ||
![]() |
9581ae8245 | ||
![]() |
4202b7a9dc | ||
![]() |
b4c398542a | ||
![]() |
081148b2d7 | ||
![]() |
a32c4561ed | ||
![]() |
cc79a96fa3 | ||
![]() |
ff340ce3d8 | ||
![]() |
134508193d | ||
![]() |
c2b74aa83e | ||
![]() |
3358eab991 | ||
![]() |
a609e0aad4 | ||
![]() |
f97866a961 | ||
![]() |
e1987c42c4 | ||
![]() |
18566715e1 | ||
![]() |
79f0f3230c | ||
![]() |
63a89d9f04 | ||
![]() |
f639f39e79 | ||
![]() |
b4099fc5f9 | ||
![]() |
ff2513e276 | ||
![]() |
f24d52436b | ||
![]() |
9de6e8846b | ||
![]() |
01a1213463 | ||
![]() |
f0fbd9214a | ||
![]() |
c4f37c550f | ||
![]() |
448384af06 | ||
![]() |
3f840f53a0 | ||
![]() |
d8718d8ac8 | ||
![]() |
2fb46a11dc | ||
![]() |
9a11412719 | ||
![]() |
98874be171 | ||
![]() |
704f91545e | ||
![]() |
efb3239cbd | ||
![]() |
7e7ddeb9e2 | ||
![]() |
9e8218089b | ||
![]() |
3f660a3963 | ||
![]() |
daeb6711b0 | ||
![]() |
4e1aec28a0 | ||
![]() |
5512917ec1 | ||
![]() |
cd1edc5d56 | ||
![]() |
4f52587586 | ||
![]() |
d7ee4ef5f5 | ||
![]() |
31f88e0f05 | ||
![]() |
9f1740cc4f | ||
![]() |
f2c15c7701 | ||
![]() |
e67d0678f9 | ||
![]() |
b1faa5eed4 | ||
![]() |
7f1f0b9048 | ||
![]() |
183e5f2ecc | ||
![]() |
14efe4939a | ||
![]() |
3dc7d77ea9 | ||
![]() |
0f07bbb3e5 | ||
![]() |
dd5a3416bf | ||
![]() |
2fb49ad780 | ||
![]() |
92f0e53fee | ||
![]() |
876132694d | ||
![]() |
1257ba41c6 | ||
![]() |
2cc71ac7ed | ||
![]() |
753808a4ce | ||
![]() |
32cd694ad5 | ||
![]() |
f008420891 | ||
![]() |
fa8900be65 | ||
![]() |
69c2f407d6 | ||
![]() |
ffcd093db1 | ||
![]() |
8dbf93750f | ||
![]() |
e266a81167 | ||
![]() |
e841aab9e7 | ||
![]() |
49f259065d | ||
![]() |
b10379e700 | ||
![]() |
810d27a618 | ||
![]() |
9b60c005c7 | ||
![]() |
cc6ca0bda2 | ||
![]() |
4512232637 | ||
![]() |
2c092ffdef | ||
![]() |
66406227d6 | ||
![]() |
a11d25bb44 | ||
![]() |
2e58d902b7 | ||
![]() |
237794b05c | ||
![]() |
563a587882 | ||
![]() |
24505cd111 | ||
![]() |
0c681cdab4 | ||
![]() |
13ef3058c6 | ||
![]() |
50b159b43d | ||
![]() |
8c6c328730 | ||
![]() |
c9812ddf08 | ||
![]() |
2ef0449c2c | ||
![]() |
5edc750c47 | ||
![]() |
2f0e396d7f | ||
![]() |
000a163beb | ||
![]() |
80dd37ee31 | ||
![]() |
e0b5645064 | ||
![]() |
e51aacb0b7 | ||
![]() |
2d6af94aa0 | ||
![]() |
7cfce9ff7a | ||
![]() |
7f088d6241 | ||
![]() |
d11038f3de | ||
![]() |
6df42a4be7 | ||
![]() |
7fd111b91f | ||
![]() |
dd7dc2ec5a | ||
![]() |
86c586d882 | ||
![]() |
66ac6f72fc | ||
![]() |
f21f448099 | ||
![]() |
548d70f30c | ||
![]() |
39e714c6d8 | ||
![]() |
9968af0785 | ||
![]() |
be7586137c | ||
![]() |
7999b66c3c | ||
![]() |
c82a46c1ee | ||
![]() |
666ab1941f | ||
![]() |
71e37345b4 | ||
![]() |
e7c82f20e3 | ||
![]() |
afa771a980 | ||
![]() |
0d1de98cca | ||
![]() |
02bf7dca01 | ||
![]() |
8cc76b1d86 | ||
![]() |
77a275cbcd | ||
![]() |
3956cbe2d2 | ||
![]() |
945de8d9a0 | ||
![]() |
6dabd3bb2d | ||
![]() |
4c80808997 | ||
![]() |
5a39f7cdde | ||
![]() |
5d400fbe90 | ||
![]() |
e36596470c | ||
![]() |
668e549208 | ||
![]() |
256ff31d11 | ||
![]() |
2414d5d7f5 | ||
![]() |
b7fc15d399 | ||
![]() |
c09b4dabc4 | ||
![]() |
a4aa4a91a3 | ||
![]() |
8f0ea5925a | ||
![]() |
936ad1aa20 | ||
![]() |
d021bca6ef | ||
![]() |
55ed6109c1 | ||
![]() |
f6d765bf81 | ||
![]() |
88e8f2bf83 | ||
![]() |
c849759682 | ||
![]() |
605eae21bc | ||
![]() |
93eb277a88 | ||
![]() |
8edf556c9e | ||
![]() |
7fcb63230f | ||
![]() |
12093a3dad | ||
![]() |
ebb0ec6c42 | ||
![]() |
188546515c | ||
![]() |
c8990b0f68 | ||
![]() |
7dced4b9d9 | ||
![]() |
3145e67feb | ||
![]() |
e9348d9b6a | ||
![]() |
1a1b346c05 | ||
![]() |
920d059837 | ||
![]() |
bef5c3bd1b | ||
![]() |
97037f7d03 | ||
![]() |
a7392ed3d7 | ||
![]() |
3eb1a7e384 | ||
![]() |
1ecdc78c2f | ||
![]() |
d279dba37e | ||
![]() |
a4f97fa151 | ||
![]() |
ff7ac582f0 | ||
![]() |
d2c2456fbe | ||
![]() |
e9f562a8b7 | ||
![]() |
084e0a73dc | ||
![]() |
10f991b8d0 | ||
![]() |
79620c97d1 | ||
![]() |
ffec9a4ddd | ||
![]() |
9b18960bbd | ||
![]() |
a009fdbdc3 | ||
![]() |
c1fc3f373c | ||
![]() |
f4cf5dc0cd | ||
![]() |
355341f0ab | ||
![]() |
7f65f7d3ca | ||
![]() |
9fa096c6f4 | ||
![]() |
70415a396a | ||
![]() |
c921964938 | ||
![]() |
3bf47a6838 | ||
![]() |
d3d28f0623 | ||
![]() |
f880b57544 | ||
![]() |
32b7a26fa6 |
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
17
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,19 +1,18 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
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.
|
||||
All bug reports require you to **USE DEBUG 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 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 MAGISK**.
|
||||
|
||||
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.
|
||||
|
||||
@@ -31,7 +30,7 @@ Without following the rules above, your issue will be closed without explanation
|
||||
|
||||
-->
|
||||
|
||||
Device:
|
||||
Device:
|
||||
Android version:
|
||||
Magisk version name:
|
||||
Magisk version code:
|
||||
Magisk version name:
|
||||
Magisk version code:
|
||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -25,9 +25,6 @@
|
||||
[submodule "pcre"]
|
||||
path = native/jni/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
|
||||
@@ -43,3 +40,6 @@
|
||||
[submodule "zopfli"]
|
||||
path = native/jni/external/zopfli
|
||||
url = https://github.com/google/zopfli.git
|
||||
[submodule "cxx-rs"]
|
||||
path = native/jni/external/cxx-rs
|
||||
url = https://github.com/topjohnwu/cxx.git
|
||||
|
19
README.MD
19
README.MD
@@ -18,9 +18,10 @@ Some highlight features:
|
||||
|
||||
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v23.0)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.0)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.1)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.1)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||
|
||||
## Useful Links
|
||||
|
||||
@@ -30,7 +31,7 @@ Some highlight features:
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**Only bug reports from Canary builds will be accepted.**
|
||||
**Only bug reports from Debug builds will be accepted.**
|
||||
|
||||
For installation issues, upload both boot image and install logs.<br>
|
||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||
@@ -50,9 +51,15 @@ For Magisk app crashes, record and upload the logcat when the crash occurs.
|
||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||
- To start building, run `build.py` to see your options. \
|
||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources.
|
||||
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native sources.
|
||||
- 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).
|
||||
|
||||
## Signing and Distribution
|
||||
|
||||
- The certificate of the key used to sign the final Magisk APK product is also directly embedded into some executables. In release builds, Magisk's root daemon will enforce this certificate check and reject and forcefully uninstall any non-matching Magisk apps to protect users from malicious and unverified Magisk APKs.
|
||||
- To do any development on Magisk itself, switch to an **official debug build and reinstall Magisk** to bypass the signature check.
|
||||
- To distribute your own Magisk builds signed with your own keys, set your signing configs in `config.prop`.
|
||||
- Check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key) for more details on generating your own key.
|
||||
|
||||
## Translation Contributions
|
||||
|
||||
|
@@ -19,6 +19,8 @@ kapt {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.topjohnwu.magisk"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -54,11 +56,6 @@ android {
|
||||
keepDebugSymbols += "**/*.so"
|
||||
}
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
||||
}
|
||||
}
|
||||
|
||||
setupApp()
|
||||
@@ -74,22 +71,17 @@ dependencies {
|
||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||
implementation("org.bouncycastle:bcpkix-jdk18on:1.71")
|
||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.1.1")
|
||||
implementation("dev.rikka.rikkax.insets:insets:1.2.0")
|
||||
implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
||||
implementation("io.noties.markwon:core:4.6.2")
|
||||
|
||||
val vBAdapt = "4.0.0"
|
||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
||||
implementation("${bindingAdapter}:${vBAdapt}")
|
||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
||||
|
||||
val vLibsu = "3.2.1"
|
||||
val vLibsu = "5.0.2"
|
||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||
|
||||
val vRetrofit = "2.9.0"
|
||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||
@@ -105,24 +97,24 @@ dependencies {
|
||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||
|
||||
val vRoom = "2.4.1"
|
||||
val vRoom = "2.5.0-alpha02"
|
||||
implementation("androidx.room:room-runtime:${vRoom}")
|
||||
implementation("androidx.room:room-ktx:${vRoom}")
|
||||
kapt("androidx.room:room-compiler:${vRoom}")
|
||||
|
||||
val vNav = "2.5.0-alpha01"
|
||||
val vNav = "2.5.0-rc01"
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.1.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.appcompat:appcompat:1.4.1")
|
||||
implementation("androidx.appcompat:appcompat:1.4.2")
|
||||
implementation("androidx.preference:preference:1.2.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||
implementation("androidx.transition:transition:1.4.1")
|
||||
implementation("androidx.core:core-ktx:1.7.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.core:core-ktx:1.8.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.0-rc01")
|
||||
implementation("com.google.android.material:material:1.6.1")
|
||||
}
|
||||
|
26
app/proguard-rules.pro
vendored
26
app/proguard-rules.pro
vendored
@@ -1,21 +1,3 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/topjohnwu/Library/Android/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Parcelable
|
||||
-keepclassmembers class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
@@ -26,6 +8,9 @@
|
||||
public static void check*(...);
|
||||
public static void throw*(...);
|
||||
}
|
||||
-assumenosideeffects class java.util.Objects {
|
||||
public static ** requireNonNull(...);
|
||||
}
|
||||
|
||||
# Stub
|
||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||
@@ -34,6 +19,11 @@
|
||||
boolean mActivityHandlesUiMode;
|
||||
}
|
||||
|
||||
# main
|
||||
-keep,allowoptimization public class com.topjohnwu.magisk.signing.SignBoot {
|
||||
public static void main(java.lang.String[]);
|
||||
}
|
||||
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber$Tree {
|
||||
public void v(**);
|
||||
|
@@ -5,9 +5,7 @@ plugins {
|
||||
setupCommon()
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
consumerProguardFiles("proguard-rules.pro")
|
||||
}
|
||||
namespace = "com.topjohnwu.shared"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
21
app/shared/proguard-rules.pro
vendored
21
app/shared/proguard-rules.pro
vendored
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@@ -1,72 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public class DynAPK {
|
||||
private static File dynDir;
|
||||
private static Method addAssetPath;
|
||||
|
||||
private static File getDynDir(Context c) {
|
||||
if (dynDir == null) {
|
||||
if (SDK_INT >= 24) {
|
||||
// Use protected context to allow directBootAware
|
||||
c = c.createDeviceProtectedStorageContext();
|
||||
}
|
||||
dynDir = new File(c.getFilesDir().getParent(), "dyn");
|
||||
dynDir.mkdir();
|
||||
}
|
||||
return dynDir;
|
||||
}
|
||||
|
||||
public static File current(Context c) {
|
||||
return new File(getDynDir(c), "current.apk");
|
||||
}
|
||||
|
||||
public static File update(Context c) {
|
||||
return new File(getDynDir(c), "update.apk");
|
||||
}
|
||||
|
||||
public static void addAssetPath(AssetManager asset, String path) {
|
||||
try {
|
||||
if (addAssetPath == null)
|
||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||
addAssetPath.invoke(asset, path);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
// Indices of the object array
|
||||
private static final int STUB_VERSION = 0;
|
||||
private static final int CLASS_COMPONENT_MAP = 1;
|
||||
private static final int ROOT_SERVICE = 2;
|
||||
private static final int ARR_SIZE = 3;
|
||||
|
||||
private final Object[] arr;
|
||||
|
||||
public Data() { arr = new Object[ARR_SIZE]; }
|
||||
public Data(Object o) { arr = (Object[]) o; }
|
||||
public Object getObject() { return arr; }
|
||||
|
||||
public int getVersion() { return (int) arr[STUB_VERSION]; }
|
||||
public void setVersion(int version) { arr[STUB_VERSION] = version; }
|
||||
public Map<String, String> getClassToComponent() {
|
||||
// noinspection unchecked
|
||||
return (Map<String, String>) arr[CLASS_COMPONENT_MAP];
|
||||
}
|
||||
public void setClassToComponent(Map<String, String> map) {
|
||||
arr[CLASS_COMPONENT_MAP] = map;
|
||||
}
|
||||
public Class<?> getRootService() { return (Class<?>) arr[ROOT_SERVICE]; }
|
||||
public void setRootService(Class<?> service) { arr[ROOT_SERVICE] = service; }
|
||||
}
|
||||
}
|
109
app/shared/src/main/java/com/topjohnwu/magisk/StubApk.java
Normal file
109
app/shared/src/main/java/com/topjohnwu/magisk/StubApk.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.res.AssetManager;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.loader.ResourcesLoader;
|
||||
import android.content.res.loader.ResourcesProvider;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public class StubApk {
|
||||
private static File dynDir;
|
||||
private static Method addAssetPath;
|
||||
|
||||
private static File getDynDir(ApplicationInfo info) {
|
||||
if (dynDir == null) {
|
||||
final String dataDir;
|
||||
if (SDK_INT >= 24) {
|
||||
// Use device protected path to allow directBootAware
|
||||
dataDir = info.deviceProtectedDataDir;
|
||||
} else {
|
||||
dataDir = info.dataDir;
|
||||
}
|
||||
dynDir = new File(dataDir, "dyn");
|
||||
dynDir.mkdirs();
|
||||
}
|
||||
return dynDir;
|
||||
}
|
||||
|
||||
public static File current(Context c) {
|
||||
return new File(getDynDir(c.getApplicationInfo()), "current.apk");
|
||||
}
|
||||
|
||||
public static File current(ApplicationInfo info) {
|
||||
return new File(getDynDir(info), "current.apk");
|
||||
}
|
||||
|
||||
public static File update(Context c) {
|
||||
return new File(getDynDir(c.getApplicationInfo()), "update.apk");
|
||||
}
|
||||
|
||||
public static File update(ApplicationInfo info) {
|
||||
return new File(getDynDir(info), "update.apk");
|
||||
}
|
||||
|
||||
public static void addAssetPath(Resources res, String path) {
|
||||
if (SDK_INT >= 30) {
|
||||
try (var fd = ParcelFileDescriptor.open(new File(path), MODE_READ_ONLY)) {
|
||||
var loader = new ResourcesLoader();
|
||||
loader.addProvider(ResourcesProvider.loadFromApk(fd));
|
||||
res.addLoaders(loader);
|
||||
} catch (IOException ignored) {}
|
||||
} else {
|
||||
AssetManager asset = res.getAssets();
|
||||
try {
|
||||
if (addAssetPath == null)
|
||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||
addAssetPath.invoke(asset, path);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public static void restartProcess(Activity activity) {
|
||||
Intent intent = activity.getPackageManager()
|
||||
.getLaunchIntentForPackage(activity.getPackageName());
|
||||
activity.finishAffinity();
|
||||
activity.startActivity(intent);
|
||||
Runtime.getRuntime().exit(0);
|
||||
}
|
||||
|
||||
public static class Data {
|
||||
// Indices of the object array
|
||||
private static final int STUB_VERSION = 0;
|
||||
private static final int CLASS_COMPONENT_MAP = 1;
|
||||
private static final int ROOT_SERVICE = 2;
|
||||
private static final int ARR_SIZE = 3;
|
||||
|
||||
private final Object[] arr;
|
||||
|
||||
public Data() { arr = new Object[ARR_SIZE]; }
|
||||
public Data(Object o) { arr = (Object[]) o; }
|
||||
public Object getObject() { return arr; }
|
||||
|
||||
public int getVersion() { return (int) arr[STUB_VERSION]; }
|
||||
public void setVersion(int version) { arr[STUB_VERSION] = version; }
|
||||
public Map<String, String> getClassToComponent() {
|
||||
// noinspection unchecked
|
||||
return (Map<String, String>) arr[CLASS_COMPONENT_MAP];
|
||||
}
|
||||
public void setClassToComponent(Map<String, String> map) {
|
||||
arr[CLASS_COMPONENT_MAP] = map;
|
||||
}
|
||||
public Class<?> getRootService() { return (Class<?>) arr[ROOT_SERVICE]; }
|
||||
public void setRootService(Class<?> service) { arr[ROOT_SERVICE] = service; }
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import static android.content.pm.PackageInstaller.EXTRA_SESSION_ID;
|
||||
import static android.content.pm.PackageInstaller.EXTRA_STATUS;
|
||||
import static android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID;
|
||||
import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
|
||||
@@ -10,17 +11,17 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageInstaller.Session;
|
||||
import android.content.pm.PackageInstaller.SessionParams;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -28,29 +29,6 @@ import io.michaelrocks.paranoid.Obfuscate;
|
||||
|
||||
@Obfuscate
|
||||
public final class APKInstall {
|
||||
// @WorkerThread
|
||||
public static void installapk(Context context, File apk) {
|
||||
//noinspection InlinedApi
|
||||
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
|
||||
var action = APKInstall.class.getName();
|
||||
var intent = new Intent(action).setPackage(context.getPackageName());
|
||||
var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
|
||||
|
||||
var installer = context.getPackageManager().getPackageInstaller();
|
||||
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
|
||||
}
|
||||
try (Session session = installer.openSession(installer.createSession(params))) {
|
||||
OutputStream out = session.openWrite(apk.getName(), 0, apk.length());
|
||||
try (var in = new FileInputStream(apk); out) {
|
||||
transfer(in, out);
|
||||
}
|
||||
session.commit(pending.getIntentSender());
|
||||
} catch (IOException e) {
|
||||
Log.e(APKInstall.class.getSimpleName(), "", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void transfer(InputStream in, OutputStream out) throws IOException {
|
||||
int size = 8192;
|
||||
@@ -61,61 +39,136 @@ public final class APKInstall {
|
||||
}
|
||||
}
|
||||
|
||||
public static InstallReceiver register(Context context, String packageName, Runnable onSuccess) {
|
||||
var receiver = new InstallReceiver(context, packageName, onSuccess);
|
||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||
filter.addDataScheme("package");
|
||||
context.registerReceiver(receiver, filter);
|
||||
context.registerReceiver(receiver, new IntentFilter(APKInstall.class.getName()));
|
||||
public static Session startSession(Context context) {
|
||||
return startSession(context, null, null, null);
|
||||
}
|
||||
|
||||
public static Session startSession(Context context, String pkg,
|
||||
Runnable onFailure, Runnable onSuccess) {
|
||||
var receiver = new InstallReceiver(pkg, onSuccess, onFailure);
|
||||
context = context.getApplicationContext();
|
||||
if (pkg != null) {
|
||||
// If pkg is not null, look for package added event
|
||||
var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
||||
filter.addDataScheme("package");
|
||||
context.registerReceiver(receiver, filter);
|
||||
}
|
||||
context.registerReceiver(receiver, new IntentFilter(receiver.sessionId));
|
||||
return receiver;
|
||||
}
|
||||
|
||||
public static class InstallReceiver extends BroadcastReceiver {
|
||||
private final Context context;
|
||||
public interface Session {
|
||||
// @WorkerThread
|
||||
OutputStream openStream(Context context) throws IOException;
|
||||
// @WorkerThread
|
||||
void install(Context context, File apk) throws IOException;
|
||||
// @WorkerThread @Nullable
|
||||
Intent waitIntent();
|
||||
}
|
||||
|
||||
private static class InstallReceiver extends BroadcastReceiver implements Session {
|
||||
private final String packageName;
|
||||
private final Runnable onSuccess;
|
||||
private final Runnable onFailure;
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private Intent intent = null;
|
||||
private Intent userAction = null;
|
||||
|
||||
private InstallReceiver(Context context, String packageName, Runnable onSuccess) {
|
||||
this.context = context;
|
||||
final String sessionId = UUID.randomUUID().toString();
|
||||
|
||||
private InstallReceiver(String packageName, Runnable onSuccess, Runnable onFailure) {
|
||||
this.packageName = packageName;
|
||||
this.onSuccess = onSuccess;
|
||||
this.onFailure = onFailure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context c, Intent i) {
|
||||
if (Intent.ACTION_PACKAGE_ADDED.equals(i.getAction())) {
|
||||
Uri data = i.getData();
|
||||
if (data == null || onSuccess == null) return;
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
|
||||
Uri data = intent.getData();
|
||||
if (data == null)
|
||||
return;
|
||||
String pkg = data.getSchemeSpecificPart();
|
||||
if (pkg.equals(packageName)) {
|
||||
onSuccess.run();
|
||||
context.unregisterReceiver(this);
|
||||
onSuccess(context);
|
||||
}
|
||||
return;
|
||||
} else if (sessionId.equals(intent.getAction())) {
|
||||
int status = intent.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||
switch (status) {
|
||||
case STATUS_PENDING_USER_ACTION:
|
||||
userAction = intent.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
break;
|
||||
case STATUS_SUCCESS:
|
||||
if (packageName == null) {
|
||||
onSuccess(context);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
int id = intent.getIntExtra(EXTRA_SESSION_ID, 0);
|
||||
if (id > 0) {
|
||||
var installer = context.getPackageManager().getPackageInstaller();
|
||||
var info = installer.getSessionInfo(id);
|
||||
if (info != null) {
|
||||
installer.abandonSession(info.getSessionId());
|
||||
}
|
||||
}
|
||||
if (onFailure != null) {
|
||||
onFailure.run();
|
||||
}
|
||||
context.getApplicationContext().unregisterReceiver(this);
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
int status = i.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
|
||||
switch (status) {
|
||||
case STATUS_PENDING_USER_ACTION:
|
||||
intent = i.getParcelableExtra(Intent.EXTRA_INTENT);
|
||||
break;
|
||||
case STATUS_SUCCESS:
|
||||
if (onSuccess != null) onSuccess.run();
|
||||
default:
|
||||
context.unregisterReceiver(this);
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
// @WorkerThread @Nullable
|
||||
private void onSuccess(Context context) {
|
||||
if (onSuccess != null)
|
||||
onSuccess.run();
|
||||
context.getApplicationContext().unregisterReceiver(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent waitIntent() {
|
||||
try {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
latch.await(5, TimeUnit.SECONDS);
|
||||
} catch (Exception ignored) {
|
||||
} catch (Exception ignored) {}
|
||||
return userAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream openStream(Context context) throws IOException {
|
||||
// noinspection InlinedApi
|
||||
var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
|
||||
var intent = new Intent(sessionId).setPackage(context.getPackageName());
|
||||
var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
|
||||
|
||||
var installer = context.getPackageManager().getPackageInstaller();
|
||||
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
|
||||
}
|
||||
var session = installer.openSession(installer.createSession(params));
|
||||
var out = session.openWrite(sessionId, 0, -1);
|
||||
return new FilterOutputStream(out) {
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
out.write(b, off, len);
|
||||
}
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
session.commit(pending.getIntentSender());
|
||||
session.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void install(Context context, File apk) throws IOException {
|
||||
try (var src = new FileInputStream(apk);
|
||||
var out = openStream(context)) {
|
||||
transfer(src, out);
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,18 +5,17 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import dalvik.system.DexClassLoader;
|
||||
import dalvik.system.BaseDexClassLoader;
|
||||
|
||||
public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
private static final ClassLoader base = Object.class.getClassLoader();
|
||||
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||
|
||||
public DynamicClassLoader(File apk) {
|
||||
super(apk.getPath(), apk.getParent(), null, base);
|
||||
this(apk, getSystemClassLoader());
|
||||
}
|
||||
|
||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
||||
super(apk.getPath(), apk.getParent(), null, parent);
|
||||
// Set optimizedDirectory to null to bypass DexFile's security checks
|
||||
super(apk.getPath(), null, null, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,7 +27,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
try {
|
||||
// Then check boot classpath
|
||||
return base.loadClass(name);
|
||||
return getSystemClassLoader().loadClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
try {
|
||||
// Next try current dex
|
||||
@@ -46,7 +45,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
URL resource = base.getResource(name);
|
||||
URL resource = getSystemClassLoader().getResource(name);
|
||||
if (resource != null)
|
||||
return resource;
|
||||
resource = findResource(name);
|
||||
@@ -58,7 +57,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
return new CompoundEnumeration<>(base.getResources(name),
|
||||
return new CompoundEnumeration<>(getSystemClassLoader().getResources(name),
|
||||
findResources(name), getParent().getResources(name));
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".core.App"
|
||||
@@ -29,8 +26,8 @@
|
||||
<activity
|
||||
android:name=".ui.surequest.SuRequestActivity"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -40,11 +37,11 @@
|
||||
|
||||
<receiver
|
||||
android:name=".core.Receiver"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||
<action android:name="android.intent.action.UID_REMOVED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||
|
@@ -0,0 +1,9 @@
|
||||
// IRootUtils.aidl
|
||||
package com.topjohnwu.magisk.core.utils;
|
||||
|
||||
// Declare any non-default types here with import statements
|
||||
|
||||
interface IRootUtils {
|
||||
android.app.ActivityManager.RunningAppProcessInfo getAppProcess(int pid);
|
||||
IBinder getFileSystem();
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class AsyncLoadViewModel : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
@MainThread
|
||||
fun startLoading() {
|
||||
if (loadingJob?.isActive == true) {
|
||||
// Prevent multiple jobs from running at the same time
|
||||
return
|
||||
}
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
protected abstract suspend fun doLoadWork()
|
||||
}
|
@@ -20,11 +20,12 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
protected abstract val layoutRes: Int
|
||||
|
||||
private val navigation get() = activity?.navigation
|
||||
open val snackbarView: View? get() = null
|
||||
open val snackbarAnchorView: View? get() = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
startObserveEvents()
|
||||
startObserveLiveData()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -36,9 +37,14 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = viewLifecycleOwner
|
||||
}
|
||||
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
viewModel.onSaveState(outState)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity?.supportActionBar?.subtitle = null
|
||||
@@ -70,7 +76,10 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
viewModel.let {
|
||||
if (it is AsyncLoadViewModel)
|
||||
it.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onPreBind(binding: Binding) {
|
||||
@@ -78,7 +87,7 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
||||
}
|
||||
|
||||
fun NavDirections.navigate() {
|
||||
navigation?.navigate(this)
|
||||
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,129 +0,0 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||
|
||||
companion object {
|
||||
private var doPreload = true
|
||||
}
|
||||
|
||||
private val latch = CountDownLatch(1)
|
||||
private val uninstallPkg = registerForActivityResult(UninstallPackage) { latch.countDown() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(Theme.selected.themeRes)
|
||||
|
||||
if (isRunningAsStub && doPreload) {
|
||||
// Manually apply splash theme for stub
|
||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!isRunningAsStub) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { doPreload }
|
||||
}
|
||||
|
||||
if (doPreload) {
|
||||
Shell.getShell(null) {
|
||||
if (isRunningAsStub && !it.isRoot) {
|
||||
showInvalidStateMessage()
|
||||
return@getShell
|
||||
}
|
||||
preLoad()
|
||||
runOnUiThread {
|
||||
doPreload = false
|
||||
if (isRunningAsStub) {
|
||||
// Re-launch main activity without splash theme
|
||||
relaunch()
|
||||
} else {
|
||||
showMainUI(savedInstanceState)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showMainUI(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
||||
|
||||
private fun showInvalidStateMessage() {
|
||||
runOnUiThread {
|
||||
MagiskDialog(this).apply {
|
||||
setTitle(R.string.unsupport_nonroot_stub_title)
|
||||
setMessage(R.string.unsupport_nonroot_stub_msg)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.install
|
||||
onClick { HideAPK.restore(this@BaseMainActivity) }
|
||||
}
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun preLoad() {
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
||||
|
||||
Config.load(prevPkg)
|
||||
handleRepackage(prevPkg)
|
||||
Notifications.setup(this)
|
||||
JobService.schedule(this)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
// Pre-fetch network services
|
||||
ServiceLocator.networkService
|
||||
}
|
||||
|
||||
private fun handleRepackage(pkg: String?) {
|
||||
if (packageName != APPLICATION_ID) {
|
||||
runCatching {
|
||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
||||
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) {
|
||||
uninstallPkg.launch(pkg)
|
||||
// Wait for the uninstallation to finish
|
||||
latch.await()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object UninstallPackage : ActivityResultContract<String, Boolean>() {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val uri = Uri.Builder().scheme("package").opaquePart(input).build()
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
|
||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) = resultCode == RESULT_OK
|
||||
}
|
||||
}
|
@@ -1,84 +1,39 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.Manifest
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.Observable
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||
import com.topjohnwu.magisk.databinding.set
|
||||
import com.topjohnwu.magisk.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.events.NavigationEvent
|
||||
import com.topjohnwu.magisk.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import kotlinx.coroutines.Job
|
||||
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : ViewModel(), ObservableHost {
|
||||
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
enum class State {
|
||||
LOADED, LOADING, LOADING_FAILED
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
val loading get() = state == State.LOADING
|
||||
@get:Bindable
|
||||
val loaded get() = state == State.LOADED
|
||||
@get:Bindable
|
||||
val loadFailed get() = state == State.LOADING_FAILED
|
||||
|
||||
val isConnected get() = Info.isConnected
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
var state= initialState
|
||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
||||
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
private var runningJob: Job? = null
|
||||
private val refreshCallback = object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
requestRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
isConnected.addOnPropertyChangedCallback(refreshCallback)
|
||||
}
|
||||
|
||||
/** This should probably never be called manually, it's called manually via delegate. */
|
||||
@Synchronized
|
||||
fun requestRefresh() {
|
||||
if (runningJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
runningJob = refresh()
|
||||
}
|
||||
|
||||
protected open fun refresh(): Job? = null
|
||||
|
||||
@CallSuper
|
||||
override fun onCleared() {
|
||||
isConnected.removeOnPropertyChangedCallback(refreshCallback)
|
||||
super.onCleared()
|
||||
}
|
||||
open fun onSaveState(state: Bundle) {}
|
||||
open fun onRestoreState(state: Bundle) {}
|
||||
open fun onNetworkChanged(network: Boolean) {}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
PermissionEvent(permission, callback).publish()
|
||||
}
|
||||
|
||||
inline fun withExternalRW(crossinline callback: () -> Unit) {
|
||||
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
||||
withPermission(WRITE_EXTERNAL_STORAGE) {
|
||||
if (!it) {
|
||||
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
||||
} else {
|
||||
@@ -87,6 +42,17 @@ abstract class BaseViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
inline fun withInstallPermission(crossinline callback: () -> Unit) {
|
||||
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||
if (!it) {
|
||||
SnackbarEvent(R.string.install_unknown_denied).publish()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun back() = BackPressEvent().publish()
|
||||
|
||||
fun <Event : ViewEvent> Event.publish() {
|
||||
|
@@ -10,9 +10,11 @@ import androidx.core.content.res.use
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.widget.Pre23CardViewBackgroundColorFixLayoutInflaterListener
|
||||
import rikka.insets.WindowInsetsHelper
|
||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||
|
||||
@@ -25,17 +27,21 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
open val snackbarAnchorView: View? get() = null
|
||||
|
||||
init {
|
||||
val theme = Config.darkTheme
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
this.addOnViewCreatedListener(Pre23CardViewBackgroundColorFixLayoutInflaterListener.getInstance())
|
||||
}
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
startObserveEvents()
|
||||
startObserveLiveData()
|
||||
|
||||
// We need to set the window background explicitly since for whatever reason it's not
|
||||
// propagated upstream
|
||||
@@ -74,9 +80,19 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
||||
binding.root.rootView.accessibilityDelegate = delegate
|
||||
}
|
||||
|
||||
fun showSnackbar(
|
||||
message: CharSequence,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
builder: Snackbar.() -> Unit = {}
|
||||
) = Snackbar.make(snackbarView, message, length)
|
||||
.setAnchorView(snackbarAnchorView).apply(builder).show()
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.requestRefresh()
|
||||
viewModel.let {
|
||||
if (it is AsyncLoadViewModel)
|
||||
it.startLoading()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||
|
@@ -1,15 +1,24 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
|
||||
interface ViewModelHolder : LifecycleOwner {
|
||||
interface ViewModelHolder : LifecycleOwner, ViewModelStoreOwner {
|
||||
|
||||
val viewModel: BaseViewModel
|
||||
|
||||
fun startObserveEvents() {
|
||||
viewModel.viewEvents.observe(this) {
|
||||
onEventDispatched(it)
|
||||
}
|
||||
fun startObserveLiveData() {
|
||||
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,3 +26,23 @@ interface ViewModelHolder : LifecycleOwner {
|
||||
*/
|
||||
fun onEventDispatched(event: ViewEvent) {}
|
||||
}
|
||||
|
||||
object VMFactory : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when (modelClass) {
|
||||
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||
InstallViewModel::class.java -> InstallViewModel(ServiceLocator.networkService)
|
||||
SuRequestViewModel::class.java ->
|
||||
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified VM : ViewModel> ViewModelHolder.viewModel() =
|
||||
lazy(LazyThreadSafetyMode.NONE) {
|
||||
ViewModelProvider(this, VMFactory)[VM::class.java]
|
||||
}
|
||||
|
@@ -1,29 +1,30 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.*
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
open class App() : Application() {
|
||||
|
||||
constructor(o: Any) : this() {
|
||||
val data = DynAPK.Data(o)
|
||||
val data = StubApk.Data(o)
|
||||
// Add the root service name mapping
|
||||
data.classToComponent[RootRegistry::class.java.name] = data.rootService.name
|
||||
data.classToComponent[RootUtils::class.java.name] = data.rootService.name
|
||||
// Send back the actual root service class
|
||||
data.rootService = RootRegistry::class.java
|
||||
data.rootService = RootUtils::class.java
|
||||
Info.stub = data
|
||||
}
|
||||
|
||||
@@ -37,43 +38,38 @@ open class App() : Application() {
|
||||
}
|
||||
|
||||
override fun attachBaseContext(context: Context) {
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(ShellInit::class.java)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||
|
||||
// Get the actual ContextImpl
|
||||
val app: Application
|
||||
val base: Context
|
||||
if (context is Application) {
|
||||
app = context
|
||||
base = context.baseContext
|
||||
AppApkPath = StubApk.current(base).path
|
||||
} else {
|
||||
app = this
|
||||
base = context
|
||||
AppApkPath = base.packageResourcePath
|
||||
}
|
||||
super.attachBaseContext(base)
|
||||
ServiceLocator.context = base
|
||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
||||
.setInitializers(ShellInit::class.java)
|
||||
.setContext(base)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
|
||||
RootUtils.bindTask = RootService.bindOrTask(
|
||||
intent<RootUtils>(),
|
||||
UiThreadHandler.executor,
|
||||
RootUtils.Connection
|
||||
)
|
||||
// Pre-heat the shell ASAP
|
||||
Shell.getShell(null) {}
|
||||
|
||||
refreshLocale()
|
||||
AppApkPath = if (isRunningAsStub) {
|
||||
DynAPK.current(base).path
|
||||
} else {
|
||||
base.packageResourcePath
|
||||
}
|
||||
|
||||
base.resources.patch()
|
||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
RootRegistry.bindTask = RootService.createBindTask(
|
||||
intent<RootRegistry>(),
|
||||
UiThreadHandler.executor,
|
||||
RootRegistry.Connection
|
||||
)
|
||||
resources.patch()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
@@ -85,20 +81,21 @@ open class App() : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
val foreground: Activity? get() = ref.get()
|
||||
|
||||
val hasForeground get() = foreground != null
|
||||
@Volatile
|
||||
private var ref = WeakReference<Activity>(null)
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
if (activity is SuRequestActivity) return
|
||||
ref = WeakReference(activity)
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
if (activity is SuRequestActivity) return
|
||||
ref.clear()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
@@ -6,21 +6,23 @@ import android.util.Xml
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.repository.BoolDBPropertyNoWrite
|
||||
import com.topjohnwu.magisk.core.repository.DBConfig
|
||||
import com.topjohnwu.magisk.core.repository.PreferenceConfig
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.data.repository.DBBoolSettingsNoWrite
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
object Config : PreferenceConfig, DBConfig {
|
||||
|
||||
override val stringDB get() = ServiceLocator.stringDB
|
||||
override val settingsDB get() = ServiceLocator.settingsDB
|
||||
override val context get() = ServiceLocator.deContext
|
||||
override val coroutineScope get() = GlobalScope
|
||||
|
||||
@get:SuppressLint("ApplySharedPref")
|
||||
val prefsFile: File get() {
|
||||
@@ -70,6 +72,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
@@ -106,6 +109,8 @@ object Config : PreferenceModel, DBConfig {
|
||||
|
||||
private val defaultChannel =
|
||||
if (BuildConfig.DEBUG)
|
||||
Value.DEBUG_CHANNEL
|
||||
else if (Const.APP_IS_CANARY)
|
||||
Value.CANARY_CHANNEL
|
||||
else
|
||||
Value.DEFAULT_CHANNEL
|
||||
@@ -149,7 +154,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||
var zygisk by dbSettings(Key.ZYGISK, false)
|
||||
var denyList by DBBoolSettingsNoWrite(Key.DENYLIST, false)
|
||||
var denyList by BoolDBPropertyNoWrite(Key.DENYLIST, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||
|
||||
@@ -158,7 +163,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
fun load(pkg: String?) {
|
||||
// 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.preferencesUri(pkg))?.use {
|
||||
prefs.edit { parsePrefs(it) }
|
||||
}
|
||||
}
|
||||
@@ -169,10 +174,11 @@ object Config : PreferenceModel, DBConfig {
|
||||
suBiometric = true
|
||||
remove(SU_FINGERPRINT)
|
||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
||||
if (it == null)
|
||||
if (it == null ||
|
||||
it.toInt() > Value.DEBUG_CHANNEL ||
|
||||
it.toInt() < Value.DEFAULT_CHANNEL) {
|
||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||
else if (it.toInt() > Value.CANARY_CHANNEL)
|
||||
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -25,11 +25,11 @@ object Const {
|
||||
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||
|
||||
object Version {
|
||||
const val MIN_VERSION = "v21.0"
|
||||
const val MIN_VERCODE = 21000
|
||||
const val MIN_VERSION = "v22.0"
|
||||
const val MIN_VERCODE = 22000
|
||||
|
||||
fun atLeast_21_2() = Info.env.versionCode >= 21200 || isCanary()
|
||||
fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||
fun isCanary() = isCanary(Info.env.versionCode)
|
||||
|
||||
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||
@@ -37,8 +37,6 @@ object Const {
|
||||
|
||||
object ID {
|
||||
const val JOB_SERVICE_ID = 7
|
||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||
}
|
||||
|
||||
object Url {
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
@@ -11,47 +10,51 @@ import android.content.res.AssetManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.util.DisplayMetrics
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.unwrap
|
||||
|
||||
lateinit var AppApkPath: String
|
||||
|
||||
fun AssetManager.addAssetPath(path: String) = DynAPK.addAssetPath(this, path)
|
||||
|
||||
fun Context.wrap(): Context = if (this is PatchedContext) this else PatchedContext(this)
|
||||
|
||||
private class PatchedContext(base: Context) : ContextWrapper(base) {
|
||||
init { base.resources.patch() }
|
||||
override fun getClassLoader() = javaClass.classLoader!!
|
||||
override fun createConfigurationContext(config: Configuration) =
|
||||
super.createConfigurationContext(config).wrap()
|
||||
}
|
||||
fun Resources.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
||||
|
||||
fun Resources.patch(): Resources {
|
||||
syncLocale()
|
||||
if (isRunningAsStub)
|
||||
assets.addAssetPath(AppApkPath)
|
||||
addAssetPath(AppApkPath)
|
||||
syncLocale()
|
||||
return this
|
||||
}
|
||||
|
||||
fun Context.patch(): Context {
|
||||
unwrap().resources.patch()
|
||||
return this
|
||||
}
|
||||
|
||||
// Wrapping is only necessary for ContextThemeWrapper to support configuration overrides
|
||||
fun Context.wrap(): Context {
|
||||
patch()
|
||||
return object : ContextWrapper(this) {
|
||||
override fun createConfigurationContext(config: Configuration): Context {
|
||||
return super.createConfigurationContext(config).wrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createNewResources(): Resources {
|
||||
val asset = AssetManager::class.java.newInstance()
|
||||
asset.addAssetPath(AppApkPath)
|
||||
val config = Configuration(AppContext.resources.configuration)
|
||||
val metrics = DisplayMetrics()
|
||||
metrics.setTo(AppContext.resources.displayMetrics)
|
||||
return Resources(asset, metrics, config)
|
||||
val res = Resources(asset, metrics, config)
|
||||
res.addAssetPath(AppApkPath)
|
||||
return res
|
||||
}
|
||||
|
||||
fun Class<*>.cmp(pkg: String) =
|
||||
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
||||
|
||||
inline fun <reified T> Activity.redirect() = Intent(intent)
|
||||
.setComponent(T::class.java.cmp(packageName))
|
||||
.setFlags(0)
|
||||
|
||||
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||
|
||||
// Keep a reference to these resources to prevent it from
|
||||
|
@@ -1,22 +1,21 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.os.Build
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.getProperty
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
val isRunningAsStub get() = Info.stub != null
|
||||
|
||||
object Info {
|
||||
|
||||
var stub: DynAPK.Data? = null
|
||||
var stub: StubApk.Data? = null
|
||||
|
||||
val EMPTY_REMOTE = UpdateInfo()
|
||||
var remote = EMPTY_REMOTE
|
||||
@@ -36,6 +35,7 @@ object Info {
|
||||
@JvmField var vbmeta = false
|
||||
var crypto = ""
|
||||
var noDataExec = false
|
||||
var isRooted = false
|
||||
|
||||
@JvmField var hasGMS = true
|
||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||
@@ -43,26 +43,31 @@ object Info {
|
||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||
getProperty("ro.boot.qemu", "0") == "1"
|
||||
|
||||
val isConnected by lazy {
|
||||
ObservableBoolean(false).also { field ->
|
||||
val isConnected: LiveData<Boolean> by lazy {
|
||||
MutableLiveData(false).also { field ->
|
||||
NetworkObserver.observe(AppContext) {
|
||||
UiThreadHandler.run { field.set(it) }
|
||||
remote = EMPTY_REMOTE
|
||||
field.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadState() = Env(
|
||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||
)
|
||||
private fun loadState(): Env {
|
||||
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||
return Env(
|
||||
v[0], v.size >= 3 && v[2] == "D",
|
||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||
)
|
||||
}
|
||||
|
||||
class Env(
|
||||
val versionString: String = "",
|
||||
val isDebug: Boolean = false,
|
||||
code: Int = -1
|
||||
) {
|
||||
val versionCode = when {
|
||||
code < Const.Version.MIN_VERCODE -> -1
|
||||
else -> if (Shell.rootAccess()) code else -1
|
||||
else -> if (isRooted) code else -1
|
||||
}
|
||||
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||
val isActive = versionCode >= 0
|
||||
|
@@ -7,7 +7,7 @@ import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -23,16 +23,20 @@ class JobService : BaseJobService() {
|
||||
override fun onStartJob(params: JobParameters): Boolean {
|
||||
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
|
||||
coroutineScope.launch {
|
||||
svc.fetchUpdate()?.run {
|
||||
Info.remote = this
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
|
||||
Notifications.managerUpdate(this@JobService)
|
||||
}
|
||||
doWork()
|
||||
jobFinished(params, false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun doWork() {
|
||||
svc.fetchUpdate()?.let {
|
||||
Info.remote = it
|
||||
if (Info.env.isActive && BuildConfig.VERSION_CODE < it.magisk.versionCode)
|
||||
Notifications.updateAvailable(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters): Boolean {
|
||||
job.cancel()
|
||||
return false
|
||||
|
@@ -1,22 +1,13 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import java.io.File
|
||||
|
||||
class Provider : ContentProvider() {
|
||||
|
||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||
super.attachInfo(context.wrap(), info)
|
||||
}
|
||||
class Provider : BaseProvider() {
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
SuCallbackHandler.run(context!!, method, extras)
|
||||
@@ -25,24 +16,13 @@ class Provider : ContentProvider() {
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
return when (uri.encodedPath ?: return null) {
|
||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||
else -> super.openFile(uri, mode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun APK_URI(pkg: String) =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
||||
|
||||
fun PREFS_URI(pkg: String) =
|
||||
fun preferencesUri(pkg: String): Uri =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||
}
|
||||
|
||||
override fun onCreate() = true
|
||||
override fun getType(uri: Uri): String? = null
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -25,8 +26,9 @@ open class Receiver : BaseReceiver() {
|
||||
return if (uid == -1) null else uid
|
||||
}
|
||||
|
||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
intent ?: return
|
||||
super.onReceive(context, intent)
|
||||
|
||||
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||
policyDB.delete(uid)
|
||||
@@ -42,9 +44,16 @@ open class Receiver : BaseReceiver() {
|
||||
getUid(intent)?.let { rmPolicy(it) }
|
||||
}
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
getPkg(intent)?.let { Shell.su("magisk --denylist rm $it").submit() }
|
||||
getPkg(intent)?.let { Shell.cmd("magisk --denylist rm $it").submit() }
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
|
||||
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
||||
@Suppress("DEPRECATION")
|
||||
val installer = context.packageManager.getInstallerPackageName(context.packageName)
|
||||
if (installer == context.packageName) {
|
||||
Notifications.updateDone(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,19 +1,32 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.GetContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ktx.reflectField
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
|
||||
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||
fun onActivityLaunch() {}
|
||||
// Make the result type explicitly non-null
|
||||
override fun onActivityResult(result: Uri)
|
||||
}
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
|
||||
@@ -22,17 +35,27 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
permissionCallback?.invoke(it)
|
||||
permissionCallback = null
|
||||
}
|
||||
private val requestInstall = registerForActivityResult(RequestInstall()) {
|
||||
permissionCallback?.invoke(it)
|
||||
permissionCallback = null
|
||||
}
|
||||
|
||||
private var contentCallback: ((Uri) -> Unit)? = null
|
||||
private var contentCallback: ContentResultCallback? = null
|
||||
private val getContent = registerForActivityResult(GetContent()) {
|
||||
if (it != null) contentCallback?.invoke(it)
|
||||
if (it != null) contentCallback?.onActivityResult(it)
|
||||
contentCallback = null
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
super.applyOverrideConfiguration(config)
|
||||
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||
Activity::class.java.reflectField("mReferrer")
|
||||
}
|
||||
|
||||
val realCallingPackage: String? get() {
|
||||
callingPackage?.let { return it }
|
||||
if (Build.VERSION.SDK_INT >= 22) {
|
||||
mReferrerField.get(this)?.let { return it as String }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@@ -47,9 +70,17 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
||||
}
|
||||
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
contentCallback?.let {
|
||||
outState.putParcelable(CONTENT_CALLBACK_KEY, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
||||
// We do not need external rw on 30+
|
||||
@@ -57,12 +88,21 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
return
|
||||
}
|
||||
permissionCallback = callback
|
||||
requestPermission.launch(permission)
|
||||
if (permission == REQUEST_INSTALL_PACKAGES) {
|
||||
requestInstall.launch(Unit)
|
||||
} else {
|
||||
requestPermission.launch(permission)
|
||||
}
|
||||
}
|
||||
|
||||
fun getContent(type: String, callback: (Uri) -> Unit) {
|
||||
fun getContent(type: String, callback: ContentResultCallback) {
|
||||
contentCallback = callback
|
||||
getContent.launch(type)
|
||||
try {
|
||||
getContent.launch(type)
|
||||
callback.onActivityLaunch()
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun recreate() {
|
||||
@@ -74,4 +114,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
startActivity(Intent(intent).setFlags(0))
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTENT_CALLBACK_KEY = "content_callback"
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@ package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.job.JobService
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseJobService : JobService() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
super.attachBaseContext(base.patch())
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,21 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
open class BaseProvider : ContentProvider() {
|
||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
||||
super.attachInfo(context.patch(), info)
|
||||
}
|
||||
override fun onCreate() = true
|
||||
override fun getType(uri: Uri): String? = null
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
|
||||
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
|
||||
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
|
||||
}
|
@@ -2,15 +2,13 @@ package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import androidx.annotation.CallSuper
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseReceiver : BroadcastReceiver() {
|
||||
|
||||
final override fun onReceive(context: Context, intent: Intent?) {
|
||||
onReceive(context.wrap() as ContextWrapper, intent)
|
||||
@CallSuper
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
context.patch()
|
||||
}
|
||||
|
||||
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
||||
}
|
||||
|
@@ -2,10 +2,13 @@ package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.magisk.core.patch
|
||||
|
||||
abstract class BaseService : Service() {
|
||||
open class BaseService : Service() {
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
super.attachBaseContext(base.patch())
|
||||
}
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
package com.topjohnwu.magisk.core.data
|
||||
|
||||
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
package com.topjohnwu.magisk.core.data
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
@@ -0,0 +1,47 @@
|
||||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
open class MagiskDB {
|
||||
|
||||
suspend fun <R> exec(
|
||||
query: String,
|
||||
mapper: suspend (Map<String, String>) -> R
|
||||
): List<R> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val out = Shell.cmd("magisk --sqlite '$query'").await().out
|
||||
out.map { line ->
|
||||
line.split("\\|".toRegex())
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.associate { it[0] to it[1] }
|
||||
.let { mapper(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun exec(query: String) {
|
||||
exec(query) {}
|
||||
}
|
||||
|
||||
fun Map<String, Any>.toQuery(): String {
|
||||
val keys = this.keys.joinToString(",")
|
||||
val values = this.values.joinToString(",") {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
return "($keys) VALUES($values)"
|
||||
}
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class PolicyDao : MagiskDB() {
|
||||
|
||||
suspend fun deleteOutdated() {
|
||||
val nowSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
|
||||
val query = "DELETE FROM ${Table.POLICY} WHERE " +
|
||||
"(until > 0 AND until < $nowSeconds) OR until < 0"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun delete(uid: Int) {
|
||||
val query = "DELETE FROM ${Table.POLICY} WHERE uid == $uid"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetch(uid: Int): SuPolicy? {
|
||||
val query = "SELECT * FROM ${Table.POLICY} WHERE uid == $uid LIMIT = 1"
|
||||
return exec(query, ::toPolicy).firstOrNull()
|
||||
}
|
||||
|
||||
suspend fun update(policy: SuPolicy) {
|
||||
val map = policy.toMap()
|
||||
if (!Const.Version.atLeast_25_0()) {
|
||||
// Put in package_name for old database
|
||||
map["package_name"] = AppContext.packageManager.getNameForUid(policy.uid)!!
|
||||
}
|
||||
val query = "REPLACE INTO ${Table.POLICY} ${map.toQuery()}"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetchAll(): List<SuPolicy> {
|
||||
val query = "SELECT * FROM ${Table.POLICY} WHERE uid/100000 == ${Const.USER_ID}"
|
||||
return exec(query, ::toPolicy).filterNotNull()
|
||||
}
|
||||
|
||||
private fun toPolicy(map: Map<String, String>): SuPolicy? {
|
||||
val uid = map["uid"]?.toInt() ?: return null
|
||||
val policy = SuPolicy(uid)
|
||||
|
||||
map["policy"]?.toInt()?.let { policy.policy = it }
|
||||
map["until"]?.toLong()?.let { policy.until = it }
|
||||
map["logging"]?.toInt()?.let { policy.logging = it != 0 }
|
||||
map["notification"]?.toInt()?.let { policy.notification = it != 0 }
|
||||
return policy
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
class SettingsDao : MagiskDB() {
|
||||
|
||||
suspend fun delete(key: String) {
|
||||
val query = "DELETE FROM ${Table.SETTINGS} WHERE key == \"$key\""
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun put(key: String, value: Int) {
|
||||
val kv = mapOf("key" to key, "value" to value)
|
||||
val query = "REPLACE INTO ${Table.SETTINGS} ${kv.toQuery()}"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetch(key: String, default: Int = -1): Int {
|
||||
val query = "SELECT value FROM ${Table.SETTINGS} WHERE key == \"$key\" LIMIT 1"
|
||||
return exec(query) { it["value"]?.toInt() }.firstOrNull() ?: default
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.topjohnwu.magisk.core.data.magiskdb
|
||||
|
||||
class StringDao : MagiskDB() {
|
||||
|
||||
suspend fun delete(key: String) {
|
||||
val query = "DELETE FROM ${Table.STRINGS} WHERE key == \"$key\""
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun put(key: String, value: String) {
|
||||
val kv = mapOf("key" to key, "value" to value)
|
||||
val query = "REPLACE INTO ${Table.STRINGS} ${kv.toQuery()}"
|
||||
exec(query)
|
||||
}
|
||||
|
||||
suspend fun fetch(key: String, default: String = ""): String {
|
||||
val query = "SELECT value FROM ${Table.STRINGS} WHERE key == \"$key\" LIMIT 1"
|
||||
return exec(query) { it["value"] }.firstOrNull() ?: default
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
package com.topjohnwu.magisk.core.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
@@ -1,25 +1,17 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
package com.topjohnwu.magisk.core.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.text.method.LinkMovementMethod
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||
|
||||
@@ -50,27 +42,8 @@ object ServiceLocator {
|
||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
||||
)
|
||||
}
|
||||
|
||||
object VMFactory : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when (modelClass) {
|
||||
HomeViewModel::class.java -> HomeViewModel(networkService)
|
||||
LogViewModel::class.java -> LogViewModel(logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(policyDB)
|
||||
InstallViewModel::class.java -> InstallViewModel(networkService)
|
||||
SuRequestViewModel::class.java -> SuRequestViewModel(policyDB, timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified VM : ViewModel> ViewModelStoreOwner.viewModel() =
|
||||
lazy(LazyThreadSafetyMode.NONE) {
|
||||
ViewModelProvider(this, ServiceLocator.VMFactory)[VM::class.java]
|
||||
}
|
||||
|
||||
private fun createSuLogDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||
.fallbackToDestructiveMigration()
|
@@ -1,79 +1,73 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.synchronized
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.ktx.forEach
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Notifications.mgr
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.ResponseBody
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class DownloadService : BaseService() {
|
||||
class DownloadService : NotificationService() {
|
||||
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
|
||||
private val job = Job()
|
||||
|
||||
val service get() = ServiceLocator.networkService
|
||||
|
||||
// -- Service overrides
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { doDownload(it) }
|
||||
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { download(it) }
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { mgr.cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
// -- Download logic
|
||||
|
||||
private fun doDownload(subject: Subject) {
|
||||
private fun download(subject: Subject) {
|
||||
update(subject.notifyId)
|
||||
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val stream = service.fetchFile(subject.url).toProgressStream(subject)
|
||||
when (subject) {
|
||||
is Subject.Manager -> handleAPK(subject, stream)
|
||||
is Subject.Module -> stream.toModule(subject.file, assets.open("module_installer.sh"))
|
||||
is Subject.App -> handleApp(stream, subject)
|
||||
is Subject.Module -> handleModule(stream, subject.file)
|
||||
}
|
||||
val activity = ActivityTracker.foreground
|
||||
if (activity != null && subject.autoStart) {
|
||||
if (activity != null && subject.autoLaunch) {
|
||||
remove(subject.notifyId)
|
||||
subject.pendingIntent(activity).send()
|
||||
subject.pendingIntent(activity)?.send()
|
||||
} else {
|
||||
notifyFinish(subject)
|
||||
}
|
||||
subject.postDownload?.invoke()
|
||||
if (!hasNotifications)
|
||||
stopSelf()
|
||||
} catch (e: Exception) {
|
||||
@@ -83,88 +77,113 @@ class DownloadService : BaseService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyId
|
||||
private suspend fun handleApp(stream: InputStream, subject: Subject.App) {
|
||||
fun writeTee(output: OutputStream) {
|
||||
val uri = MediaStoreUtils.getFile("${subject.title}.apk").uri
|
||||
val external = uri.outputStream()
|
||||
stream.copyAndClose(TeeOutputStream(external, output))
|
||||
}
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
if (isRunningAsStub) {
|
||||
val updateApk = StubApk.update(this)
|
||||
try {
|
||||
// Download full APK to stub update path
|
||||
writeTee(updateApk.outputStream())
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
update(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
.setProgress(max.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||
// Also upgrade stub
|
||||
update(subject.notifyId) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
|
||||
// Download
|
||||
val apk = subject.file.toFile()
|
||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||
|
||||
// Patch and install
|
||||
val session = APKInstall.startSession(this)
|
||||
session.openStream(this).use {
|
||||
val label = applicationInfo.nonLocalizedLabel
|
||||
if (!HideAPK.patch(this, apk, it, packageName, label)) {
|
||||
throw IOException("HideAPK patch error")
|
||||
}
|
||||
}
|
||||
apk.delete()
|
||||
subject.intent = session.waitIntent()
|
||||
} else {
|
||||
broadcast(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
ActivityTracker.foreground?.let {
|
||||
// Relaunch the process if we are foreground
|
||||
StubApk.restartProcess(it)
|
||||
} ?: run {
|
||||
// Or else kill the current process after posting notification
|
||||
subject.intent = Notifications.selfLaunchIntent(this)
|
||||
subject.postDownload = { Runtime.getRuntime().exit(0) }
|
||||
}
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// If any error occurred, do not let stub load the new APK
|
||||
updateApk.delete()
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
val session = APKInstall.startSession(this)
|
||||
writeTee(session.openStream(this))
|
||||
subject.intent = session.waitIntent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleModule(src: InputStream, file: Uri) {
|
||||
val input = ZipInputStream(src.buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
assets.open("module_installer.sh").copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray())
|
||||
|
||||
zin.forEach { entry ->
|
||||
val path = entry.name
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Notification management
|
||||
|
||||
private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(-2f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(1f, subject)
|
||||
it.setContentIntent(subject.pendingIntent(this))
|
||||
.setContentTitle(subject.title)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = remove(id)?.also(editor) ?: return -1
|
||||
val newId = Notifications.nextId()
|
||||
mgr.notify(newId, notification.build())
|
||||
return newId
|
||||
}
|
||||
|
||||
private fun create() = Notifications.progress(this, "")
|
||||
|
||||
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
val wasEmpty = !hasNotifications
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
else
|
||||
mgr.notify(id, notification.build())
|
||||
}
|
||||
|
||||
private fun remove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)?.also { updateForeground() }
|
||||
mgr.cancel(id)
|
||||
return n
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications) {
|
||||
val (id, notification) = notifications.entries.first()
|
||||
startForeground(id, notification.build())
|
||||
} else {
|
||||
stopForeground(false)
|
||||
private class TeeOutputStream(
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SUBJECT_KEY = "download_subject"
|
||||
private const val SUBJECT_KEY = "subject"
|
||||
private const val REQUEST_CODE = 1
|
||||
|
||||
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||
|
||||
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
|
||||
progressBroadcast.value = null
|
||||
progressBroadcast.observe(owner) {
|
||||
@@ -173,10 +192,6 @@ class DownloadService : BaseService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun broadcast(progress: Float, subject: Subject) {
|
||||
progressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
|
||||
private fun intent(context: Context, subject: Subject) =
|
||||
context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
|
||||
|
||||
@@ -200,5 +215,4 @@ class DownloadService : BaseService() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,77 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.AlarmManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.ktx.copyAndClose
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
private class TeeOutputStream(
|
||||
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 DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
|
||||
fun write(output: OutputStream) {
|
||||
val external = subject.externalFile.outputStream()
|
||||
stream.copyAndClose(TeeOutputStream(external, output))
|
||||
}
|
||||
|
||||
if (isRunningAsStub) {
|
||||
val apk = subject.file.toFile()
|
||||
val id = subject.notifyId
|
||||
write(DynAPK.update(this).outputStream())
|
||||
if (Info.stub!!.version < subject.stub.versionCode) {
|
||||
// Also upgrade stub
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_app_title))
|
||||
.setContentText("")
|
||||
}
|
||||
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
} else {
|
||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||
intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
//noinspection InlinedApi
|
||||
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val pending = PendingIntent.getActivity(this, id, intent, flag)
|
||||
if (ActivityTracker.hasForeground) {
|
||||
val alarm = getSystemService<AlarmManager>()
|
||||
alarm!!.set(AlarmManager.RTC, System.currentTimeMillis() + 1000, pending)
|
||||
}
|
||||
stopSelf()
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
} else {
|
||||
write(subject.file.outputStream())
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.net.Uri
|
||||
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.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun InputStream.toModule(file: Uri, installer: InputStream) {
|
||||
|
||||
val input = ZipInputStream(buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
installer.copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||
|
||||
zin.forEach { entry ->
|
||||
val path = entry.name
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseService
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.ktx.synchronized
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.InputStream
|
||||
|
||||
open class NotificationService : BaseService() {
|
||||
|
||||
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
|
||||
protected val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
protected val service get() = ServiceLocator.networkService
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
protected fun ResponseBody.toProgressStream(subject: Subject): InputStream {
|
||||
val max = contentLength()
|
||||
val total = max.toFloat() / 1048576
|
||||
val id = subject.notifyId
|
||||
|
||||
update(id) { it.setContentTitle(subject.title) }
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it.toFloat() / 1048576
|
||||
update(id) { notification ->
|
||||
if (max > 0) {
|
||||
broadcast(progress / total, subject)
|
||||
notification
|
||||
.setProgress(max.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, total))
|
||||
} else {
|
||||
broadcast(-1f, subject)
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
|
||||
val notification = remove(id)?.also(editor) ?: return -1
|
||||
val newId = Notifications.nextId()
|
||||
Notifications.mgr.notify(newId, notification.build())
|
||||
return newId
|
||||
}
|
||||
|
||||
protected fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(-2f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
|
||||
protected fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
|
||||
broadcast(1f, subject)
|
||||
it.setContentTitle(subject.title)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
subject.pendingIntent(this)?.let { intent -> it.setContentIntent(intent) }
|
||||
}
|
||||
|
||||
private fun create() = Notifications.progress(this, "")
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications) {
|
||||
val (id, notification) = notifications.entries.first()
|
||||
startForeground(id, notification.build())
|
||||
} else {
|
||||
stopForeground(false)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
|
||||
val wasEmpty = !hasNotifications
|
||||
val notification = notifications.getOrPut(id, ::create).also(editor)
|
||||
if (wasEmpty)
|
||||
updateForeground()
|
||||
else
|
||||
Notifications.mgr.notify(id, notification.build())
|
||||
}
|
||||
|
||||
protected fun remove(id: Int): Notification.Builder? {
|
||||
val n = notifications.remove(id)?.also { updateForeground() }
|
||||
Notifications.mgr.cancel(id)
|
||||
return n
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
protected val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
|
||||
|
||||
private fun broadcast(progress: Float, subject: Subject) {
|
||||
progressBroadcast.postValue(progress to subject)
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,17 +6,15 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.StubJson
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -34,9 +32,10 @@ sealed class Subject : Parcelable {
|
||||
abstract val file: Uri
|
||||
abstract val title: String
|
||||
abstract val notifyId: Int
|
||||
open val autoStart: Boolean get() = true
|
||||
open val autoLaunch: Boolean get() = true
|
||||
open val postDownload: (() -> Unit)? get() = null
|
||||
|
||||
abstract fun pendingIntent(context: Context): PendingIntent
|
||||
abstract fun pendingIntent(context: Context): PendingIntent?
|
||||
|
||||
@Parcelize
|
||||
class Module(
|
||||
@@ -46,7 +45,7 @@ sealed class Subject : Parcelable {
|
||||
) : Subject() {
|
||||
override val url: String get() = module.zipUrl
|
||||
override val title: String get() = module.downloadFilename
|
||||
override val autoStart: Boolean get() = action == Action.Flash
|
||||
override val autoLaunch: Boolean get() = action == Action.Flash
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
@@ -58,7 +57,7 @@ sealed class Subject : Parcelable {
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class Manager(
|
||||
class App(
|
||||
private val json: MagiskJson = Info.remote.magisk,
|
||||
val stub: StubJson = Info.remote.stub,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
@@ -71,14 +70,12 @@ sealed class Subject : Parcelable {
|
||||
cachedFile("manager.apk")
|
||||
}
|
||||
|
||||
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
|
||||
@IgnoredOnParcel
|
||||
override var postDownload: (() -> Unit)? = null
|
||||
|
||||
override fun pendingIntent(context: Context): PendingIntent {
|
||||
val receiver = APKInstall.register(context, null, null)
|
||||
APKInstall.installapk(context, file.toFile())
|
||||
val intent = receiver.waitIntent() ?: Intent()
|
||||
return intent.toPending(context)
|
||||
}
|
||||
@IgnoredOnParcel
|
||||
var intent: Intent? = null
|
||||
override fun pendingIntent(context: Context) = intent?.toPending(context)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
|
@@ -1,28 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class TableStrict
|
||||
|
||||
@TableStrict
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : Query.Builder> buildQuery(builder: Builder.() -> Unit = {}) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { Query(it) }
|
||||
|
||||
}
|
@@ -1,63 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class PolicyDao : BaseDao() {
|
||||
|
||||
override val table: String = Table.POLICY
|
||||
|
||||
suspend fun deleteOutdated() = buildQuery<Delete> {
|
||||
condition {
|
||||
greaterThan("until", "0")
|
||||
and {
|
||||
lessThan("until", TimeUnit.MILLISECONDS.toSeconds(now).toString())
|
||||
}
|
||||
or {
|
||||
lessThan("until", "0")
|
||||
}
|
||||
}
|
||||
}.commit()
|
||||
|
||||
suspend fun delete(uid: Int) = buildQuery<Delete> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(uid: Int) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.query().first().toPolicyOrNull()
|
||||
|
||||
suspend fun update(policy: SuPolicy) = buildQuery<Replace> {
|
||||
values(policy.toMap())
|
||||
}.commit()
|
||||
|
||||
suspend fun <R: Any> fetchAll(mapper: (SuPolicy) -> R) = buildQuery<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.query {
|
||||
it.toPolicyOrNull()?.let(mapper)
|
||||
}
|
||||
|
||||
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
|
||||
return runCatching { toPolicy(AppContext.packageManager) }.getOrElse {
|
||||
Timber.w(it)
|
||||
val uid = getOrElse("uid") { return null }
|
||||
GlobalScope.launch { delete(uid.toInt()) }
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,161 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class Query(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
|
||||
interface Builder {
|
||||
val requestType: String
|
||||
var table: String
|
||||
}
|
||||
|
||||
suspend inline fun <R : Any> query(crossinline mapper: (Map<String, String>) -> R?): List<R> =
|
||||
withContext(Dispatchers.Default) {
|
||||
Shell.su(query).await().out.map { line ->
|
||||
async {
|
||||
line.split("\\|".toRegex())
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { it[0] to it[1] }
|
||||
.toMap()
|
||||
.let(mapper)
|
||||
}
|
||||
}.awaitAll().filterNotNull()
|
||||
}
|
||||
|
||||
suspend inline fun query() = query { it }
|
||||
|
||||
suspend inline fun commit() = Shell.su(query).to(null).await()
|
||||
}
|
||||
|
||||
class Delete : Query.Builder {
|
||||
override val requestType: String = "DELETE FROM"
|
||||
override var table = ""
|
||||
|
||||
private var condition = ""
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Select : Query.Builder {
|
||||
override val requestType: String get() = "SELECT $fields FROM"
|
||||
override lateinit var table: String
|
||||
|
||||
private var fields = "*"
|
||||
private var condition = ""
|
||||
private var orderField = ""
|
||||
|
||||
fun fields(vararg newFields: String) {
|
||||
if (newFields.isEmpty()) {
|
||||
fields = "*"
|
||||
return
|
||||
}
|
||||
fields = newFields.joinToString(", ")
|
||||
}
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
fun orderBy(field: String, @OrderStrict order: String) {
|
||||
orderField = "ORDER BY $field $order"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Replace : Insert() {
|
||||
override val requestType: String = "REPLACE INTO"
|
||||
}
|
||||
|
||||
open class Insert : Query.Builder {
|
||||
override val requestType: String = "INSERT INTO"
|
||||
override lateinit var table: String
|
||||
|
||||
private val keys get() = _values.keys.joinToString(",")
|
||||
private val values get() = _values.values.joinToString(",") {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
private var _values: Map<String, Any> = mapOf()
|
||||
|
||||
fun values(vararg pairs: Pair<String, Any>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, Any>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Condition {
|
||||
|
||||
private val conditionWord = "WHERE %s"
|
||||
private var condition: String = ""
|
||||
|
||||
fun equals(field: String, value: Any) {
|
||||
condition = when (value) {
|
||||
is String -> "$field=\"$value\""
|
||||
else -> "$field=$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun greaterThan(field: String, value: String) {
|
||||
condition = "$field > $value"
|
||||
}
|
||||
|
||||
fun lessThan(field: String, value: String) {
|
||||
condition = "$field < $value"
|
||||
}
|
||||
|
||||
fun greaterOrEqualTo(field: String, value: String) {
|
||||
condition = "$field >= $value"
|
||||
}
|
||||
|
||||
fun lessOrEqualTo(field: String, value: String) {
|
||||
condition = "$field <= $value"
|
||||
}
|
||||
|
||||
fun and(builder: Condition.() -> Unit) {
|
||||
condition = "($condition AND ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
fun or(builder: Condition.() -> Unit) {
|
||||
condition = "($condition OR ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return conditionWord.format(condition)
|
||||
}
|
||||
}
|
||||
|
||||
object Order {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
@StringDef(Order.ASC, Order.DESC)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class OrderStrict
|
@@ -1,22 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = Table.SETTINGS
|
||||
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.commit()
|
||||
|
||||
suspend fun put(key: String, value: Int) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(key: String, default: Int = -1) = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.query {
|
||||
it["value"]?.toIntOrNull()
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.magiskdb
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = Table.STRINGS
|
||||
|
||||
suspend fun delete(key: String) = buildQuery<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.commit()
|
||||
|
||||
suspend fun put(key: String, value: String) = buildQuery<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.commit()
|
||||
|
||||
suspend fun fetch(key: String, default: String = "") = buildQuery<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.query {
|
||||
it["value"]
|
||||
}.firstOrNull() ?: default
|
||||
|
||||
}
|
@@ -2,9 +2,9 @@ package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
@@ -26,13 +26,12 @@ data class LocalModule(
|
||||
var outdated = false
|
||||
|
||||
private var updateUrl: String = ""
|
||||
private val removeFile = SuFile(path, "remove")
|
||||
private val disableFile = SuFile(path, "disable")
|
||||
private val updateFile = SuFile(path, "update")
|
||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
||||
private val riruFolder = SuFile(path, "riru")
|
||||
private val zygiskFolder = SuFile(path, "zygisk")
|
||||
private val unloaded = SuFile(zygiskFolder, "unloaded")
|
||||
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||
private val updateFile = RootUtils.fs.getFile(path, "update")
|
||||
private val riruFolder = RootUtils.fs.getFile(path, "riru")
|
||||
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
|
||||
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
|
||||
|
||||
val updated: Boolean get() = updateFile.exists()
|
||||
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||
@@ -42,19 +41,12 @@ data class LocalModule(
|
||||
var enable: Boolean
|
||||
get() = !disableFile.exists()
|
||||
set(enable) {
|
||||
val dir = "$PERSIST/$id"
|
||||
if (enable) {
|
||||
disableFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("rm -rf $dir").submit()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,16 +56,10 @@ data class LocalModule(
|
||||
if (remove) {
|
||||
if (updateFile.exists()) return
|
||||
removeFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
} else {
|
||||
removeFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
Shell.cmd("copy_sepolicy_rules").submit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +89,7 @@ data class LocalModule(
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
parseProps(Shell.cmd("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
@@ -138,8 +124,10 @@ data class LocalModule(
|
||||
|
||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
||||
|
||||
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||
|
||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||
SuFile(Const.MAGISK_PATH)
|
||||
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||
.listFiles()
|
||||
.orEmpty()
|
||||
.filter { !it.isFile }
|
||||
|
@@ -1,11 +1,13 @@
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
|
||||
@Entity(tableName = "logs")
|
||||
data class SuLog(
|
||||
class SuLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
val fromPid: Int,
|
||||
@@ -13,7 +15,44 @@ data class SuLog(
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val time: Long = now
|
||||
val time: Long = System.currentTimeMillis()
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
}
|
||||
|
||||
fun PackageManager.createSuLog(
|
||||
info: PackageInfo,
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
policy: Int
|
||||
): SuLog {
|
||||
val appInfo = info.applicationInfo
|
||||
return SuLog(
|
||||
fromUid = appInfo.uid,
|
||||
toUid = toUid,
|
||||
fromPid = fromPid,
|
||||
packageName = getNameForUid(appInfo.uid)!!,
|
||||
appName = appInfo.getLabel(this),
|
||||
command = command,
|
||||
action = policy == SuPolicy.ALLOW
|
||||
)
|
||||
}
|
||||
|
||||
fun createSuLog(
|
||||
fromUid: Int,
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
policy: Int
|
||||
): SuLog {
|
||||
return SuLog(
|
||||
fromUid = fromUid,
|
||||
toUid = toUid,
|
||||
fromPid = fromPid,
|
||||
packageName = "[UID] $fromUid",
|
||||
appName = "[UID] $fromUid",
|
||||
command = command,
|
||||
action = policy == SuPolicy.ALLOW
|
||||
)
|
||||
}
|
||||
|
@@ -1,85 +1,22 @@
|
||||
@file:SuppressLint("InlinedApi")
|
||||
|
||||
package com.topjohnwu.magisk.core.model.su
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
|
||||
data class SuPolicy(
|
||||
val uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val icon: Drawable,
|
||||
var policy: Int = INTERACTIVE,
|
||||
var until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true
|
||||
) {
|
||||
|
||||
class SuPolicy(val uid: Int) {
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
}
|
||||
|
||||
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
|
||||
uid, toUid, fromPid, packageName, appName,
|
||||
command, policy == ALLOW)
|
||||
var policy: Int = INTERACTIVE
|
||||
var until: Long = -1L
|
||||
var logging: Boolean = true
|
||||
var notification: Boolean = true
|
||||
|
||||
fun toMap() = mapOf(
|
||||
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
|
||||
return SuPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
||||
until = get("until")?.toLongOrNull() ?: -1L,
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES)
|
||||
return SuPolicy(
|
||||
uid = info.uid,
|
||||
packageName = pkg,
|
||||
appName = info.getLabel(pm),
|
||||
icon = info.loadIcon(pm),
|
||||
policy = policy
|
||||
)
|
||||
}
|
||||
|
||||
fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy {
|
||||
return SuPolicy(
|
||||
uid = this,
|
||||
packageName = "[UID] $this",
|
||||
appName = "[UID] $this",
|
||||
icon = pm.defaultActivityIcon,
|
||||
policy = policy
|
||||
)
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
@@ -11,26 +11,27 @@ import kotlin.reflect.KProperty
|
||||
interface DBConfig {
|
||||
val settingsDB: SettingsDao
|
||||
val stringDB: StringDao
|
||||
val coroutineScope: CoroutineScope
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Int
|
||||
) = DBSettingsValue(name, default)
|
||||
) = IntDBProperty(name, default)
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) = DBBoolSettings(name, default)
|
||||
) = BoolDBProperty(name, default)
|
||||
|
||||
fun dbStrings(
|
||||
name: String,
|
||||
default: String,
|
||||
sync: Boolean = false
|
||||
) = DBStringsValue(name, default, sync)
|
||||
) = StringDBProperty(name, default, sync)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
class IntDBProperty(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
@@ -48,18 +49,18 @@ class DBSettingsValue(
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.settingsDB.put(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class DBBoolSettings(
|
||||
open class BoolDBProperty(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||
|
||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
||||
val base = IntDBProperty(name, if (default) 1 else 0)
|
||||
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||
base.getValue(thisRef, property) != 0
|
||||
@@ -68,10 +69,10 @@ open class DBBoolSettings(
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBBoolSettingsNoWrite(
|
||||
class BoolDBPropertyNoWrite(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : DBBoolSettings(name, default) {
|
||||
) : BoolDBProperty(name, default) {
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||
synchronized(base) {
|
||||
base.value = if (value) 1 else 0
|
||||
@@ -79,7 +80,7 @@ class DBBoolSettingsNoWrite(
|
||||
}
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
class StringDBProperty(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
@@ -106,7 +107,7 @@ class DBStringsValue(
|
||||
thisRef.stringDB.delete(name)
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.stringDB.delete(name)
|
||||
}
|
||||
}
|
||||
@@ -116,7 +117,7 @@ class DBStringsValue(
|
||||
thisRef.stringDB.put(name, value)
|
||||
}
|
||||
} else {
|
||||
GlobalScope.launch {
|
||||
thisRef.coroutineScope.launch {
|
||||
thisRef.stringDB.put(name, value)
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.data.SuLogDao
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
@@ -29,9 +29,9 @@ class LogRepository(
|
||||
}
|
||||
}
|
||||
if (Info.env.isActive) {
|
||||
Shell.su("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
|
||||
Shell.cmd("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
|
||||
} else {
|
||||
Shell.sh("logcat -d").to(list).await()
|
||||
Shell.cmd("logcat -d").to(list).await()
|
||||
}
|
||||
return list.buf.toString()
|
||||
}
|
||||
@@ -39,7 +39,7 @@ class LogRepository(
|
||||
suspend fun clearLogs() = logDao.deleteAll()
|
||||
|
||||
fun clearMagiskLogs(cb: (Shell.Result) -> Unit) =
|
||||
Shell.su("echo -n > ${Const.MAGISK_LOG}").submit(cb)
|
||||
Shell.cmd("echo -n > ${Const.MAGISK_LOG}").submit(cb)
|
||||
|
||||
suspend fun insert(log: SuLog) = logDao.insert(log)
|
||||
|
@@ -1,15 +1,16 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
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.CUSTOM_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.DEBUG_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
||||
import com.topjohnwu.magisk.data.network.RawServices
|
||||
import com.topjohnwu.magisk.core.data.GithubApiServices
|
||||
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||
import com.topjohnwu.magisk.core.data.RawServices
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
@@ -24,12 +25,12 @@ class NetworkService(
|
||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||
BETA_CHANNEL -> fetchBetaUpdate()
|
||||
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||
Config.updateChannel == DEFAULT_CHANNEL
|
||||
) {
|
||||
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||
Config.updateChannel = BETA_CHANNEL
|
||||
info = fetchBetaUpdate()
|
||||
}
|
||||
@@ -40,11 +41,15 @@ class NetworkService(
|
||||
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.json")
|
||||
private suspend fun fetchDebugUpdate() = pages.fetchUpdateJSON("debug.json")
|
||||
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
||||
|
||||
private inline fun <T> safe(factory: () -> T): T? {
|
||||
return try {
|
||||
factory()
|
||||
if (Info.isConnected.value == true)
|
||||
factory()
|
||||
else
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
null
|
@@ -1,11 +1,73 @@
|
||||
package com.topjohnwu.magisk.data.preference
|
||||
package com.topjohnwu.magisk.core.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
abstract class Property {
|
||||
interface PreferenceConfig {
|
||||
|
||||
val context: Context
|
||||
|
||||
val fileName: String
|
||||
get() = "${context.packageName}_preferences"
|
||||
|
||||
val prefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
fun preferenceStrInt(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = object: ReadWriteProperty<PreferenceConfig, Int> {
|
||||
val base = StringProperty(name, default.toString(), commit)
|
||||
override fun getValue(thisRef: PreferenceConfig, property: KProperty<*>): Int =
|
||||
base.getValue(thisRef, property).toInt()
|
||||
|
||||
override fun setValue(thisRef: PreferenceConfig, property: KProperty<*>, value: Int) =
|
||||
base.setValue(thisRef, property, value.toString())
|
||||
}
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Boolean,
|
||||
commit: Boolean = false
|
||||
) = BooleanProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = false
|
||||
) = FloatProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = IntProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = false
|
||||
) = LongProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = false
|
||||
) = StringProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = false
|
||||
) = StringSetProperty(name, default, commit)
|
||||
|
||||
}
|
||||
|
||||
abstract class PreferenceProperty {
|
||||
|
||||
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
|
||||
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
|
||||
@@ -27,10 +89,10 @@ class BooleanProperty(
|
||||
private val name: String,
|
||||
private val default: Boolean,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Boolean {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -38,7 +100,7 @@ class BooleanProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Boolean
|
||||
) {
|
||||
@@ -51,10 +113,10 @@ class FloatProperty(
|
||||
private val name: String,
|
||||
private val default: Float,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Float {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -62,7 +124,7 @@ class FloatProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Float
|
||||
) {
|
||||
@@ -75,10 +137,10 @@ class IntProperty(
|
||||
private val name: String,
|
||||
private val default: Int,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Int {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -86,7 +148,7 @@ class IntProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Int
|
||||
) {
|
||||
@@ -99,10 +161,10 @@ class LongProperty(
|
||||
private val name: String,
|
||||
private val default: Long,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Long {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -110,7 +172,7 @@ class LongProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Long
|
||||
) {
|
||||
@@ -123,10 +185,10 @@ class StringProperty(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, String> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): String {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -134,7 +196,7 @@ class StringProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: String
|
||||
) {
|
||||
@@ -147,10 +209,10 @@ class StringSetProperty(
|
||||
private val name: String,
|
||||
private val default: Set<String>,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
|
||||
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>
|
||||
): Set<String> {
|
||||
val prefName = name.ifBlank { property.name }
|
||||
@@ -158,7 +220,7 @@ class StringSetProperty(
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
thisRef: PreferenceConfig,
|
||||
property: KProperty<*>,
|
||||
value: Set<String>
|
||||
) {
|
@@ -6,13 +6,13 @@ import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toUidPolicy
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.ktx.getPackageInfo
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
|
||||
object SuCallbackHandler {
|
||||
@@ -53,59 +53,47 @@ object SuCallbackHandler {
|
||||
private fun handleLogging(context: Context, data: Bundle) {
|
||||
val fromUid = data.getIntComp("from.uid", -1)
|
||||
val notify = data.getBoolean("notify", true)
|
||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
val toUid = data.getIntComp("to.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
val command = data.getString("command", "")
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val policy = runCatching {
|
||||
fromUid.toPolicy(pm, allow)
|
||||
}.getOrElse {
|
||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
||||
fromUid.toUidPolicy(pm, allow)
|
||||
}
|
||||
val log = runCatching {
|
||||
pm.getPackageInfo(fromUid, pid)?.let {
|
||||
pm.createSuLog(it, toUid, pid, command, policy)
|
||||
}
|
||||
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
|
||||
|
||||
if (notify)
|
||||
notify(context, policy)
|
||||
notify(context, log.action, log.appName)
|
||||
|
||||
val toUid = data.getIntComp("to.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
|
||||
val command = data.getString("command", "")
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command
|
||||
)
|
||||
|
||||
GlobalScope.launch {
|
||||
ServiceLocator.logRepo.insert(log)
|
||||
}
|
||||
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||
}
|
||||
|
||||
private fun handleNotify(context: Context, data: Bundle) {
|
||||
val fromUid = data.getIntComp("from.uid", -1)
|
||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
val uid = data.getIntComp("from.uid", -1)
|
||||
val pid = data.getIntComp("pid", -1)
|
||||
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||
|
||||
val pm = context.packageManager
|
||||
|
||||
val appName = runCatching {
|
||||
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||
}.getOrNull() ?: "[UID] $uid"
|
||||
|
||||
val policy = runCatching {
|
||||
fromUid.toPolicy(pm, allow)
|
||||
}.getOrElse {
|
||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
||||
fromUid.toUidPolicy(pm, allow)
|
||||
}
|
||||
notify(context, policy)
|
||||
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||
}
|
||||
|
||||
private fun notify(context: Context, policy: SuPolicy) {
|
||||
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
||||
val resId = if (granted)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast
|
||||
|
||||
Utils.toast(context.getString(resId, policy.appName), Toast.LENGTH_SHORT)
|
||||
Utils.toast(context.getString(resId, appName), Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,32 +1,30 @@
|
||||
package com.topjohnwu.magisk.core.su
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.magisk.ktx.getPackageInfo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.Closeable
|
||||
import java.io.DataOutputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SuRequestHandler(
|
||||
private val pm: PackageManager,
|
||||
val pm: PackageManager,
|
||||
private val policyDB: PolicyDao
|
||||
) : Closeable {
|
||||
) {
|
||||
|
||||
private lateinit var output: DataOutputStream
|
||||
lateinit var policy: SuPolicy
|
||||
private lateinit var policy: SuPolicy
|
||||
lateinit var pkgInfo: PackageInfo
|
||||
private set
|
||||
|
||||
// Return true to indicate undetermined policy, require user interaction
|
||||
@@ -35,8 +33,8 @@ class SuRequestHandler(
|
||||
return false
|
||||
|
||||
// Never allow com.topjohnwu.magisk (could be malware)
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID) {
|
||||
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
||||
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -54,50 +52,57 @@ class SuRequestHandler(
|
||||
return true
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
private fun close() {
|
||||
if (::output.isInitialized)
|
||||
output.close()
|
||||
runCatching { output.close() }
|
||||
}
|
||||
|
||||
private class SuRequestError : IOException()
|
||||
|
||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
||||
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
||||
policy = uid.toPolicy(pm)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is IOException, is PackageManager.NameNotFoundException -> {
|
||||
Timber.e(e)
|
||||
runCatching { close() }
|
||||
false
|
||||
}
|
||||
else -> throw e // Unexpected error
|
||||
val fifo = intent.getStringExtra("fifo") ?: throw IOException("fifo == null")
|
||||
output = DataOutputStream(FileOutputStream(fifo))
|
||||
val uid = intent.getIntExtra("uid", -1)
|
||||
if (uid <= 0) {
|
||||
throw IOException("uid == $uid")
|
||||
}
|
||||
policy = SuPolicy(uid)
|
||||
val pid = intent.getIntExtra("pid", -1)
|
||||
try {
|
||||
pkgInfo = pm.getPackageInfo(uid, pid) ?: PackageInfo().apply {
|
||||
val name = pm.getNameForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
// We only fill in sharedUserId and leave other fields uninitialized
|
||||
sharedUserId = name.split(":")[0]
|
||||
}
|
||||
return@withContext true
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
respond(SuPolicy.DENY, -1)
|
||||
return@withContext false
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
close()
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
fun respond(action: Int, time: Int) {
|
||||
suspend fun respond(action: Int, time: Int) {
|
||||
val until = if (time > 0)
|
||||
TimeUnit.MILLISECONDS.toSeconds(now) + TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) +
|
||||
TimeUnit.MINUTES.toSeconds(time.toLong())
|
||||
else
|
||||
time.toLong()
|
||||
|
||||
policy.policy = action
|
||||
policy.until = until
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
output.writeInt(policy.policy)
|
||||
output.flush()
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
runCatching { close() }
|
||||
close()
|
||||
if (until >= 0)
|
||||
policyDB.update(policy)
|
||||
}
|
||||
|
@@ -3,10 +3,10 @@ package com.topjohnwu.magisk.core.tasks
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.unzip
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -63,7 +63,7 @@ open class FlashZip(
|
||||
|
||||
console.add("- Installing ${mUri.displayName}")
|
||||
|
||||
return Shell.su("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||
return Shell.cmd("sh $installDir/update-binary dummy 1 \'$zipFile\'")
|
||||
.to(console, logs).exec().isSuccess
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ open class FlashZip(
|
||||
Timber.e(e)
|
||||
false
|
||||
} finally {
|
||||
Shell.su("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
||||
Shell.cmd("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,15 +5,16 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
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.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.AXML
|
||||
import com.topjohnwu.magisk.core.utils.Keygen
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ktx.await
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.signing.JarMap
|
||||
import com.topjohnwu.magisk.signing.SignApk
|
||||
@@ -21,18 +22,19 @@ import com.topjohnwu.magisk.utils.APKInstall
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Runnable
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.security.SecureRandom
|
||||
|
||||
object HideAPK {
|
||||
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||
private const val ALPHADOTS = "$ALPHA....."
|
||||
private const val APP_NAME = "Magisk"
|
||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
// Some arbitrary limit
|
||||
@@ -65,27 +67,29 @@ object HideAPK {
|
||||
|
||||
fun patch(
|
||||
context: Context,
|
||||
apk: File, out: File,
|
||||
apk: File, out: OutputStream,
|
||||
pkg: String, label: CharSequence
|
||||
): Boolean {
|
||||
val info = context.packageManager.getPackageArchiveInfo(apk.path, 0) ?: return false
|
||||
val name = info.applicationInfo.nonLocalizedLabel.toString()
|
||||
try {
|
||||
val jar = JarMap.open(apk, true)
|
||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||
val xml = AXML(jar.getRawData(je))
|
||||
JarMap.open(apk, true).use { jar ->
|
||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||
val xml = AXML(jar.getRawData(je))
|
||||
|
||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
|
||||
return false
|
||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, name to label.toString()))
|
||||
return false
|
||||
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).write(xml.bytes)
|
||||
val keys = Keygen(context)
|
||||
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
|
||||
// Write apk changes
|
||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||
val keys = Keygen()
|
||||
SignApk.sign(keys.cert, keys.key, jar, out)
|
||||
return true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun launchApp(activity: Activity, pkg: String) {
|
||||
@@ -93,14 +97,14 @@ object HideAPK {
|
||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
||||
val self = activity.packageName
|
||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
|
||||
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
|
||||
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||
intent.putExtra(Const.Key.PREV_PKG, self)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||
val stub = File(activity.cacheDir, "stub.apk")
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
|
||||
@@ -108,7 +112,8 @@ object HideAPK {
|
||||
Timber.e(e)
|
||||
stub.createNewFile()
|
||||
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
|
||||
if (!Shell.su(cmd).exec().isSuccess) return false
|
||||
if (!Shell.cmd(cmd).exec().isSuccess)
|
||||
return false
|
||||
}
|
||||
|
||||
// Generate a new random package name and signature
|
||||
@@ -116,18 +121,24 @@ object HideAPK {
|
||||
val pkg = genPackageName()
|
||||
Config.keyStoreRaw = ""
|
||||
|
||||
if (!patch(activity, stub, repack, pkg, label))
|
||||
if (!patch(activity, stub, FileOutputStream(repack), pkg, label))
|
||||
return false
|
||||
|
||||
// Install and auto launch app
|
||||
val receiver = APKInstall.register(activity, pkg) {
|
||||
val session = APKInstall.startSession(activity, pkg, onFailure) {
|
||||
launchApp(activity, pkg)
|
||||
}
|
||||
|
||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
||||
if (!Shell.su(cmd).exec().isSuccess) {
|
||||
APKInstall.installapk(activity, repack)
|
||||
receiver.waitIntent()?.let { activity.startActivity(it) }
|
||||
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||
|
||||
try {
|
||||
session.install(activity, repack)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
session.waitIntent()?.let { activity.startActivity(it) } ?: return false
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -139,33 +150,45 @@ object HideAPK {
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label)
|
||||
}
|
||||
if (!result) {
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
patchAndHide(activity, label, onFailure)
|
||||
}
|
||||
if (!success) onFailure.run()
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun restore(activity: Activity) {
|
||||
suspend fun restore(activity: Activity) {
|
||||
val dialog = android.app.ProgressDialog(activity).apply {
|
||||
setTitle(activity.getString(R.string.restore_img_msg))
|
||||
isIndeterminate = true
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
val apk = DynAPK.current(activity)
|
||||
val receiver = APKInstall.register(activity, APPLICATION_ID) {
|
||||
val onFailure = Runnable {
|
||||
dialog.dismiss()
|
||||
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
val apk = StubApk.current(activity)
|
||||
val session = APKInstall.startSession(activity, APPLICATION_ID, onFailure) {
|
||||
launchApp(activity, APPLICATION_ID)
|
||||
dialog.dismiss()
|
||||
}
|
||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
||||
Shell.su(cmd).submit(Shell.EXECUTOR) { ret ->
|
||||
if (ret.isSuccess) return@submit
|
||||
APKInstall.installapk(activity, apk)
|
||||
receiver.waitIntent()?.let { activity.startActivity(it) }
|
||||
if (Shell.cmd(cmd).await().isSuccess) return
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
session.install(activity, apk)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return@withContext false
|
||||
}
|
||||
session.waitIntent()?.let { activity.startActivity(it) } ?: return@withContext false
|
||||
return@withContext true
|
||||
}
|
||||
if (!success) onFailure.run()
|
||||
}
|
||||
}
|
||||
|
@@ -6,13 +6,14 @@ import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.ktx.reboot
|
||||
import com.topjohnwu.magisk.ktx.withStreams
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
@@ -22,9 +23,8 @@ import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.NOPList
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.jpountz.lz4.LZ4FrameInputStream
|
||||
@@ -37,6 +37,7 @@ import java.io.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@@ -45,21 +46,24 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
private val logs: MutableList<String> = NOPList.getInstance()
|
||||
) {
|
||||
|
||||
protected var installDir = File("xxx")
|
||||
private lateinit var srcBoot: File
|
||||
protected lateinit var installDir: ExtendedFile
|
||||
private lateinit var srcBoot: ExtendedFile
|
||||
|
||||
private val shell = Shell.getShell()
|
||||
private val service get() = ServiceLocator.networkService
|
||||
protected val context get() = ServiceLocator.deContext
|
||||
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||
|
||||
private val rootFS get() = RootUtils.fs
|
||||
private val localFS get() = FileSystemManager.getLocal()
|
||||
|
||||
private fun findImage(): Boolean {
|
||||
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
val bootPath = "RECOVERYMODE=${Config.recovery} find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
|
||||
if (bootPath.isEmpty()) {
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
srcBoot = rootFS.getFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
@@ -77,7 +81,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
console.add("! Unable to detect target image")
|
||||
return false
|
||||
}
|
||||
srcBoot = SuFile(bootPath)
|
||||
srcBoot = rootFS.getFile(bootPath)
|
||||
console.add("- Target image: $bootPath")
|
||||
return true
|
||||
}
|
||||
@@ -86,14 +90,14 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
|
||||
installDir = File(context.filesDir.parent, "install")
|
||||
installDir = localFS.getFile(context.filesDir.parent, "install")
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
try {
|
||||
// Extract binaries
|
||||
if (isRunningAsStub) {
|
||||
val zf = ZipFile(DynAPK.current(context))
|
||||
val zf = ZipFile(StubApk.current(context))
|
||||
|
||||
// Also extract magisk32 on non 64-bit only 64-bit devices
|
||||
val is32lib = Const.CPU_ABI_32?.let {
|
||||
@@ -146,7 +150,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
if (useRootDir) {
|
||||
// Move everything to tmpfs to workaround Samsung bullshit
|
||||
SuFile(Const.TMPDIR).also {
|
||||
rootFS.getFile(Const.TMPDIR).also {
|
||||
arrayOf(
|
||||
"rm -rf $it",
|
||||
"mkdir -p $it",
|
||||
@@ -160,14 +164,6 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
return true
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -198,8 +194,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
val name = entry.name.replace(".lz4", "")
|
||||
console.add("-- Extracting: $name")
|
||||
|
||||
val extract = installDirFile(name)
|
||||
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
||||
val extract = installDir.getChildFile(name)
|
||||
decompressedStream().cleanPump(extract.newOutputStream())
|
||||
} else if (entry.name.contains("vbmeta.img")) {
|
||||
val rawData = decompressedStream().readBytes()
|
||||
// Valid vbmeta.img should be at least 256 bytes
|
||||
@@ -219,8 +215,8 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
val boot = installDirFile("boot.img")
|
||||
val recovery = installDirFile("recovery.img")
|
||||
val boot = installDir.getChildFile("boot.img")
|
||||
val recovery = installDir.getChildFile("recovery.img")
|
||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||
// Install to recovery
|
||||
srcBoot = recovery
|
||||
@@ -234,7 +230,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
"./magiskboot cleanup",
|
||||
"rm -f new-boot.img",
|
||||
"cd /").sh()
|
||||
SuFileInputStream.open(boot).use {
|
||||
boot.newInputStream().use {
|
||||
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
||||
it.copyTo(tarOut)
|
||||
}
|
||||
@@ -280,9 +276,9 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
processTar(src, outFile!!.uri.outputStream())
|
||||
} else {
|
||||
// raw image
|
||||
srcBoot = installDirFile("boot.img")
|
||||
srcBoot = installDir.getChildFile("boot.img")
|
||||
console.add("- Copying image to cache")
|
||||
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
||||
src.cleanPump(srcBoot.newOutputStream())
|
||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||
outFile!!.uri.outputStream()
|
||||
}
|
||||
@@ -302,12 +298,12 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
// Output file
|
||||
try {
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
val newBoot = installDir.getChildFile("new-boot.img")
|
||||
if (outStream is TarOutputStream) {
|
||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||
}
|
||||
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
||||
newBoot.newInputStream().cleanPump(outStream)
|
||||
newBoot.delete()
|
||||
|
||||
console.add("")
|
||||
@@ -331,9 +327,9 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
|
||||
private fun patchBoot(): Boolean {
|
||||
var isSigned = false
|
||||
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
|
||||
if (!srcBoot.isCharacter) {
|
||||
try {
|
||||
SuFileInputStream.open(srcBoot).use {
|
||||
srcBoot.newInputStream().use {
|
||||
if (SignBoot.verifySignature(it, null)) {
|
||||
isSigned = true
|
||||
console.add("- Boot image is signed with AVB 1.0")
|
||||
@@ -346,7 +342,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
}
|
||||
}
|
||||
|
||||
val newBoot = installDirFile("new-boot.img")
|
||||
val newBoot = installDir.getChildFile("new-boot.img")
|
||||
if (!useRootDir) {
|
||||
// Create output files before hand
|
||||
newBoot.createNewFile()
|
||||
@@ -370,7 +366,7 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
console.add("- Signing boot image with verity keys")
|
||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||
try {
|
||||
val src = SuFileInputStream.open(newBoot).buffered()
|
||||
val src = newBoot.newInputStream().buffered()
|
||||
val out = signed.outputStream().buffered()
|
||||
withStreams(src, out) { _, _ ->
|
||||
SignBoot.doSignature(null, null, src, out, "/boot")
|
||||
@@ -410,11 +406,11 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
|
||||
protected fun patchFile(file: Uri) = extractFiles() && handleFile(file)
|
||||
|
||||
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||
|
||||
protected suspend fun secondSlot() =
|
||||
protected fun secondSlot() =
|
||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||
@@ -425,20 +421,15 @@ abstract class MagiskInstallImpl protected constructor(
|
||||
protected abstract suspend fun operations(): Boolean
|
||||
|
||||
open suspend fun exec(): Boolean {
|
||||
synchronized(Companion) {
|
||||
if (haveActiveSession)
|
||||
return false
|
||||
haveActiveSession = true
|
||||
}
|
||||
if (haveActiveSession.getAndSet(true))
|
||||
return false
|
||||
val result = withContext(Dispatchers.IO) { operations() }
|
||||
synchronized(Companion) {
|
||||
haveActiveSession = false
|
||||
}
|
||||
haveActiveSession.set(false)
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var haveActiveSession = false
|
||||
private var haveActiveSession = AtomicBoolean(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +443,7 @@ abstract class MagiskInstaller(
|
||||
if (success) {
|
||||
console.add("- All done!")
|
||||
} else {
|
||||
Shell.sh("rm -rf $installDir").submit()
|
||||
Shell.cmd("rm -rf $installDir").submit()
|
||||
console.add("! Installation failed")
|
||||
}
|
||||
return success
|
||||
@@ -463,7 +454,7 @@ abstract class MagiskInstaller(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>
|
||||
) : MagiskInstaller(console, logs) {
|
||||
override suspend fun operations() = doPatchFile(uri)
|
||||
override suspend fun operations() = patchFile(uri)
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
@@ -497,7 +488,7 @@ abstract class MagiskInstaller(
|
||||
val success = super.exec()
|
||||
if (success) {
|
||||
UiThreadHandler.handler.postDelayed(3000) {
|
||||
Shell.su("pm uninstall ${context.packageName}").exec()
|
||||
Shell.cmd("pm uninstall ${context.packageName}").exec()
|
||||
}
|
||||
}
|
||||
return success
|
||||
|
@@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
class AXML(b: ByteArray) {
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
|
||||
object BiometricHelper {
|
||||
|
||||
|
@@ -1,23 +1,17 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Base64
|
||||
import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.magisk.signing.CryptoUtils.readPrivateKey
|
||||
import com.topjohnwu.magisk.signing.KeyData
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
@@ -29,13 +23,11 @@ private interface CertKeyProvider {
|
||||
val key: PrivateKey
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
class Keygen(context: Context) : CertKeyProvider {
|
||||
class Keygen : CertKeyProvider {
|
||||
|
||||
companion object {
|
||||
private const val ALIAS = "magisk"
|
||||
private val PASSWORD get() = "magisk".toCharArray()
|
||||
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||
private const val DNAME = "C=US,ST=California,L=Mountain View,O=Google Inc.,OU=Android,CN=Android"
|
||||
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||
}
|
||||
@@ -43,49 +35,9 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
||||
|
||||
override val cert get() = provider.cert
|
||||
override val key get() = provider.key
|
||||
|
||||
private val provider: CertKeyProvider
|
||||
|
||||
inner class KeyStoreProvider :
|
||||
CertKeyProvider {
|
||||
private val ks by lazy { init() }
|
||||
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
||||
override val key by lazy { ks.getKey(
|
||||
ALIAS,
|
||||
PASSWORD
|
||||
) as PrivateKey }
|
||||
}
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
override val cert by lazy {
|
||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
||||
}
|
||||
override val key by lazy {
|
||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val pm = context.packageManager
|
||||
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||
val sig = info.signatures[0]
|
||||
val digest = MessageDigest.getInstance("SHA1")
|
||||
val chksum = digest.digest(sig.toByteArray())
|
||||
|
||||
val sb = StringBuilder()
|
||||
for (b in chksum) {
|
||||
sb.append("%02x".format(0xFF and b.toInt()))
|
||||
}
|
||||
|
||||
provider = if (sb.toString() == TESTKEY_CERT) {
|
||||
// The app was signed by the test key, continue to use it (legacy mode)
|
||||
TestProvider()
|
||||
} else {
|
||||
KeyStoreProvider()
|
||||
}
|
||||
}
|
||||
private val ks = init()
|
||||
override val cert = ks.getCertificate(ALIAS) as X509Certificate
|
||||
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
|
||||
|
||||
private fun init(): KeyStore {
|
||||
val raw = Config.keyStoreRaw
|
||||
@@ -93,12 +45,8 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
if (raw.isEmpty()) {
|
||||
ks.load(null)
|
||||
} else {
|
||||
GZIPInputStream(Base64.decode(raw,
|
||||
BASE64_FLAG
|
||||
).inputStream()).use {
|
||||
ks.load(it,
|
||||
PASSWORD
|
||||
)
|
||||
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||
ks.load(it, PASSWORD)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,22 +57,19 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
// Generate new private key and certificate
|
||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||
val dname = X500Name(DNAME)
|
||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||
start.time, end.time, dname, kp.public)
|
||||
val builder = X509v3CertificateBuilder(
|
||||
dname, BigInteger(160, Random()),
|
||||
start.time, end.time, Locale.ROOT, dname,
|
||||
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
|
||||
)
|
||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||
|
||||
// Store them into keystore
|
||||
ks.setKeyEntry(
|
||||
ALIAS, kp.private,
|
||||
PASSWORD, arrayOf(cert))
|
||||
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||
val bytes = ByteArrayOutputStream()
|
||||
GZIPOutputStream(Base64OutputStream(bytes,
|
||||
BASE64_FLAG
|
||||
)).use {
|
||||
ks.store(it,
|
||||
PASSWORD
|
||||
)
|
||||
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||
ks.store(it, PASSWORD)
|
||||
}
|
||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||
|
||||
|
@@ -6,13 +6,13 @@ import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.ActivityTracker
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.createNewResources
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
var currentLocale: Locale = Locale.getDefault()
|
||||
|
||||
@@ -28,6 +28,11 @@ withContext(Dispatchers.Default) {
|
||||
// Create a completely new resource to prevent cross talk over active configs
|
||||
val res = createNewResources()
|
||||
|
||||
fun changeLocale(locale: Locale) {
|
||||
res.configuration.setLocale(locale)
|
||||
res.updateConfiguration(res.configuration, res.displayMetrics)
|
||||
}
|
||||
|
||||
val locales = ArrayList<String>().apply {
|
||||
// Add default locale
|
||||
add("en")
|
||||
@@ -41,13 +46,13 @@ withContext(Dispatchers.Default) {
|
||||
}.map {
|
||||
Locale.forLanguageTag(it)
|
||||
}.distinctBy {
|
||||
res.setLocale(it)
|
||||
changeLocale(it)
|
||||
res.getString(compareId)
|
||||
}.sortedWith { a, b ->
|
||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||
}
|
||||
|
||||
res.setLocale(defaultLocale)
|
||||
changeLocale(defaultLocale)
|
||||
val defName = res.getString(R.string.system_default)
|
||||
|
||||
val names = ArrayList<String>(locales.size + 1)
|
||||
@@ -71,11 +76,6 @@ fun Resources.setConfig(config: Configuration) {
|
||||
|
||||
fun Resources.syncLocale() = setConfig(configuration)
|
||||
|
||||
fun Resources.setLocale(locale: Locale) {
|
||||
configuration.setLocale(locale)
|
||||
updateConfiguration(configuration, displayMetrics)
|
||||
}
|
||||
|
||||
fun refreshLocale() {
|
||||
val localeConfig = Config.locale
|
||||
currentLocale = when {
|
||||
@@ -84,4 +84,5 @@ fun refreshLocale() {
|
||||
}
|
||||
Locale.setDefault(currentLocale)
|
||||
AppContext.resources.syncLocale()
|
||||
ActivityTracker.foreground?.recreate()
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
@@ -0,0 +1,34 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
class RequestInstall : ActivityResultContract<Unit, Boolean>() {
|
||||
|
||||
@TargetApi(26)
|
||||
override fun createIntent(context: Context, input: Unit): Intent {
|
||||
// This will only be called on API 26+
|
||||
return Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
||||
.setData(Uri.parse("package:${context.packageName}"))
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?) =
|
||||
resultCode == Activity.RESULT_OK
|
||||
|
||||
override fun getSynchronousResult(
|
||||
context: Context,
|
||||
input: Unit
|
||||
): SynchronousResult<Boolean>? {
|
||||
if (Build.VERSION.SDK_INT < 26)
|
||||
return SynchronousResult(true)
|
||||
if (context.packageManager.canRequestPackageInstalls())
|
||||
return SynchronousResult(true)
|
||||
return null
|
||||
}
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class RootRegistry(stub: Any?) : RootService() {
|
||||
|
||||
constructor() : this(null)
|
||||
|
||||
private val className: String? = stub?.javaClass?.name
|
||||
|
||||
init {
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
||||
Timber.e(e)
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
// TODO: PLACEHOLDER
|
||||
return Binder()
|
||||
}
|
||||
|
||||
override fun getComponentName(): ComponentName {
|
||||
return ComponentName(packageName, className ?: javaClass.name)
|
||||
}
|
||||
|
||||
// TODO: PLACEHOLDER
|
||||
object Connection : CountDownLatch(1), ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Timber.d("onServiceConnected")
|
||||
countDown()
|
||||
}
|
||||
override fun onNullBinding(name: ComponentName) {
|
||||
Timber.d("onServiceConnected")
|
||||
countDown()
|
||||
}
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
bind(Intent().setComponent(name), this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var bindTask: Runnable? = null
|
||||
}
|
||||
}
|
131
app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt
Normal file
131
app/src/main/java/com/topjohnwu/magisk/core/utils/RootUtils.kt
Normal file
@@ -0,0 +1,131 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.system.Os
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.locks.AbstractQueuedSynchronizer
|
||||
|
||||
class RootUtils(stub: Any?) : RootService() {
|
||||
|
||||
private val className: String = stub?.javaClass?.name ?: javaClass.name
|
||||
private lateinit var am: ActivityManager
|
||||
|
||||
constructor() : this(null) {
|
||||
Timber.plant(object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
super.log(priority, "Magisk", message, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
am = getSystemService()!!
|
||||
}
|
||||
|
||||
override fun getComponentName(): ComponentName {
|
||||
return ComponentName(packageName, className)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return object : IRootUtils.Stub() {
|
||||
override fun getAppProcess(pid: Int) = safe(null) { getAppProcessImpl(pid) }
|
||||
override fun getFileSystem(): IBinder = FileSystemManager.getService()
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> safe(default: T, block: () -> T): T {
|
||||
return try {
|
||||
block()
|
||||
} catch (e: Throwable) {
|
||||
// The process died unexpectedly
|
||||
Timber.e(e)
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppProcessImpl(_pid: Int): ActivityManager.RunningAppProcessInfo? {
|
||||
val procList = am.runningAppProcesses
|
||||
var pid = _pid
|
||||
while (pid > 1) {
|
||||
val proc = procList.find { it.pid == pid }
|
||||
if (proc != null)
|
||||
return proc
|
||||
|
||||
// Stop find when root process
|
||||
if (Os.stat("/proc/$pid").st_uid == 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Find PPID
|
||||
File("/proc/$pid/status").useLines {
|
||||
val line = it.find { l -> l.startsWith("PPid:") } ?: return null
|
||||
pid = line.substring(5).trim().toInt()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
object Connection : AbstractQueuedSynchronizer(), ServiceConnection {
|
||||
init {
|
||||
state = 1
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
Timber.d("onServiceConnected")
|
||||
IRootUtils.Stub.asInterface(service).let {
|
||||
obj = it
|
||||
fs = FileSystemManager.getRemote(it.fileSystem)
|
||||
}
|
||||
releaseShared(1)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
state = 1
|
||||
obj = null
|
||||
bind(Intent().setComponent(name), this)
|
||||
}
|
||||
|
||||
override fun tryAcquireShared(acquires: Int) = if (state == 0) 1 else -1
|
||||
|
||||
override fun tryReleaseShared(releases: Int): Boolean {
|
||||
// Decrement count; signal when transition to zero
|
||||
while (true) {
|
||||
val c = state
|
||||
if (c == 0)
|
||||
return false
|
||||
val n = c - 1
|
||||
if (compareAndSetState(c, n))
|
||||
return n == 0
|
||||
}
|
||||
}
|
||||
|
||||
fun await() {
|
||||
// We cannot await on the main thread
|
||||
if (Info.isRooted && !ShellUtils.onMainThread())
|
||||
acquireSharedInterruptibly(1)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var bindTask: Shell.Task? = null
|
||||
var fs = FileSystemManager.getLocal()
|
||||
private set
|
||||
var obj: IRootUtils? = null
|
||||
get() {
|
||||
Connection.await()
|
||||
return field
|
||||
}
|
||||
private set
|
||||
}
|
||||
}
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
@@ -19,8 +19,9 @@ import java.util.jar.JarFile
|
||||
class ShellInit : Shell.Initializer() {
|
||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||
if (shell.isRoot) {
|
||||
RootRegistry.bindTask?.run()
|
||||
RootRegistry.bindTask = null
|
||||
Info.isRooted = true
|
||||
RootUtils.bindTask?.let { shell.execTask(it) }
|
||||
RootUtils.bindTask = null
|
||||
}
|
||||
shell.newJob().apply {
|
||||
add("export ASH_STANDALONE=1")
|
||||
@@ -29,7 +30,7 @@ class ShellInit : Shell.Initializer() {
|
||||
if (isRunningAsStub) {
|
||||
if (!shell.isRoot)
|
||||
return true
|
||||
val jar = JarFile(DynAPK.current(context))
|
||||
val jar = JarFile(StubApk.current(context))
|
||||
val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
|
||||
localBB = context.deviceProtectedContext.cachedFile("busybox")
|
||||
localBB.delete()
|
||||
@@ -42,7 +43,7 @@ class ShellInit : Shell.Initializer() {
|
||||
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
|
||||
Info.noDataExec = !shell.newJob().add("$localBB sh -c \"$localBB true\"").exec().isSuccess
|
||||
}
|
||||
|
||||
if (Info.noDataExec) {
|
||||
|
@@ -1,7 +1,5 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@@ -31,12 +29,12 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
||||
else
|
||||
entry.name
|
||||
|
||||
var dest = File(folder, name)
|
||||
if (!dest.parentFile!!.exists() && !dest.parentFile!!.mkdirs()) {
|
||||
dest = SuFile(folder, name)
|
||||
dest.parentFile!!.mkdirs()
|
||||
val dest = File(folder, name)
|
||||
dest.parentFile!!.let {
|
||||
if (!it.exists())
|
||||
it.mkdirs()
|
||||
}
|
||||
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
||||
dest.outputStream().use { out -> zin.copyTo(out) }
|
||||
}
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw IOException(e)
|
||||
|
@@ -1,67 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface PreferenceModel {
|
||||
|
||||
val context: Context
|
||||
|
||||
val fileName: String
|
||||
get() = "${context.packageName}_preferences"
|
||||
|
||||
val prefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
fun preferenceStrInt(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = object: ReadWriteProperty<PreferenceModel, Int> {
|
||||
val base = StringProperty(name, default.toString(), commit)
|
||||
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
|
||||
base.getValue(thisRef, property).toInt()
|
||||
|
||||
override fun setValue(thisRef: PreferenceModel, property: KProperty<*>, value: Int) =
|
||||
base.setValue(thisRef, property, value.toString())
|
||||
}
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Boolean,
|
||||
commit: Boolean = false
|
||||
) = BooleanProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = false
|
||||
) = FloatProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = false
|
||||
) = IntProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = false
|
||||
) = LongProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = false
|
||||
) = StringProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = false
|
||||
) = StringSetProperty(name, default, commit)
|
||||
|
||||
}
|
@@ -1,13 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() {
|
||||
|
||||
override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
@@ -8,10 +8,7 @@ import android.text.Spanned
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.*
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.cardview.widget.CardView
|
||||
@@ -29,7 +26,8 @@ import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||
import kotlin.math.roundToInt
|
||||
@@ -289,3 +287,13 @@ fun TextView.setTextColorAttr(attr: Int) {
|
||||
context.theme.resolveAttribute(attr, tv, true)
|
||||
setTextColor(tv.data)
|
||||
}
|
||||
|
||||
@BindingAdapter("android:text")
|
||||
fun TextView.setText(text: TextHolder) {
|
||||
this.text = text.getText(context.resources)
|
||||
}
|
||||
|
||||
@BindingAdapter("items", "layout")
|
||||
fun Spinner.setAdapter(items: Array<Any>, layoutRes: Int) {
|
||||
adapter = ArrayAdapter(context, layoutRes, items)
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import androidx.databinding.ObservableList
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
/**
|
||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
||||
|
@@ -1,35 +1,7 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
|
||||
fun <T : AnyDiffRvItem> diffListOf() =
|
||||
DiffObservableList(DiffRvItem.callback<T>())
|
||||
|
||||
fun <T : AnyDiffRvItem> diffListOf(newItems: List<T>) =
|
||||
DiffObservableList(DiffRvItem.callback<T>()).also { it.update(newItems) }
|
||||
|
||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
||||
|
||||
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: T
|
||||
) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
item.onBindingBound(binding)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T : RvItem> itemBindingOf(
|
||||
crossinline body: (ItemBinding<*>) -> Unit = {}
|
||||
) = OnItemBind<T> { itemBinding, _, item ->
|
||||
item.bind(itemBinding)
|
||||
body(itemBinding)
|
||||
}
|
||||
|
@@ -0,0 +1,162 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ListChangeRegistry
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||
import java.util.*
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class MergeObservableList<T> : AbstractList<T>(), ObservableList<T> {
|
||||
|
||||
private val lists: MutableList<List<T>> = mutableListOf()
|
||||
private val listeners = ListChangeRegistry()
|
||||
private val callback = Callback<T>()
|
||||
|
||||
override fun addOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||
listeners.add(callback)
|
||||
}
|
||||
|
||||
override fun removeOnListChangedCallback(callback: OnListChangedCallback<out ObservableList<T>>) {
|
||||
listeners.remove(callback)
|
||||
}
|
||||
|
||||
override fun get(index: Int): T {
|
||||
if (index < 0)
|
||||
throw IndexOutOfBoundsException()
|
||||
var idx = index
|
||||
for (list in lists) {
|
||||
val size = list.size
|
||||
if (idx < size) {
|
||||
return list[idx]
|
||||
}
|
||||
idx -= size
|
||||
}
|
||||
throw IndexOutOfBoundsException()
|
||||
}
|
||||
|
||||
override val size: Int
|
||||
get() = lists.fold(0) { i, it -> i + it.size }
|
||||
|
||||
|
||||
fun insertItem(obj: T): MergeObservableList<T> {
|
||||
val idx = size
|
||||
lists.add(listOf(obj))
|
||||
++modCount
|
||||
listeners.notifyInserted(this, idx, 1)
|
||||
return this
|
||||
}
|
||||
|
||||
fun insertList(list: ObservableList<out T>): MergeObservableList<T> {
|
||||
val idx = size
|
||||
lists.add(list)
|
||||
++modCount
|
||||
(list as ObservableList<T>).addOnListChangedCallback(callback)
|
||||
if (list.isNotEmpty())
|
||||
listeners.notifyInserted(this, idx, list.size)
|
||||
return this
|
||||
}
|
||||
|
||||
fun removeItem(obj: T): Boolean {
|
||||
var idx = 0
|
||||
for ((i, list) in lists.withIndex()) {
|
||||
if (list !is ObservableList<*>) {
|
||||
if (obj == list[0]) {
|
||||
lists.removeAt(i)
|
||||
++modCount
|
||||
listeners.notifyRemoved(this, idx, 1)
|
||||
return true
|
||||
}
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun removeList(listToRemove: ObservableList<out T>): Boolean {
|
||||
var idx = 0
|
||||
for ((i, list) in lists.withIndex()) {
|
||||
if (listToRemove === list) {
|
||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
||||
lists.removeAt(i)
|
||||
++modCount
|
||||
listeners.notifyRemoved(this, idx, list.size)
|
||||
return true
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
val sz = size
|
||||
for (list in lists) {
|
||||
if (list is ObservableList<*>) {
|
||||
(list as ObservableList<T>).removeOnListChangedCallback(callback)
|
||||
}
|
||||
}
|
||||
++modCount
|
||||
lists.clear()
|
||||
if (sz > 0)
|
||||
listeners.notifyRemoved(this, 0, sz)
|
||||
}
|
||||
|
||||
private fun subIndexToIndex(subList: List<*>, index: Int): Int {
|
||||
if (index < 0)
|
||||
throw IndexOutOfBoundsException()
|
||||
var idx = 0
|
||||
for (list in lists) {
|
||||
if (subList === list) {
|
||||
return idx + index
|
||||
}
|
||||
idx += list.size
|
||||
}
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
inner class Callback<T> : OnListChangedCallback<ObservableList<T>>() {
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
++modCount
|
||||
listeners.notifyChanged(this@MergeObservableList)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
listeners.notifyChanged(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
++modCount
|
||||
listeners.notifyInserted(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
val idx = subIndexToIndex(sender, 0)
|
||||
listeners.notifyMoved(this@MergeObservableList,
|
||||
idx + fromPosition, idx + toPosition, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
++modCount
|
||||
listeners.notifyRemoved(this@MergeObservableList,
|
||||
subIndexToIndex(sender, positionStart), itemCount)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,33 +1,21 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.BR
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
abstract class RvItem {
|
||||
|
||||
abstract val layoutRes: Int
|
||||
|
||||
@CallSuper
|
||||
open fun bind(binding: ItemBinding<*>) {
|
||||
binding.set(BR.item, layoutRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is useful if you want to manipulate your views directly.
|
||||
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
|
||||
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
|
||||
*/
|
||||
open fun onBindingBound(binding: ViewDataBinding) {}
|
||||
}
|
||||
|
||||
interface RvContainer<E> {
|
||||
val item: E
|
||||
}
|
||||
|
||||
interface ViewAwareRvItem {
|
||||
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
}
|
||||
|
||||
interface ComparableRv<T> : Comparable<T> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun comparableEqual(o: Any?) =
|
||||
@@ -77,13 +65,3 @@ abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
|
||||
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||
override var callbacks: PropertyChangeRegistry? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
||||
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
|
||||
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
|
||||
* */
|
||||
|
||||
interface LenientRvItem {
|
||||
fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||
}
|
||||
|
@@ -1,35 +0,0 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
class RvBindingAdapter<T : RvItem> : BindingRecyclerViewAdapter<T>() {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: T
|
||||
) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
when (item) {
|
||||
is LenientRvItem -> {
|
||||
val recycler = recyclerView ?: return
|
||||
item.onBindingBound(binding)
|
||||
item.onBindingBound(binding, recycler)
|
||||
}
|
||||
else -> item.onBindingBound(binding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
this.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ObservableList
|
||||
import androidx.databinding.ObservableList.OnListChangedCallback
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.BR
|
||||
|
||||
class RvItemAdapter<T: RvItem>(
|
||||
private val items: List<T>,
|
||||
private val extraBindings: SparseArray<*>?
|
||||
) : RecyclerView.Adapter<RvItemAdapter.ViewHolder>() {
|
||||
|
||||
private var lifecycleOwner: LifecycleOwner? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private val observer by lazy(LazyThreadSafetyMode.NONE) { ListObserver<T>() }
|
||||
|
||||
override fun onAttachedToRecyclerView(rv: RecyclerView) {
|
||||
lifecycleOwner = rv.findViewTreeLifecycleOwner()
|
||||
recyclerView = rv
|
||||
if (items is ObservableList)
|
||||
items.addOnListChangedCallback(observer)
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(rv: RecyclerView) {
|
||||
lifecycleOwner = null
|
||||
recyclerView = null
|
||||
if (items is ObservableList)
|
||||
items.removeOnListChangedCallback(observer)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, layoutRes: Int): ViewHolder {
|
||||
val inflator = LayoutInflater.from(parent.context)
|
||||
return ViewHolder(DataBindingUtil.inflate(inflator, layoutRes, parent, false))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
holder.binding.setVariable(BR.item, item)
|
||||
extraBindings?.let {
|
||||
for (i in 0 until it.size()) {
|
||||
holder.binding.setVariable(it.keyAt(i), it.valueAt(i))
|
||||
}
|
||||
}
|
||||
holder.binding.lifecycleOwner = lifecycleOwner
|
||||
holder.binding.executePendingBindings()
|
||||
recyclerView?.let {
|
||||
if (item is ViewAwareRvItem)
|
||||
item.onBind(holder.binding, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun getItemViewType(position: Int) = items[position].layoutRes
|
||||
|
||||
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
inner class ListObserver<T: RvItem> : OnListChangedCallback<ObservableList<T>>() {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onChanged(sender: ObservableList<T>) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeChanged(positionStart, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeInserted(positionStart, itemCount)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>?,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
for (i in 0 until itemCount) {
|
||||
notifyItemMoved(fromPosition + i, toPosition + i)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
notifyItemRangeRemoved(positionStart, itemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun bindExtra(body: (SparseArray<Any?>) -> Unit) = SparseArray<Any?>().also(body)
|
||||
|
||||
@BindingAdapter("items", "extraBindings", requireAll = false)
|
||||
fun <T: RvItem> RecyclerView.setAdapter(items: List<T>?, extraBindings: SparseArray<*>?) {
|
||||
if (items != null) {
|
||||
adapter = RvItemAdapter(items, extraBindings)
|
||||
}
|
||||
}
|
@@ -8,7 +8,6 @@ import android.widget.PopupMenu
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.magisk.ktx.reboot as systemReboot
|
||||
|
||||
object RebootEvent {
|
||||
@@ -20,7 +19,7 @@ object RebootEvent {
|
||||
R.id.action_reboot_bootloader -> systemReboot("bootloader")
|
||||
R.id.action_reboot_download -> systemReboot("download")
|
||||
R.id.action_reboot_edl -> systemReboot("edl")
|
||||
R.id.action_reboot_recovery -> Shell.su("/system/bin/reboot recovery").submit()
|
||||
R.id.action_reboot_recovery -> systemReboot("recovery")
|
||||
else -> Unit
|
||||
}
|
||||
return true
|
||||
|
@@ -1,44 +0,0 @@
|
||||
package com.topjohnwu.magisk.events
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.arch.ActivityExecutor
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.arch.ViewEvent
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
|
||||
class SnackbarEvent constructor(
|
||||
private val msg: TextHolder,
|
||||
private val length: Int = Snackbar.LENGTH_SHORT,
|
||||
private val builder: Snackbar.() -> Unit = {}
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
|
||||
constructor(
|
||||
@StringRes res: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
builder: Snackbar.() -> Unit = {}
|
||||
) : this(res.asText(), length, builder)
|
||||
|
||||
constructor(
|
||||
msg: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
builder: Snackbar.() -> Unit = {}
|
||||
) : this(msg.asText(), length, builder)
|
||||
|
||||
|
||||
private fun snackbar(
|
||||
view: View,
|
||||
anchor: View?,
|
||||
message: String,
|
||||
length: Int,
|
||||
builder: Snackbar.() -> Unit
|
||||
) = Snackbar.make(view, message, length).setAnchorView(anchor).apply(builder).show()
|
||||
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
snackbar(activity.snackbarView, activity.snackbarAnchorView,
|
||||
msg.getText(activity.resources).toString(),
|
||||
length, builder)
|
||||
}
|
||||
}
|
@@ -1,16 +1,14 @@
|
||||
package com.topjohnwu.magisk.events
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.topjohnwu.magisk.arch.*
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||
import com.topjohnwu.magisk.utils.TextHolder
|
||||
import com.topjohnwu.magisk.utils.asText
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
|
||||
class PermissionEvent(
|
||||
@@ -48,16 +46,12 @@ class RecreateEvent : ViewEvent(), ActivityExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
class MagiskInstallFileEvent(
|
||||
private val callback: (Uri) -> Unit
|
||||
class GetContentEvent(
|
||||
private val type: String,
|
||||
private val callback: ContentResultCallback
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
try {
|
||||
activity.getContent("*/*", callback)
|
||||
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||
}
|
||||
activity.getContent(type, callback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +61,7 @@ class NavigationEvent(
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
(activity as? NavigationActivity<*>)?.apply {
|
||||
if (pop) navigation?.popBackStack()
|
||||
if (pop) navigation.popBackStack()
|
||||
directions.navigate()
|
||||
}
|
||||
}
|
||||
@@ -79,16 +73,25 @@ class AddHomeIconEvent : ViewEvent(), ContextExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
class SelectModuleEvent : ViewEvent(), FragmentExecutor {
|
||||
override fun invoke(fragment: BaseFragment<*>) {
|
||||
try {
|
||||
fragment.apply {
|
||||
activity?.getContent("application/zip") {
|
||||
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
|
||||
}
|
||||
}
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||
}
|
||||
class SnackbarEvent(
|
||||
private val msg: TextHolder,
|
||||
private val length: Int = Snackbar.LENGTH_SHORT,
|
||||
private val builder: Snackbar.() -> Unit = {}
|
||||
) : ViewEvent(), ActivityExecutor {
|
||||
|
||||
constructor(
|
||||
@StringRes res: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
builder: Snackbar.() -> Unit = {}
|
||||
) : this(res.asText(), length, builder)
|
||||
|
||||
constructor(
|
||||
msg: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
builder: Snackbar.() -> Unit = {}
|
||||
) : this(msg.asText(), length, builder)
|
||||
|
||||
override fun invoke(activity: UIActivity<*>) {
|
||||
activity.showSnackbar(msg.getText(activity.resources), length, builder)
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package com.topjohnwu.magisk.events.dialog
|
||||
import android.app.Activity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.UIActivity
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
@@ -33,6 +34,6 @@ class DarkThemeDialog : DialogEvent() {
|
||||
|
||||
private fun selectTheme(mode: Int, activity: Activity) {
|
||||
Config.darkTheme = mode
|
||||
activity.recreate()
|
||||
(activity as UIActivity<*>).delegate.localNightMode = mode
|
||||
}
|
||||
}
|
||||
|
@@ -2,10 +2,10 @@ package com.topjohnwu.magisk.events.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.AppContext
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import java.io.File
|
||||
|
||||
@@ -29,7 +29,7 @@ class ManagerInstallDialog : MarkDownDialog() {
|
||||
setCancelable(true)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.install
|
||||
onClick { DownloadService.start(context, Subject.Manager()) }
|
||||
onClick { DownloadService.start(context, Subject.App()) }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
|
@@ -6,7 +6,7 @@ import androidx.annotation.CallSuper
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@@ -1,11 +1,11 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
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.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||
|
@@ -1,22 +0,0 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class RestoreAppDialog : DialogEvent() {
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
dialog.apply {
|
||||
setTitle(R.string.settings_restore_app_title)
|
||||
setMessage(R.string.restore_app_confirmation)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = android.R.string.ok
|
||||
onClick { HideAPK.restore(dialog.ownerActivity!!) }
|
||||
}
|
||||
setButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
text = android.R.string.cancel
|
||||
}
|
||||
setCancelable(true)
|
||||
}
|
||||
}
|
||||
}
|
@@ -34,7 +34,7 @@ class UninstallDialog : DialogEvent() {
|
||||
show()
|
||||
}
|
||||
|
||||
Shell.su("restore_imgs").submit { result ->
|
||||
Shell.cmd("restore_imgs").submit { result ->
|
||||
dialog.dismiss()
|
||||
if (result.isSuccess) {
|
||||
Utils.toast(R.string.restore_done, Toast.LENGTH_SHORT)
|
||||
|
@@ -7,11 +7,10 @@ import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.AdaptiveIconDrawable
|
||||
@@ -19,40 +18,24 @@ import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.text.PrecomputedText
|
||||
import android.os.Process
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.text.PrecomputedTextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.TransitionManager
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.di.AppContext
|
||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import kotlin.Array
|
||||
import kotlin.String
|
||||
import java.lang.reflect.Array as JArray
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
@@ -79,12 +62,10 @@ val Context.deviceProtectedContext: Context get() =
|
||||
createDeviceProtectedStorageContext()
|
||||
} else { this }
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun Intent.startActivityWithRoot() {
|
||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||
val cmd = toCommand(args).joinToString(" ")
|
||||
Shell.su(cmd).submit()
|
||||
Shell.cmd(cmd).submit()
|
||||
}
|
||||
|
||||
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
||||
@@ -185,16 +166,8 @@ fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<S
|
||||
return args
|
||||
}
|
||||
|
||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
||||
|
||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||
|
||||
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
|
||||
val out = mutableListOf<Result>()
|
||||
while (moveToNext()) out.add(transformer(this))
|
||||
return out
|
||||
}
|
||||
|
||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
runCatching {
|
||||
if (labelRes > 0) {
|
||||
@@ -209,37 +182,6 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
return loadLabel(pm).toString()
|
||||
}
|
||||
|
||||
fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null
|
||||
|
||||
fun Context.colorCompat(@ColorRes id: Int) = try {
|
||||
ContextCompat.getColor(this, id)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun Context.colorStateListCompat(@ColorRes id: Int) = try {
|
||||
ContextCompat.getColorStateList(this, id)
|
||||
} catch (e: Resources.NotFoundException) {
|
||||
null
|
||||
}
|
||||
|
||||
fun Context.drawableCompat(@DrawableRes id: Int) = AppCompatResources.getDrawable(this, id)
|
||||
/**
|
||||
* Pass [start] and [end] dimensions, function will return left and right
|
||||
* with respect to RTL layout direction
|
||||
*/
|
||||
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
|
||||
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
|
||||
return end to start
|
||||
}
|
||||
return start to end
|
||||
}
|
||||
|
||||
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())
|
||||
|
||||
inline fun <reified T> T.createClassLoader(apk: File) =
|
||||
DynamicClassLoader(apk, T::class.java.classLoader)
|
||||
|
||||
fun Context.unwrap(): Context {
|
||||
var context = this
|
||||
while (true) {
|
||||
@@ -262,21 +204,6 @@ fun Activity.hideKeyboard() {
|
||||
view.clearFocus()
|
||||
}
|
||||
|
||||
fun Fragment.hideKeyboard() {
|
||||
activity?.hideKeyboard()
|
||||
}
|
||||
|
||||
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
|
||||
|
||||
fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Unit) =
|
||||
viewTreeObserver.addOnGlobalLayoutListener(object :
|
||||
ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
callback()
|
||||
}
|
||||
})
|
||||
|
||||
fun ViewGroup.startAnimations() {
|
||||
val transition = AutoTransition()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
@@ -308,3 +235,33 @@ fun getProperty(key: String, def: String): String {
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun PackageManager.getPackageInfo(uid: Int, pid: Int): PackageInfo? {
|
||||
val flag = PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
val pkgs = getPackagesForUid(uid) ?: throw PackageManager.NameNotFoundException()
|
||||
if (pkgs.size > 1) {
|
||||
if (pid <= 0) {
|
||||
return null
|
||||
}
|
||||
// Try to find package name from PID
|
||||
val proc = RootUtils.obj?.getAppProcess(pid)
|
||||
if (proc == null) {
|
||||
if (uid == Process.SHELL_UID) {
|
||||
// It is possible that some apps installed are sharing UID with shell.
|
||||
// We will not be able to find a package from the active process list,
|
||||
// because the client is forked from ADB shell, not any app process.
|
||||
return getPackageInfo("com.android.shell", flag)
|
||||
}
|
||||
} else if (uid == proc.uid) {
|
||||
return getPackageInfo(proc.pkgList[0], flag)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
if (pkgs.size == 1) {
|
||||
return getPackageInfo(pkgs[0], flag)
|
||||
}
|
||||
throw PackageManager.NameNotFoundException()
|
||||
}
|
||||
|
@@ -1,65 +0,0 @@
|
||||
package com.topjohnwu.magisk.ktx
|
||||
|
||||
import androidx.databinding.ObservableList
|
||||
|
||||
fun <T> ObservableList<T>.addOnListChangedCallback(
|
||||
onChanged: ((sender: ObservableList<T>) -> Unit)? = null,
|
||||
onItemRangeRemoved: ((sender: ObservableList<T>, positionStart: Int, itemCount: Int) -> Unit)? = null,
|
||||
onItemRangeMoved: ((sender: ObservableList<T>, fromPosition: Int, toPosition: Int, itemCount: Int) -> Unit)? = null,
|
||||
onItemRangeInserted: ((sender: ObservableList<T>, positionStart: Int, itemCount: Int) -> Unit)? = null,
|
||||
onItemRangeChanged: ((sender: ObservableList<T>, positionStart: Int, itemCount: Int) -> Unit)? = null
|
||||
) = addOnListChangedCallback(object : ObservableList.OnListChangedCallback<ObservableList<T>>() {
|
||||
override fun onChanged(sender: ObservableList<T>?) {
|
||||
onChanged?.invoke(sender ?: return)
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeRemoved?.invoke(
|
||||
sender ?: return,
|
||||
positionStart,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(
|
||||
sender: ObservableList<T>?,
|
||||
fromPosition: Int,
|
||||
toPosition: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeMoved?.invoke(
|
||||
sender ?: return,
|
||||
fromPosition,
|
||||
toPosition,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeInserted?.invoke(
|
||||
sender ?: return,
|
||||
positionStart,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemRangeChanged(
|
||||
sender: ObservableList<T>?,
|
||||
positionStart: Int,
|
||||
itemCount: Int
|
||||
) {
|
||||
onItemRangeChanged?.invoke(
|
||||
sender ?: return,
|
||||
positionStart,
|
||||
itemCount
|
||||
)
|
||||
}
|
||||
})
|
@@ -1,11 +0,0 @@
|
||||
package com.topjohnwu.magisk.ktx
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R): Flow<R> {
|
||||
return flatMapMerge { value ->
|
||||
flow { emit(transform(value)) }
|
||||
}
|
||||
}
|
@@ -1,11 +1,15 @@
|
||||
package com.topjohnwu.magisk.ktx
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import timber.log.Timber
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.lang.reflect.Field
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
@@ -45,8 +49,28 @@ fun <T> MutableSet<T>.synchronized(): MutableSet<T> = Collections.synchronizedSe
|
||||
|
||||
fun <K, V> MutableMap<K, V>.synchronized(): MutableMap<K, V> = Collections.synchronizedMap(this)
|
||||
|
||||
fun SimpleDateFormat.parseOrNull(date: String) =
|
||||
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
|
||||
|
||||
fun Class<*>.reflectField(name: String): Field =
|
||||
getDeclaredField(name).apply { isAccessible = true }
|
||||
|
||||
inline fun <T, R> Flow<T>.concurrentMap(crossinline transform: suspend (T) -> R): Flow<R> {
|
||||
return flatMapMerge { value ->
|
||||
flow { emit(transform(value)) }
|
||||
}
|
||||
}
|
||||
|
||||
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||
|
||||
// Some devices don't allow filenames containing ":"
|
||||
val timeFormatStandard by lazy {
|
||||
SimpleDateFormat(
|
||||
"yyyy-MM-dd'T'HH.mm.ss",
|
||||
currentLocale
|
||||
)
|
||||
}
|
||||
val timeDateFormat: DateFormat by lazy {
|
||||
DateFormat.getDateTimeInstance(
|
||||
DateFormat.DEFAULT,
|
||||
DateFormat.DEFAULT,
|
||||
currentLocale
|
||||
)
|
||||
}
|
@@ -8,14 +8,18 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
||||
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||
if (reason == "recovery") {
|
||||
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
|
||||
Shell.cmd("/system/bin/input keyevent 26").submit()
|
||||
}
|
||||
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||
}
|
||||
|
||||
fun relaunchApp(context: Context) {
|
||||
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return
|
||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||
val cmd = intent.toCommand(args).joinToString(separator = " ")
|
||||
Shell.su("run_delay 1 \"$cmd\"").exec()
|
||||
Shell.cmd("run_delay 1 \"$cmd\"").exec()
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
|
@@ -1,21 +0,0 @@
|
||||
package com.topjohnwu.magisk.ktx
|
||||
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
val now get() = System.currentTimeMillis()
|
||||
|
||||
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss",
|
||||
currentLocale
|
||||
) }
|
||||
|
||||
val timeDateFormat: DateFormat by lazy {
|
||||
DateFormat.getDateTimeInstance(
|
||||
DateFormat.DEFAULT,
|
||||
DateFormat.DEFAULT,
|
||||
currentLocale
|
||||
)
|
||||
}
|
@@ -17,7 +17,6 @@ import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -492,7 +491,7 @@ public class SignApk {
|
||||
}
|
||||
|
||||
public static void sign(X509Certificate cert, PrivateKey key,
|
||||
JarMap inputJar, FileOutputStream outputFile) throws Exception {
|
||||
JarMap inputJar, OutputStream outputStream) throws Exception {
|
||||
int alignment = 4;
|
||||
int hashes = 0;
|
||||
|
||||
@@ -531,7 +530,7 @@ public class SignApk {
|
||||
// This assumes outputChunks are array-backed. To avoid this assumption, the
|
||||
// code could be rewritten to use FileChannel.
|
||||
for (ByteBuffer outputChunk : outputChunks) {
|
||||
outputFile.write(outputChunk.array(),
|
||||
outputStream.write(outputChunk.array(),
|
||||
outputChunk.arrayOffset() + outputChunk.position(), outputChunk.remaining());
|
||||
outputChunk.position(outputChunk.limit());
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.signing;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -33,7 +32,6 @@ import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
|
||||
@Keep
|
||||
public class SignBoot {
|
||||
|
||||
private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
|
||||
|
@@ -13,11 +13,14 @@ import androidx.core.view.isVisible
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseMainActivity
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.arch.viewModel
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
||||
import com.topjohnwu.magisk.di.viewModel
|
||||
import com.topjohnwu.magisk.ktx.startAnimations
|
||||
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
@@ -27,11 +30,16 @@ import java.io.File
|
||||
|
||||
class MainViewModel : BaseViewModel()
|
||||
|
||||
class MainActivity : BaseMainActivity<ActivityMainMd2Binding>() {
|
||||
class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
||||
|
||||
override val layoutRes = R.layout.activity_main_md2
|
||||
override val viewModel by viewModel<MainViewModel>()
|
||||
override val navHostId: Int = R.id.main_nav_host
|
||||
override val snackbarView: View
|
||||
get() {
|
||||
val fragmentOverride = currentFragment?.snackbarView
|
||||
return fragmentOverride ?: super.snackbarView
|
||||
}
|
||||
override val snackbarAnchorView: View?
|
||||
get() {
|
||||
val fragmentAnchor = currentFragment?.snackbarAnchorView
|
||||
@@ -51,7 +59,7 @@ class MainActivity : BaseMainActivity<ActivityMainMd2Binding>() {
|
||||
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
navigation?.addOnDestinationChangedListener { _, destination, _ ->
|
||||
navigation.addOnDestinationChangedListener { _, destination, _ ->
|
||||
isRootFragment = when (destination.id) {
|
||||
R.id.homeFragment,
|
||||
R.id.modulesFragment,
|
||||
@@ -81,7 +89,7 @@ class MainActivity : BaseMainActivity<ActivityMainMd2Binding>() {
|
||||
}
|
||||
binding.mainNavigation.menu.apply {
|
||||
findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
|
||||
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive
|
||||
findItem(R.id.modulesFragment)?.isEnabled = Info.env.isActive && LocalModule.loaded()
|
||||
}
|
||||
|
||||
val section =
|
||||
|
146
app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt
Normal file
146
app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.StubApk
|
||||
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.JobService
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||
|
||||
companion object {
|
||||
private var skipSplash = false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(Theme.selected.themeRes)
|
||||
|
||||
if (isRunningAsStub && !skipSplash) {
|
||||
// Manually apply splash theme for stub
|
||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!isRunningAsStub) {
|
||||
val splashScreen = installSplashScreen()
|
||||
splashScreen.setKeepOnScreenCondition { !skipSplash }
|
||||
}
|
||||
|
||||
if (skipSplash) {
|
||||
showMainUI(savedInstanceState)
|
||||
} else {
|
||||
Shell.getShell(Shell.EXECUTOR) {
|
||||
if (isRunningAsStub && !it.isRoot) {
|
||||
showInvalidStateMessage()
|
||||
return@getShell
|
||||
}
|
||||
preLoad(savedInstanceState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun showInvalidStateMessage(): Unit = runOnUiThread {
|
||||
MagiskDialog(this).apply {
|
||||
setTitle(R.string.unsupport_nonroot_stub_title)
|
||||
setMessage(R.string.unsupport_nonroot_stub_msg)
|
||||
setButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
text = R.string.install
|
||||
onClick {
|
||||
withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||
if (!it) {
|
||||
Utils.toast(R.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||
showInvalidStateMessage()
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
HideAPK.restore(this@SplashActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun preLoad(savedState: Bundle?) {
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)?.let {
|
||||
// Make sure the calling package matches (prevent DoS)
|
||||
if (it == realCallingPackage)
|
||||
it
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
Config.load(prevPkg)
|
||||
handleRepackage(prevPkg)
|
||||
if (prevPkg != null) {
|
||||
runOnUiThread {
|
||||
// Relaunch the process after package migration
|
||||
StubApk.restartProcess(this)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Notifications.setup(this)
|
||||
JobService.schedule(this)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
// Pre-fetch network services
|
||||
ServiceLocator.networkService
|
||||
|
||||
// Wait for root service
|
||||
RootUtils.Connection.await()
|
||||
|
||||
runOnUiThread {
|
||||
skipSplash = true
|
||||
if (isRunningAsStub) {
|
||||
// Re-launch main activity without splash theme
|
||||
relaunch()
|
||||
} else {
|
||||
showMainUI(savedState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRepackage(pkg: String?) {
|
||||
if (packageName != APPLICATION_ID) {
|
||||
runCatching {
|
||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
||||
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (!Const.Version.atLeast_25_0() && Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user