mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-16 08:27:27 +00:00
Compare commits
268 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 |
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,19 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ""
|
||||||
labels: ''
|
labels: ""
|
||||||
assignees: ''
|
assignees: ""
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
## READ BEFORE OPENING ISSUES
|
## 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.
|
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.
|
||||||
|
|
||||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -25,9 +25,6 @@
|
|||||||
[submodule "pcre"]
|
[submodule "pcre"]
|
||||||
path = native/jni/external/pcre
|
path = native/jni/external/pcre
|
||||||
url = https://android.googlesource.com/platform/external/pcre
|
url = https://android.googlesource.com/platform/external/pcre
|
||||||
[submodule "xhook"]
|
|
||||||
path = native/jni/external/xhook
|
|
||||||
url = https://github.com/iqiyi/xHook.git
|
|
||||||
[submodule "libcxx"]
|
[submodule "libcxx"]
|
||||||
path = native/jni/external/libcxx
|
path = native/jni/external/libcxx
|
||||||
url = https://github.com/topjohnwu/libcxx.git
|
url = https://github.com/topjohnwu/libcxx.git
|
||||||
@@ -43,3 +40,6 @@
|
|||||||
[submodule "zopfli"]
|
[submodule "zopfli"]
|
||||||
path = native/jni/external/zopfli
|
path = native/jni/external/zopfli
|
||||||
url = https://github.com/google/zopfli.git
|
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.
|
[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/v24.1)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.1)
|
||||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v24.2)
|
[](https://github.com/topjohnwu/Magisk/releases/tag/v25.1)
|
||||||
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-release.apk)
|
||||||
|
[](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
|
||||||
|
|
||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ Some highlight features:
|
|||||||
|
|
||||||
## Bug Reports
|
## 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 installation issues, upload both boot image and install logs.<br>
|
||||||
For Magisk issues, upload boot logcat or dmesg.<br>
|
For Magisk issues, upload boot logcat or dmesg.<br>
|
||||||
@@ -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
|
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||||
- To start building, run `build.py` to see your options. \
|
- To start building, run `build.py` to see your options. \
|
||||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||||
- To start development, open the project 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.
|
- 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
|
## Translation Contributions
|
||||||
|
|
||||||
|
@@ -19,6 +19,8 @@ kapt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
namespace = "com.topjohnwu.magisk"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.topjohnwu.magisk"
|
applicationId = "com.topjohnwu.magisk"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
@@ -54,11 +56,6 @@ android {
|
|||||||
keepDebugSymbols += "**/*.so"
|
keepDebugSymbols += "**/*.so"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "11"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=enable")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupApp()
|
setupApp()
|
||||||
@@ -74,22 +71,17 @@ dependencies {
|
|||||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation("org.bouncycastle:bcpkix-jdk18on:1.71")
|
||||||
implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
|
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("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
|
||||||
implementation("io.noties.markwon:core:4.6.2")
|
implementation("io.noties.markwon:core:4.6.2")
|
||||||
|
|
||||||
val vBAdapt = "4.0.0"
|
val vLibsu = "5.0.2"
|
||||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
|
||||||
implementation("${bindingAdapter}:${vBAdapt}")
|
|
||||||
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
|
|
||||||
|
|
||||||
val vLibsu = "4.0.2"
|
|
||||||
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
|
||||||
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
|
|
||||||
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
|
||||||
|
implementation("com.github.topjohnwu.libsu:nio:${vLibsu}")
|
||||||
|
|
||||||
val vRetrofit = "2.9.0"
|
val vRetrofit = "2.9.0"
|
||||||
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
|
||||||
@@ -105,24 +97,24 @@ dependencies {
|
|||||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||||
|
|
||||||
val vRoom = "2.4.1"
|
val vRoom = "2.5.0-alpha02"
|
||||||
implementation("androidx.room:room-runtime:${vRoom}")
|
implementation("androidx.room:room-runtime:${vRoom}")
|
||||||
implementation("androidx.room:room-ktx:${vRoom}")
|
implementation("androidx.room:room-ktx:${vRoom}")
|
||||||
kapt("androidx.room:room-compiler:${vRoom}")
|
kapt("androidx.room:room-compiler:${vRoom}")
|
||||||
|
|
||||||
val vNav = "2.5.0-alpha01"
|
val vNav = "2.5.0-rc01"
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||||
|
|
||||||
implementation("androidx.biometric:biometric:1.1.0")
|
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.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.preference:preference:1.2.0")
|
||||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
implementation("androidx.fragment:fragment-ktx:1.4.1")
|
||||||
implementation("androidx.transition:transition:1.4.1")
|
implementation("androidx.transition:transition:1.4.1")
|
||||||
implementation("androidx.core:core-ktx:1.7.0")
|
implementation("androidx.core:core-ktx:1.8.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.0-beta01")
|
implementation("androidx.core:core-splashscreen:1.0.0-rc01")
|
||||||
implementation("com.google.android.material:material:1.5.0")
|
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
|
# Parcelable
|
||||||
-keepclassmembers class * implements android.os.Parcelable {
|
-keepclassmembers class * implements android.os.Parcelable {
|
||||||
public static final ** CREATOR;
|
public static final ** CREATOR;
|
||||||
@@ -26,6 +8,9 @@
|
|||||||
public static void check*(...);
|
public static void check*(...);
|
||||||
public static void throw*(...);
|
public static void throw*(...);
|
||||||
}
|
}
|
||||||
|
-assumenosideeffects class java.util.Objects {
|
||||||
|
public static ** requireNonNull(...);
|
||||||
|
}
|
||||||
|
|
||||||
# Stub
|
# Stub
|
||||||
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
|
||||||
@@ -34,6 +19,11 @@
|
|||||||
boolean mActivityHandlesUiMode;
|
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
|
# Strip Timber verbose and debug logging
|
||||||
-assumenosideeffects class timber.log.Timber$Tree {
|
-assumenosideeffects class timber.log.Timber$Tree {
|
||||||
public void v(**);
|
public void v(**);
|
||||||
|
@@ -5,9 +5,7 @@ plugins {
|
|||||||
setupCommon()
|
setupCommon()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
namespace = "com.topjohnwu.shared"
|
||||||
consumerProguardFiles("proguard-rules.pro")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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,14 +1,20 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
|
import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.res.AssetManager;
|
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.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -50,13 +56,22 @@ public class StubApk {
|
|||||||
return new File(getDynDir(info), "update.apk");
|
return new File(getDynDir(info), "update.apk");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addAssetPath(AssetManager asset, String path) {
|
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 {
|
try {
|
||||||
if (addAssetPath == null)
|
if (addAssetPath == null)
|
||||||
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
|
||||||
addAssetPath.invoke(asset, path);
|
addAssetPath.invoke(asset, path);
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void restartProcess(Activity activity) {
|
public static void restartProcess(Activity activity) {
|
||||||
Intent intent = activity.getPackageManager()
|
Intent intent = activity.getPackageManager()
|
||||||
|
@@ -5,18 +5,17 @@ import java.io.IOException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
import dalvik.system.DexClassLoader;
|
import dalvik.system.BaseDexClassLoader;
|
||||||
|
|
||||||
public class DynamicClassLoader extends DexClassLoader {
|
public class DynamicClassLoader extends BaseDexClassLoader {
|
||||||
|
|
||||||
private static final ClassLoader base = Object.class.getClassLoader();
|
|
||||||
|
|
||||||
public DynamicClassLoader(File apk) {
|
public DynamicClassLoader(File apk) {
|
||||||
super(apk.getPath(), apk.getParent(), null, base);
|
this(apk, getSystemClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
public DynamicClassLoader(File apk, ClassLoader parent) {
|
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
|
@Override
|
||||||
@@ -28,7 +27,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Then check boot classpath
|
// Then check boot classpath
|
||||||
return base.loadClass(name);
|
return getSystemClassLoader().loadClass(name);
|
||||||
} catch (ClassNotFoundException ignored) {
|
} catch (ClassNotFoundException ignored) {
|
||||||
try {
|
try {
|
||||||
// Next try current dex
|
// Next try current dex
|
||||||
@@ -46,7 +45,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public URL getResource(String name) {
|
public URL getResource(String name) {
|
||||||
URL resource = base.getResource(name);
|
URL resource = getSystemClassLoader().getResource(name);
|
||||||
if (resource != null)
|
if (resource != null)
|
||||||
return resource;
|
return resource;
|
||||||
resource = findResource(name);
|
resource = findResource(name);
|
||||||
@@ -58,7 +57,7 @@ public class DynamicClassLoader extends DexClassLoader {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Enumeration<URL> getResources(String name) throws IOException {
|
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));
|
findResources(name), getParent().getResources(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="com.topjohnwu.magisk">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".core.App"
|
android:name=".core.App"
|
||||||
@@ -29,7 +26,6 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.surequest.SuRequestActivity"
|
android:name=".ui.surequest.SuRequestActivity"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
tools:ignore="AppLinkUrlError">
|
tools:ignore="AppLinkUrlError">
|
||||||
@@ -41,7 +37,6 @@
|
|||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".core.Receiver"
|
android:name=".core.Receiver"
|
||||||
android:directBootAware="true"
|
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
|
@@ -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
|
protected abstract val layoutRes: Int
|
||||||
|
|
||||||
private val navigation get() = activity?.navigation
|
private val navigation get() = activity?.navigation
|
||||||
|
open val snackbarView: View? get() = null
|
||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
@@ -36,9 +37,14 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
it.setVariable(BR.viewModel, viewModel)
|
it.setVariable(BR.viewModel, viewModel)
|
||||||
it.lifecycleOwner = viewLifecycleOwner
|
it.lifecycleOwner = viewLifecycleOwner
|
||||||
}
|
}
|
||||||
|
savedInstanceState?.let { viewModel.onRestoreState(it) }
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
viewModel.onSaveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
activity?.supportActionBar?.subtitle = null
|
activity?.supportActionBar?.subtitle = null
|
||||||
@@ -70,7 +76,10 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.requestRefresh()
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected open fun onPreBind(binding: Binding) {
|
protected open fun onPreBind(binding: Binding) {
|
||||||
@@ -78,7 +87,7 @@ abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun NavDirections.navigate() {
|
fun NavDirections.navigate() {
|
||||||
navigation?.navigate(this)
|
navigation?.currentDestination?.getAction(actionId)?.let { navigation!!.navigate(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,77 +3,30 @@ package com.topjohnwu.magisk.arch
|
|||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.CallSuper
|
import android.os.Bundle
|
||||||
import androidx.databinding.Bindable
|
|
||||||
import androidx.databinding.Observable
|
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
|
||||||
import com.topjohnwu.magisk.databinding.ObservableHost
|
import com.topjohnwu.magisk.databinding.ObservableHost
|
||||||
import com.topjohnwu.magisk.databinding.set
|
|
||||||
import com.topjohnwu.magisk.events.BackPressEvent
|
import com.topjohnwu.magisk.events.BackPressEvent
|
||||||
import com.topjohnwu.magisk.events.NavigationEvent
|
import com.topjohnwu.magisk.events.NavigationEvent
|
||||||
import com.topjohnwu.magisk.events.PermissionEvent
|
import com.topjohnwu.magisk.events.PermissionEvent
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
|
|
||||||
abstract class BaseViewModel(
|
abstract class BaseViewModel : ViewModel(), ObservableHost {
|
||||||
initialState: State = State.LOADING
|
|
||||||
) : ViewModel(), ObservableHost {
|
|
||||||
|
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
override var callbacks: PropertyChangeRegistry? = null
|
||||||
|
|
||||||
enum class State {
|
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||||
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
|
|
||||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||||
|
|
||||||
var state= initialState
|
open fun onSaveState(state: Bundle) {}
|
||||||
set(value) = set(value, field, { field = it }, BR.loading, BR.loaded, BR.loadFailed)
|
open fun onRestoreState(state: Bundle) {}
|
||||||
|
open fun onNetworkChanged(network: Boolean) {}
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
PermissionEvent(permission, callback).publish()
|
PermissionEvent(permission, callback).publish()
|
||||||
|
@@ -14,6 +14,7 @@ import com.google.android.material.snackbar.Snackbar
|
|||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
|
import com.topjohnwu.magisk.widget.Pre23CardViewBackgroundColorFixLayoutInflaterListener
|
||||||
import rikka.insets.WindowInsetsHelper
|
import rikka.insets.WindowInsetsHelper
|
||||||
import rikka.layoutinflater.view.LayoutInflaterFactory
|
import rikka.layoutinflater.view.LayoutInflaterFactory
|
||||||
|
|
||||||
@@ -26,17 +27,21 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
open val snackbarAnchorView: View? get() = null
|
open val snackbarAnchorView: View? get() = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val theme = Config.darkTheme
|
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||||
AppCompatDelegate.setDefaultNightMode(theme)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||||
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
|
||||||
|
.apply {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
this.addOnViewCreatedListener(Pre23CardViewBackgroundColorFixLayoutInflaterListener.getInstance())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
startObserveEvents()
|
startObserveLiveData()
|
||||||
|
|
||||||
// We need to set the window background explicitly since for whatever reason it's not
|
// We need to set the window background explicitly since for whatever reason it's not
|
||||||
// propagated upstream
|
// propagated upstream
|
||||||
@@ -84,7 +89,10 @@ abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModel
|
|||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.requestRefresh()
|
viewModel.let {
|
||||||
|
if (it is AsyncLoadViewModel)
|
||||||
|
it.startLoading()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
override fun onEventDispatched(event: ViewEvent) = when (event) {
|
||||||
|
@@ -1,15 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
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
|
val viewModel: BaseViewModel
|
||||||
|
|
||||||
fun startObserveEvents() {
|
fun startObserveLiveData() {
|
||||||
viewModel.viewEvents.observe(this) {
|
viewModel.viewEvents.observe(this, this::onEventDispatched)
|
||||||
onEventDispatched(it)
|
Info.isConnected.observe(this, viewModel::onNetworkChanged)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,3 +26,23 @@ interface ViewModelHolder : LifecycleOwner {
|
|||||||
*/
|
*/
|
||||||
fun onEventDispatched(event: ViewEvent) {}
|
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,20 +1,20 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.utils.*
|
import com.topjohnwu.magisk.core.utils.*
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.ipc.RootService
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
open class App() : Application() {
|
open class App() : Application() {
|
||||||
@@ -22,9 +22,9 @@ open class App() : Application() {
|
|||||||
constructor(o: Any) : this() {
|
constructor(o: Any) : this() {
|
||||||
val data = StubApk.Data(o)
|
val data = StubApk.Data(o)
|
||||||
// Add the root service name mapping
|
// 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
|
// Send back the actual root service class
|
||||||
data.rootService = RootRegistry::class.java
|
data.rootService = RootUtils::class.java
|
||||||
Info.stub = data
|
Info.stub = data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,43 +38,38 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(context: Context) {
|
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
|
// Get the actual ContextImpl
|
||||||
val app: Application
|
val app: Application
|
||||||
val base: Context
|
val base: Context
|
||||||
if (context is Application) {
|
if (context is Application) {
|
||||||
app = context
|
app = context
|
||||||
base = context.baseContext
|
base = context.baseContext
|
||||||
|
AppApkPath = StubApk.current(base).path
|
||||||
} else {
|
} else {
|
||||||
app = this
|
app = this
|
||||||
base = context
|
base = context
|
||||||
|
AppApkPath = base.packageResourcePath
|
||||||
}
|
}
|
||||||
super.attachBaseContext(base)
|
super.attachBaseContext(base)
|
||||||
ServiceLocator.context = 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()
|
refreshLocale()
|
||||||
AppApkPath = if (isRunningAsStub) {
|
resources.patch()
|
||||||
StubApk.current(base).path
|
|
||||||
} else {
|
|
||||||
base.packageResourcePath
|
|
||||||
}
|
|
||||||
|
|
||||||
base.resources.patch()
|
|
||||||
app.registerActivityLifecycleCallbacks(ActivityTracker)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
RootRegistry.bindTask = RootService.bindOrTask(
|
|
||||||
intent<RootRegistry>(),
|
|
||||||
UiThreadHandler.executor,
|
|
||||||
RootRegistry.Connection
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
@@ -86,20 +81,21 @@ open class App() : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
object ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||||
|
|
||||||
|
val foreground: Activity? get() = ref.get()
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var foreground: Activity? = null
|
private var ref = WeakReference<Activity>(null)
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
if (activity is SuRequestActivity) return
|
if (activity is SuRequestActivity) return
|
||||||
foreground = activity
|
ref = WeakReference(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityPaused(activity: Activity) {
|
override fun onActivityPaused(activity: Activity) {
|
||||||
if (activity is SuRequestActivity) return
|
if (activity is SuRequestActivity) return
|
||||||
foreground = null
|
ref.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||||
|
@@ -6,21 +6,23 @@ import android.util.Xml
|
|||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
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.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 com.topjohnwu.magisk.ui.theme.Theme
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
object Config : PreferenceModel, DBConfig {
|
object Config : PreferenceConfig, DBConfig {
|
||||||
|
|
||||||
override val stringDB get() = ServiceLocator.stringDB
|
override val stringDB get() = ServiceLocator.stringDB
|
||||||
override val settingsDB get() = ServiceLocator.settingsDB
|
override val settingsDB get() = ServiceLocator.settingsDB
|
||||||
override val context get() = ServiceLocator.deContext
|
override val context get() = ServiceLocator.deContext
|
||||||
|
override val coroutineScope get() = GlobalScope
|
||||||
|
|
||||||
@get:SuppressLint("ApplySharedPref")
|
@get:SuppressLint("ApplySharedPref")
|
||||||
val prefsFile: File get() {
|
val prefsFile: File get() {
|
||||||
@@ -70,6 +72,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val BETA_CHANNEL = 1
|
const val BETA_CHANNEL = 1
|
||||||
const val CUSTOM_CHANNEL = 2
|
const val CUSTOM_CHANNEL = 2
|
||||||
const val CANARY_CHANNEL = 3
|
const val CANARY_CHANNEL = 3
|
||||||
|
const val DEBUG_CHANNEL = 4
|
||||||
|
|
||||||
// root access mode
|
// root access mode
|
||||||
const val ROOT_ACCESS_DISABLED = 0
|
const val ROOT_ACCESS_DISABLED = 0
|
||||||
@@ -106,6 +109,8 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
|
|
||||||
private val defaultChannel =
|
private val defaultChannel =
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
|
Value.DEBUG_CHANNEL
|
||||||
|
else if (Const.APP_IS_CANARY)
|
||||||
Value.CANARY_CHANNEL
|
Value.CANARY_CHANNEL
|
||||||
else
|
else
|
||||||
Value.DEFAULT_CHANNEL
|
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 suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||||
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
|
||||||
var zygisk by dbSettings(Key.ZYGISK, 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 suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
|
||||||
@@ -158,7 +163,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
fun load(pkg: String?) {
|
fun load(pkg: String?) {
|
||||||
// Only try to load prefs when fresh install and a previous package name is set
|
// Only try to load prefs when fresh install and a previous package name is set
|
||||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
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) }
|
prefs.edit { parsePrefs(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,10 +174,11 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
suBiometric = true
|
suBiometric = true
|
||||||
remove(SU_FINGERPRINT)
|
remove(SU_FINGERPRINT)
|
||||||
prefs.getString(Key.UPDATE_CHANNEL, null).also {
|
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())
|
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)
|
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
|
||||||
|
|
||||||
object Version {
|
object Version {
|
||||||
const val MIN_VERSION = "v21.0"
|
const val MIN_VERSION = "v22.0"
|
||||||
const val MIN_VERCODE = 21000
|
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_24_0() = Info.env.versionCode >= 24000 || isCanary()
|
||||||
|
fun atLeast_25_0() = Info.env.versionCode >= 25000 || isCanary()
|
||||||
fun isCanary() = isCanary(Info.env.versionCode)
|
fun isCanary() = isCanary(Info.env.versionCode)
|
||||||
|
|
||||||
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
@@ -13,45 +12,49 @@ import android.content.res.Resources
|
|||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.utils.syncLocale
|
import com.topjohnwu.magisk.core.utils.syncLocale
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.ktx.unwrap
|
||||||
|
|
||||||
lateinit var AppApkPath: String
|
lateinit var AppApkPath: String
|
||||||
|
|
||||||
fun AssetManager.addAssetPath(path: String) = StubApk.addAssetPath(this, path)
|
fun Resources.addAssetPath(path: String) = StubApk.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.patch(): Resources {
|
fun Resources.patch(): Resources {
|
||||||
syncLocale()
|
|
||||||
if (isRunningAsStub)
|
if (isRunningAsStub)
|
||||||
assets.addAssetPath(AppApkPath)
|
addAssetPath(AppApkPath)
|
||||||
|
syncLocale()
|
||||||
return this
|
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 {
|
fun createNewResources(): Resources {
|
||||||
val asset = AssetManager::class.java.newInstance()
|
val asset = AssetManager::class.java.newInstance()
|
||||||
asset.addAssetPath(AppApkPath)
|
|
||||||
val config = Configuration(AppContext.resources.configuration)
|
val config = Configuration(AppContext.resources.configuration)
|
||||||
val metrics = DisplayMetrics()
|
val metrics = DisplayMetrics()
|
||||||
metrics.setTo(AppContext.resources.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) =
|
fun Class<*>.cmp(pkg: String) =
|
||||||
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
|
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))
|
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
|
||||||
|
|
||||||
// Keep a reference to these resources to prevent it from
|
// Keep a reference to these resources to prevent it from
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.databinding.ObservableBoolean
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
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.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.magisk.ktx.getProperty
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
import com.topjohnwu.superuser.ShellUtils.fastCmd
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
|
||||||
|
|
||||||
val isRunningAsStub get() = Info.stub != null
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
|
||||||
@@ -36,6 +35,7 @@ object Info {
|
|||||||
@JvmField var vbmeta = false
|
@JvmField var vbmeta = false
|
||||||
var crypto = ""
|
var crypto = ""
|
||||||
var noDataExec = false
|
var noDataExec = false
|
||||||
|
var isRooted = false
|
||||||
|
|
||||||
@JvmField var hasGMS = true
|
@JvmField var hasGMS = true
|
||||||
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
|
||||||
@@ -43,26 +43,31 @@ object Info {
|
|||||||
getProperty("ro.kernel.qemu", "0") == "1" ||
|
getProperty("ro.kernel.qemu", "0") == "1" ||
|
||||||
getProperty("ro.boot.qemu", "0") == "1"
|
getProperty("ro.boot.qemu", "0") == "1"
|
||||||
|
|
||||||
val isConnected by lazy {
|
val isConnected: LiveData<Boolean> by lazy {
|
||||||
ObservableBoolean(false).also { field ->
|
MutableLiveData(false).also { field ->
|
||||||
NetworkObserver.observe(AppContext) {
|
NetworkObserver.observe(AppContext) {
|
||||||
UiThreadHandler.run { field.set(it) }
|
remote = EMPTY_REMOTE
|
||||||
|
field.postValue(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadState() = Env(
|
private fun loadState(): Env {
|
||||||
fastCmd("magisk -v").split(":".toRegex())[0],
|
val v = fastCmd("magisk -v").split(":".toRegex())
|
||||||
|
return Env(
|
||||||
|
v[0], v.size >= 3 && v[2] == "D",
|
||||||
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class Env(
|
class Env(
|
||||||
val versionString: String = "",
|
val versionString: String = "",
|
||||||
|
val isDebug: Boolean = false,
|
||||||
code: Int = -1
|
code: Int = -1
|
||||||
) {
|
) {
|
||||||
val versionCode = when {
|
val versionCode = when {
|
||||||
code < Const.Version.MIN_VERCODE -> -1
|
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 isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
|
||||||
val isActive = versionCode >= 0
|
val isActive = versionCode >= 0
|
||||||
|
@@ -7,7 +7,7 @@ import android.content.Context
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.base.BaseJobService
|
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 com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@@ -1,22 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.core
|
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.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
|
import com.topjohnwu.magisk.core.base.BaseProvider
|
||||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class Provider : ContentProvider() {
|
class Provider : BaseProvider() {
|
||||||
|
|
||||||
override fun attachInfo(context: Context, info: ProviderInfo) {
|
|
||||||
super.attachInfo(context.wrap(), info)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||||
SuCallbackHandler.run(context!!, method, extras)
|
SuCallbackHandler.run(context!!, method, extras)
|
||||||
@@ -25,24 +16,13 @@ class Provider : ContentProvider() {
|
|||||||
|
|
||||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
return when (uri.encodedPath ?: return null) {
|
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)
|
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||||
else -> super.openFile(uri, mode)
|
else -> super.openFile(uri, mode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun APK_URI(pkg: String) =
|
fun preferencesUri(pkg: String): Uri =
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
|
||||||
|
|
||||||
fun PREFS_URI(pkg: String) =
|
|
||||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
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,10 @@
|
|||||||
package com.topjohnwu.magisk.core
|
package com.topjohnwu.magisk.core
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ContextWrapper
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.base.BaseReceiver
|
import com.topjohnwu.magisk.core.base.BaseReceiver
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@@ -26,8 +26,9 @@ open class Receiver : BaseReceiver() {
|
|||||||
return if (uid == -1) null else uid
|
return if (uid == -1) null else uid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
intent ?: return
|
intent ?: return
|
||||||
|
super.onReceive(context, intent)
|
||||||
|
|
||||||
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
fun rmPolicy(uid: Int) = GlobalScope.launch {
|
||||||
policyDB.delete(uid)
|
policyDB.delete(uid)
|
||||||
|
@@ -2,24 +2,31 @@ package com.topjohnwu.magisk.core.base
|
|||||||
|
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
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.GetContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.utils.RequestInstall
|
import com.topjohnwu.magisk.core.utils.RequestInstall
|
||||||
import com.topjohnwu.magisk.core.utils.UninstallPackage
|
|
||||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.wrap
|
||||||
import com.topjohnwu.magisk.ktx.reflectField
|
import com.topjohnwu.magisk.ktx.reflectField
|
||||||
import java.util.concurrent.CountDownLatch
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
interface ContentResultCallback: ActivityResultCallback<Uri>, Parcelable {
|
||||||
|
fun onActivityLaunch() {}
|
||||||
|
// Make the result type explicitly non-null
|
||||||
|
override fun onActivityResult(result: Uri)
|
||||||
|
}
|
||||||
|
|
||||||
abstract class BaseActivity : AppCompatActivity() {
|
abstract class BaseActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -33,21 +40,22 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
permissionCallback = null
|
permissionCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contentCallback: ((Uri) -> Unit)? = null
|
private var contentCallback: ContentResultCallback? = null
|
||||||
private val getContent = registerForActivityResult(GetContent()) {
|
private val getContent = registerForActivityResult(GetContent()) {
|
||||||
if (it != null) contentCallback?.invoke(it)
|
if (it != null) contentCallback?.onActivityResult(it)
|
||||||
contentCallback = null
|
contentCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var uninstallLatch = CountDownLatch(1)
|
private val mReferrerField by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
private val uninstallPkg = registerForActivityResult(UninstallPackage()) {
|
Activity::class.java.reflectField("mReferrer")
|
||||||
uninstallLatch.countDown()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
val realCallingPackage: String? get() {
|
||||||
// Force applying our preferred local
|
callingPackage?.let { return it }
|
||||||
config?.setLocale(currentLocale)
|
if (Build.VERSION.SDK_INT >= 22) {
|
||||||
super.applyOverrideConfiguration(config)
|
mReferrerField.get(this)?.let { return it as String }
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
@@ -62,9 +70,17 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
|
||||||
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
|
||||||
}
|
}
|
||||||
|
contentCallback = savedInstanceState?.getParcelable(CONTENT_CALLBACK_KEY)
|
||||||
super.onCreate(savedInstanceState)
|
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) {
|
fun withPermission(permission: String, callback: (Boolean) -> Unit) {
|
||||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
||||||
// We do not need external rw on 30+
|
// We do not need external rw on 30+
|
||||||
@@ -79,16 +95,14 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContent(type: String, callback: (Uri) -> Unit) {
|
fun getContent(type: String, callback: ContentResultCallback) {
|
||||||
contentCallback = callback
|
contentCallback = callback
|
||||||
|
try {
|
||||||
getContent.launch(type)
|
getContent.launch(type)
|
||||||
|
callback.onActivityLaunch()
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
fun uninstallAndWait(pkg: String) {
|
|
||||||
uninstallLatch = CountDownLatch(1)
|
|
||||||
uninstallPkg.launch(pkg)
|
|
||||||
uninstallLatch.await(3, TimeUnit.SECONDS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun recreate() {
|
override fun recreate() {
|
||||||
@@ -100,4 +114,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
|||||||
startActivity(Intent(intent).setFlags(0))
|
startActivity(Intent(intent).setFlags(0))
|
||||||
finish()
|
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.app.job.JobService
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseJobService : JobService() {
|
abstract class BaseJobService : JobService() {
|
||||||
override fun attachBaseContext(base: Context) {
|
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.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.core.wrap
|
import androidx.annotation.CallSuper
|
||||||
|
import com.topjohnwu.magisk.core.patch
|
||||||
|
|
||||||
abstract class BaseReceiver : BroadcastReceiver() {
|
abstract class BaseReceiver : BroadcastReceiver() {
|
||||||
|
@CallSuper
|
||||||
final override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
onReceive(context.wrap() as ContextWrapper, 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.app.Service
|
||||||
import android.content.Context
|
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) {
|
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.BranchInfo
|
||||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
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 androidx.room.*
|
||||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
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 android.content.Context
|
||||||
import com.squareup.moshi.Moshi
|
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.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.ViewModelStoreOwner
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.magiskdb.PolicyDao
|
import com.topjohnwu.magisk.core.data.SuLogDatabase
|
||||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.data.database.SuLogDatabase
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.ktx.deviceProtectedContext
|
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.Markwon
|
||||||
import io.noties.markwon.utils.NoCopySpannableFactory
|
import io.noties.markwon.utils.NoCopySpannableFactory
|
||||||
|
|
||||||
@@ -50,27 +42,8 @@ object ServiceLocator {
|
|||||||
createApiService(retrofit, Const.Url.GITHUB_API_URL)
|
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) =
|
private fun createSuLogDatabase(context: Context) =
|
||||||
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
@@ -11,7 +11,10 @@ import androidx.core.net.toFile
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.*
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.intent
|
||||||
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
|
@@ -2,12 +2,11 @@ package com.topjohnwu.magisk.core.download
|
|||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.base.BaseService
|
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.core.utils.ProgressInputStream
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ktx.synchronized
|
import com.topjohnwu.magisk.ktx.synchronized
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
@@ -20,8 +19,6 @@ open class NotificationService : BaseService() {
|
|||||||
|
|
||||||
protected val service get() = ServiceLocator.networkService
|
protected val service get() = ServiceLocator.networkService
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
notifications.forEach { Notifications.mgr.cancel(it.key) }
|
||||||
|
@@ -8,11 +8,11 @@ import android.net.Uri
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.StubJson
|
import com.topjohnwu.magisk.core.model.StubJson
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
import com.topjohnwu.magisk.ui.flash.FlashFragment
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
|
@@ -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.cmd(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.cmd(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.squareup.moshi.JsonDataException
|
||||||
import com.topjohnwu.magisk.core.Const
|
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.Shell
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -26,13 +26,12 @@ data class LocalModule(
|
|||||||
var outdated = false
|
var outdated = false
|
||||||
|
|
||||||
private var updateUrl: String = ""
|
private var updateUrl: String = ""
|
||||||
private val removeFile = SuFile(path, "remove")
|
private val removeFile = RootUtils.fs.getFile(path, "remove")
|
||||||
private val disableFile = SuFile(path, "disable")
|
private val disableFile = RootUtils.fs.getFile(path, "disable")
|
||||||
private val updateFile = SuFile(path, "update")
|
private val updateFile = RootUtils.fs.getFile(path, "update")
|
||||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
private val riruFolder = RootUtils.fs.getFile(path, "riru")
|
||||||
private val riruFolder = SuFile(path, "riru")
|
private val zygiskFolder = RootUtils.fs.getFile(path, "zygisk")
|
||||||
private val zygiskFolder = SuFile(path, "zygisk")
|
private val unloaded = RootUtils.fs.getFile(zygiskFolder, "unloaded")
|
||||||
private val unloaded = SuFile(zygiskFolder, "unloaded")
|
|
||||||
|
|
||||||
val updated: Boolean get() = updateFile.exists()
|
val updated: Boolean get() = updateFile.exists()
|
||||||
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
|
||||||
@@ -42,19 +41,12 @@ data class LocalModule(
|
|||||||
var enable: Boolean
|
var enable: Boolean
|
||||||
get() = !disableFile.exists()
|
get() = !disableFile.exists()
|
||||||
set(enable) {
|
set(enable) {
|
||||||
val dir = "$PERSIST/$id"
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
disableFile.delete()
|
disableFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_sepolicy_rules").submit()
|
||||||
else
|
|
||||||
Shell.cmd("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
|
||||||
} else {
|
} else {
|
||||||
!disableFile.createNewFile()
|
!disableFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_sepolicy_rules").submit()
|
||||||
else
|
|
||||||
Shell.cmd("rm -rf $dir").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,16 +56,10 @@ data class LocalModule(
|
|||||||
if (remove) {
|
if (remove) {
|
||||||
if (updateFile.exists()) return
|
if (updateFile.exists()) return
|
||||||
removeFile.createNewFile()
|
removeFile.createNewFile()
|
||||||
if (Const.Version.atLeast_21_2())
|
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_sepolicy_rules").submit()
|
||||||
else
|
|
||||||
Shell.cmd("rm -rf $PERSIST/$id").submit()
|
|
||||||
} else {
|
} else {
|
||||||
removeFile.delete()
|
removeFile.delete()
|
||||||
if (Const.Version.atLeast_21_2())
|
|
||||||
Shell.cmd("copy_sepolicy_rules").submit()
|
Shell.cmd("copy_sepolicy_rules").submit()
|
||||||
else
|
|
||||||
Shell.cmd("cp -af $ruleFile $PERSIST/$id").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +124,10 @@ data class LocalModule(
|
|||||||
|
|
||||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
||||||
|
|
||||||
|
fun loaded() = RootUtils.fs.getFile(Const.MAGISK_PATH).exists()
|
||||||
|
|
||||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||||
SuFile(Const.MAGISK_PATH)
|
RootUtils.fs.getFile(Const.MAGISK_PATH)
|
||||||
.listFiles()
|
.listFiles()
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.filter { !it.isFile }
|
.filter { !it.isFile }
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.core.model.su
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.topjohnwu.magisk.ktx.now
|
import com.topjohnwu.magisk.ktx.getLabel
|
||||||
|
|
||||||
@Entity(tableName = "logs")
|
@Entity(tableName = "logs")
|
||||||
data class SuLog(
|
class SuLog(
|
||||||
val fromUid: Int,
|
val fromUid: Int,
|
||||||
val toUid: Int,
|
val toUid: Int,
|
||||||
val fromPid: Int,
|
val fromPid: Int,
|
||||||
@@ -13,7 +15,44 @@ data class SuLog(
|
|||||||
val appName: String,
|
val appName: String,
|
||||||
val command: String,
|
val command: String,
|
||||||
val action: Boolean,
|
val action: Boolean,
|
||||||
val time: Long = now
|
val time: Long = System.currentTimeMillis()
|
||||||
) {
|
) {
|
||||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
@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
|
package com.topjohnwu.magisk.core.model.su
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
class SuPolicy(val uid: Int) {
|
||||||
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
|
|
||||||
) {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val INTERACTIVE = 0
|
const val INTERACTIVE = 0
|
||||||
const val DENY = 1
|
const val DENY = 1
|
||||||
const val ALLOW = 2
|
const val ALLOW = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
|
var policy: Int = INTERACTIVE
|
||||||
uid, toUid, fromPid, packageName, appName,
|
var until: Long = -1L
|
||||||
command, policy == ALLOW)
|
var logging: Boolean = true
|
||||||
|
var notification: Boolean = true
|
||||||
|
|
||||||
fun toMap() = mapOf(
|
fun toMap(): MutableMap<String, Any> = mutableMapOf(
|
||||||
"uid" to uid,
|
"uid" to uid,
|
||||||
"package_name" to packageName,
|
|
||||||
"policy" to policy,
|
"policy" to policy,
|
||||||
"until" to until,
|
"until" to until,
|
||||||
"logging" to logging,
|
"logging" to logging,
|
||||||
"notification" to notification
|
"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.data.magiskdb.SettingsDao
|
||||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
import com.topjohnwu.magisk.core.data.magiskdb.StringDao
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
@@ -11,26 +11,27 @@ import kotlin.reflect.KProperty
|
|||||||
interface DBConfig {
|
interface DBConfig {
|
||||||
val settingsDB: SettingsDao
|
val settingsDB: SettingsDao
|
||||||
val stringDB: StringDao
|
val stringDB: StringDao
|
||||||
|
val coroutineScope: CoroutineScope
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Int
|
default: Int
|
||||||
) = DBSettingsValue(name, default)
|
) = IntDBProperty(name, default)
|
||||||
|
|
||||||
fun dbSettings(
|
fun dbSettings(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) = DBBoolSettings(name, default)
|
) = BoolDBProperty(name, default)
|
||||||
|
|
||||||
fun dbStrings(
|
fun dbStrings(
|
||||||
name: String,
|
name: String,
|
||||||
default: String,
|
default: String,
|
||||||
sync: Boolean = false
|
sync: Boolean = false
|
||||||
) = DBStringsValue(name, default, sync)
|
) = StringDBProperty(name, default, sync)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBSettingsValue(
|
class IntDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int
|
private val default: Int
|
||||||
) : ReadWriteProperty<DBConfig, Int> {
|
) : ReadWriteProperty<DBConfig, Int> {
|
||||||
@@ -48,18 +49,18 @@ class DBSettingsValue(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
this.value = value
|
this.value = value
|
||||||
}
|
}
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.settingsDB.put(name, value)
|
thisRef.settingsDB.put(name, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open class DBBoolSettings(
|
open class BoolDBProperty(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : ReadWriteProperty<DBConfig, 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 =
|
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
|
||||||
base.getValue(thisRef, property) != 0
|
base.getValue(thisRef, property) != 0
|
||||||
@@ -68,10 +69,10 @@ open class DBBoolSettings(
|
|||||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBBoolSettingsNoWrite(
|
class BoolDBPropertyNoWrite(
|
||||||
name: String,
|
name: String,
|
||||||
default: Boolean
|
default: Boolean
|
||||||
) : DBBoolSettings(name, default) {
|
) : BoolDBProperty(name, default) {
|
||||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
|
||||||
synchronized(base) {
|
synchronized(base) {
|
||||||
base.value = if (value) 1 else 0
|
base.value = if (value) 1 else 0
|
||||||
@@ -79,7 +80,7 @@ class DBBoolSettingsNoWrite(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DBStringsValue(
|
class StringDBProperty(
|
||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val sync: Boolean
|
private val sync: Boolean
|
||||||
@@ -106,7 +107,7 @@ class DBStringsValue(
|
|||||||
thisRef.stringDB.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDB.delete(name)
|
thisRef.stringDB.delete(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ class DBStringsValue(
|
|||||||
thisRef.stringDB.put(name, value)
|
thisRef.stringDB.put(name, value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch {
|
thisRef.coroutineScope.launch {
|
||||||
thisRef.stringDB.put(name, value)
|
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.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
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.core.model.su.SuLog
|
||||||
import com.topjohnwu.magisk.data.database.SuLogDao
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
import com.topjohnwu.magisk.ktx.await
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
@@ -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
|
||||||
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_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.DEFAULT_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
import com.topjohnwu.magisk.core.data.GithubApiServices
|
||||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
import com.topjohnwu.magisk.core.data.GithubPageServices
|
||||||
import com.topjohnwu.magisk.data.network.RawServices
|
import com.topjohnwu.magisk.core.data.RawServices
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -24,12 +25,12 @@ class NetworkService(
|
|||||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||||
BETA_CHANNEL -> fetchBetaUpdate()
|
BETA_CHANNEL -> fetchBetaUpdate()
|
||||||
CANARY_CHANNEL -> fetchCanaryUpdate()
|
CANARY_CHANNEL -> fetchCanaryUpdate()
|
||||||
|
DEBUG_CHANNEL -> fetchDebugUpdate()
|
||||||
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
if (info.magisk.versionCode < Info.env.versionCode &&
|
if (info.magisk.versionCode < Info.env.versionCode &&
|
||||||
Config.updateChannel == DEFAULT_CHANNEL
|
Config.updateChannel == DEFAULT_CHANNEL) {
|
||||||
) {
|
|
||||||
Config.updateChannel = BETA_CHANNEL
|
Config.updateChannel = BETA_CHANNEL
|
||||||
info = fetchBetaUpdate()
|
info = fetchBetaUpdate()
|
||||||
}
|
}
|
||||||
@@ -40,11 +41,15 @@ class NetworkService(
|
|||||||
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
private suspend fun fetchStableUpdate() = pages.fetchUpdateJSON("stable.json")
|
||||||
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
private suspend fun fetchBetaUpdate() = pages.fetchUpdateJSON("beta.json")
|
||||||
private suspend fun fetchCanaryUpdate() = pages.fetchUpdateJSON("canary.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 suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
||||||
|
|
||||||
private inline fun <T> safe(factory: () -> T): T? {
|
private inline fun <T> safe(factory: () -> T): T? {
|
||||||
return try {
|
return try {
|
||||||
|
if (Info.isConnected.value == true)
|
||||||
factory()
|
factory()
|
||||||
|
else
|
||||||
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
null
|
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 android.content.SharedPreferences
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import kotlin.properties.ReadWriteProperty
|
import kotlin.properties.ReadWriteProperty
|
||||||
import kotlin.reflect.KProperty
|
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: Boolean) = putBoolean(name, value)
|
||||||
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(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 name: String,
|
||||||
private val default: Boolean,
|
private val default: Boolean,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Boolean> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -38,7 +100,7 @@ class BooleanProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Boolean
|
value: Boolean
|
||||||
) {
|
) {
|
||||||
@@ -51,10 +113,10 @@ class FloatProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Float,
|
private val default: Float,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Float> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Float {
|
): Float {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -62,7 +124,7 @@ class FloatProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Float
|
value: Float
|
||||||
) {
|
) {
|
||||||
@@ -75,10 +137,10 @@ class IntProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Int,
|
private val default: Int,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Int> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Int {
|
): Int {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -86,7 +148,7 @@ class IntProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Int
|
value: Int
|
||||||
) {
|
) {
|
||||||
@@ -99,10 +161,10 @@ class LongProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Long,
|
private val default: Long,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Long> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Long {
|
): Long {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -110,7 +172,7 @@ class LongProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Long
|
value: Long
|
||||||
) {
|
) {
|
||||||
@@ -123,10 +185,10 @@ class StringProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: String,
|
private val default: String,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, String> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, String> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): String {
|
): String {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -134,7 +196,7 @@ class StringProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: String
|
value: String
|
||||||
) {
|
) {
|
||||||
@@ -147,10 +209,10 @@ class StringSetProperty(
|
|||||||
private val name: String,
|
private val name: String,
|
||||||
private val default: Set<String>,
|
private val default: Set<String>,
|
||||||
private val commit: Boolean
|
private val commit: Boolean
|
||||||
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
|
) : PreferenceProperty(), ReadWriteProperty<PreferenceConfig, Set<String>> {
|
||||||
|
|
||||||
override operator fun getValue(
|
override operator fun getValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>
|
property: KProperty<*>
|
||||||
): Set<String> {
|
): Set<String> {
|
||||||
val prefName = name.ifBlank { property.name }
|
val prefName = name.ifBlank { property.name }
|
||||||
@@ -158,7 +220,7 @@ class StringSetProperty(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override operator fun setValue(
|
override operator fun setValue(
|
||||||
thisRef: PreferenceModel,
|
thisRef: PreferenceConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
value: Set<String>
|
value: Set<String>
|
||||||
) {
|
) {
|
@@ -6,13 +6,13 @@ import android.widget.Toast
|
|||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
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.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
import com.topjohnwu.magisk.core.model.su.createSuLog
|
||||||
import com.topjohnwu.magisk.core.model.su.toUidPolicy
|
import com.topjohnwu.magisk.ktx.getLabel
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.ktx.getPackageInfo
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
object SuCallbackHandler {
|
object SuCallbackHandler {
|
||||||
@@ -53,59 +53,47 @@ object SuCallbackHandler {
|
|||||||
private fun handleLogging(context: Context, data: Bundle) {
|
private fun handleLogging(context: Context, data: Bundle) {
|
||||||
val fromUid = data.getIntComp("from.uid", -1)
|
val fromUid = data.getIntComp("from.uid", -1)
|
||||||
val notify = data.getBoolean("notify", true)
|
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 pm = context.packageManager
|
||||||
|
|
||||||
val policy = runCatching {
|
val log = runCatching {
|
||||||
fromUid.toPolicy(pm, allow)
|
pm.getPackageInfo(fromUid, pid)?.let {
|
||||||
}.getOrElse {
|
pm.createSuLog(it, toUid, pid, command, policy)
|
||||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
|
||||||
fromUid.toUidPolicy(pm, allow)
|
|
||||||
}
|
}
|
||||||
|
}.getOrNull() ?: createSuLog(fromUid, toUid, pid, command, policy)
|
||||||
|
|
||||||
if (notify)
|
if (notify)
|
||||||
notify(context, policy)
|
notify(context, log.action, log.appName)
|
||||||
|
|
||||||
val toUid = data.getIntComp("to.uid", -1)
|
runBlocking { ServiceLocator.logRepo.insert(log) }
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNotify(context: Context, data: Bundle) {
|
private fun handleNotify(context: Context, data: Bundle) {
|
||||||
val fromUid = data.getIntComp("from.uid", -1)
|
val uid = data.getIntComp("from.uid", -1)
|
||||||
val allow = data.getIntComp("policy", SuPolicy.ALLOW)
|
val pid = data.getIntComp("pid", -1)
|
||||||
|
val policy = data.getIntComp("policy", SuPolicy.ALLOW)
|
||||||
|
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
|
|
||||||
|
val appName = runCatching {
|
||||||
|
pm.getPackageInfo(uid, pid)?.applicationInfo?.getLabel(pm)
|
||||||
|
}.getOrNull() ?: "[UID] $uid"
|
||||||
|
|
||||||
val policy = runCatching {
|
notify(context, policy == SuPolicy.ALLOW, appName)
|
||||||
fromUid.toPolicy(pm, allow)
|
|
||||||
}.getOrElse {
|
|
||||||
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
|
|
||||||
fromUid.toUidPolicy(pm, allow)
|
|
||||||
}
|
|
||||||
notify(context, policy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notify(context: Context, policy: SuPolicy) {
|
private fun notify(context: Context, granted: Boolean, appName: String) {
|
||||||
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
|
||||||
val resId = if (policy.policy == SuPolicy.ALLOW)
|
val resId = if (granted)
|
||||||
R.string.su_allow_toast
|
R.string.su_allow_toast
|
||||||
else
|
else
|
||||||
R.string.su_deny_toast
|
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
|
package com.topjohnwu.magisk.core.su
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.core.Config
|
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.SuPolicy
|
||||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
import com.topjohnwu.magisk.ktx.getPackageInfo
|
||||||
import com.topjohnwu.magisk.ktx.now
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SuRequestHandler(
|
class SuRequestHandler(
|
||||||
private val pm: PackageManager,
|
val pm: PackageManager,
|
||||||
private val policyDB: PolicyDao
|
private val policyDB: PolicyDao
|
||||||
) : Closeable {
|
) {
|
||||||
|
|
||||||
private lateinit var output: DataOutputStream
|
private lateinit var output: DataOutputStream
|
||||||
lateinit var policy: SuPolicy
|
private lateinit var policy: SuPolicy
|
||||||
|
lateinit var pkgInfo: PackageInfo
|
||||||
private set
|
private set
|
||||||
|
|
||||||
// Return true to indicate undetermined policy, require user interaction
|
// Return true to indicate undetermined policy, require user interaction
|
||||||
@@ -35,8 +33,8 @@ class SuRequestHandler(
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
// Never allow com.topjohnwu.magisk (could be malware)
|
// Never allow com.topjohnwu.magisk (could be malware)
|
||||||
if (policy.packageName == BuildConfig.APPLICATION_ID) {
|
if (pkgInfo.packageName == BuildConfig.APPLICATION_ID) {
|
||||||
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
Shell.cmd("(pm uninstall ${BuildConfig.APPLICATION_ID} >/dev/null 2>&1)&").exec()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,50 +52,57 @@ class SuRequestHandler(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
private fun close() {
|
||||||
override fun close() {
|
|
||||||
if (::output.isInitialized)
|
if (::output.isInitialized)
|
||||||
output.close()
|
runCatching { output.close() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SuRequestError : IOException()
|
|
||||||
|
|
||||||
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
|
val fifo = intent.getStringExtra("fifo") ?: throw IOException("fifo == null")
|
||||||
val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
|
output = DataOutputStream(FileOutputStream(fifo))
|
||||||
output = DataOutputStream(FileOutputStream(name).buffered())
|
val uid = intent.getIntExtra("uid", -1)
|
||||||
policy = uid.toPolicy(pm)
|
if (uid <= 0) {
|
||||||
true
|
throw IOException("uid == $uid")
|
||||||
} catch (e: Exception) {
|
}
|
||||||
when (e) {
|
policy = SuPolicy(uid)
|
||||||
is IOException, is PackageManager.NameNotFoundException -> {
|
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)
|
Timber.e(e)
|
||||||
runCatching { close() }
|
close()
|
||||||
false
|
return@withContext false
|
||||||
}
|
|
||||||
else -> throw e // Unexpected error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun respond(action: Int, time: Int) {
|
suspend fun respond(action: Int, time: Int) {
|
||||||
val until = if (time > 0)
|
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
|
else
|
||||||
time.toLong()
|
time.toLong()
|
||||||
|
|
||||||
policy.policy = action
|
policy.policy = action
|
||||||
policy.until = until
|
policy.until = until
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
output.writeInt(policy.policy)
|
output.writeInt(policy.policy)
|
||||||
output.flush()
|
output.flush()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
} finally {
|
} finally {
|
||||||
runCatching { close() }
|
close()
|
||||||
if (until >= 0)
|
if (until >= 0)
|
||||||
policyDB.update(policy)
|
policyDB.update(policy)
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,10 @@ package com.topjohnwu.magisk.core.tasks
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.Const
|
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.displayName
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@@ -11,9 +11,9 @@ import com.topjohnwu.magisk.core.Config
|
|||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.Provider
|
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.AXML
|
||||||
import com.topjohnwu.magisk.core.utils.Keygen
|
import com.topjohnwu.magisk.core.utils.Keygen
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.ktx.await
|
import com.topjohnwu.magisk.ktx.await
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
import com.topjohnwu.magisk.signing.JarMap
|
import com.topjohnwu.magisk.signing.JarMap
|
||||||
@@ -82,7 +82,7 @@ object HideAPK {
|
|||||||
|
|
||||||
// Write apk changes
|
// Write apk changes
|
||||||
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
jar.getOutputStream(je).use { it.write(xml.bytes) }
|
||||||
val keys = Keygen(context)
|
val keys = Keygen()
|
||||||
SignApk.sign(keys.cert, keys.key, jar, out)
|
SignApk.sign(keys.cert, keys.key, jar, out)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -97,13 +97,13 @@ object HideAPK {
|
|||||||
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
|
||||||
val self = activity.packageName
|
val self = activity.packageName
|
||||||
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
|
activity.grantUriPermission(pkg, Provider.preferencesUri(self), flag)
|
||||||
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
|
|
||||||
intent.putExtra(Const.Key.PREV_PKG, self)
|
intent.putExtra(Const.Key.PREV_PKG, self)
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
activity.finish()
|
activity.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
private suspend fun patchAndHide(activity: Activity, label: String, onFailure: Runnable): Boolean {
|
||||||
val stub = File(activity.cacheDir, "stub.apk")
|
val stub = File(activity.cacheDir, "stub.apk")
|
||||||
try {
|
try {
|
||||||
@@ -130,7 +130,7 @@ object HideAPK {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
|
||||||
if (Shell.su(cmd).exec().isSuccess) return true
|
if (Shell.cmd(cmd).exec().isSuccess) return true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session.install(activity, repack)
|
session.install(activity, repack)
|
||||||
@@ -178,7 +178,7 @@ object HideAPK {
|
|||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
|
||||||
if (Shell.su(cmd).await().isSuccess) return
|
if (Shell.cmd(cmd).await().isSuccess) return
|
||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
session.install(activity, apk)
|
session.install(activity, apk)
|
||||||
|
@@ -9,10 +9,11 @@ import com.topjohnwu.magisk.BuildConfig
|
|||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.StubApk
|
import com.topjohnwu.magisk.StubApk
|
||||||
import com.topjohnwu.magisk.core.*
|
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
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
import com.topjohnwu.magisk.ktx.reboot
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
import com.topjohnwu.magisk.ktx.withStreams
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
@@ -22,9 +23,8 @@ import com.topjohnwu.superuser.Shell
|
|||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import com.topjohnwu.superuser.internal.NOPList
|
import com.topjohnwu.superuser.internal.NOPList
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
import com.topjohnwu.superuser.nio.ExtendedFile
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.jpountz.lz4.LZ4FrameInputStream
|
import net.jpountz.lz4.LZ4FrameInputStream
|
||||||
@@ -37,6 +37,7 @@ import java.io.*
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
@@ -45,21 +46,24 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
private val logs: MutableList<String> = NOPList.getInstance()
|
private val logs: MutableList<String> = NOPList.getInstance()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
protected var installDir = File("xxx")
|
protected lateinit var installDir: ExtendedFile
|
||||||
private lateinit var srcBoot: File
|
private lateinit var srcBoot: ExtendedFile
|
||||||
|
|
||||||
private val shell = Shell.getShell()
|
private val shell = Shell.getShell()
|
||||||
private val service get() = ServiceLocator.networkService
|
private val service get() = ServiceLocator.networkService
|
||||||
protected val context get() = ServiceLocator.deContext
|
protected val context get() = ServiceLocator.deContext
|
||||||
private val useRootDir = shell.isRoot && Info.noDataExec
|
private val useRootDir = shell.isRoot && Info.noDataExec
|
||||||
|
|
||||||
|
private val rootFS get() = RootUtils.fs
|
||||||
|
private val localFS get() = FileSystemManager.getLocal()
|
||||||
|
|
||||||
private fun findImage(): Boolean {
|
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()) {
|
if (bootPath.isEmpty()) {
|
||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
srcBoot = SuFile(bootPath)
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
console.add("- Target image: $bootPath")
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -77,7 +81,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("! Unable to detect target image")
|
console.add("! Unable to detect target image")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
srcBoot = SuFile(bootPath)
|
srcBoot = rootFS.getFile(bootPath)
|
||||||
console.add("- Target image: $bootPath")
|
console.add("- Target image: $bootPath")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -86,7 +90,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("- Device platform: ${Const.CPU_ABI}")
|
console.add("- Device platform: ${Const.CPU_ABI}")
|
||||||
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
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.deleteRecursively()
|
||||||
installDir.mkdirs()
|
installDir.mkdirs()
|
||||||
|
|
||||||
@@ -146,7 +150,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
if (useRootDir) {
|
if (useRootDir) {
|
||||||
// Move everything to tmpfs to workaround Samsung bullshit
|
// Move everything to tmpfs to workaround Samsung bullshit
|
||||||
SuFile(Const.TMPDIR).also {
|
rootFS.getFile(Const.TMPDIR).also {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"rm -rf $it",
|
"rm -rf $it",
|
||||||
"mkdir -p $it",
|
"mkdir -p $it",
|
||||||
@@ -160,14 +164,6 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
return true
|
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, _ ->
|
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
|
||||||
src.copyTo(out)
|
src.copyTo(out)
|
||||||
}
|
}
|
||||||
@@ -198,8 +194,8 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
val name = entry.name.replace(".lz4", "")
|
val name = entry.name.replace(".lz4", "")
|
||||||
console.add("-- Extracting: $name")
|
console.add("-- Extracting: $name")
|
||||||
|
|
||||||
val extract = installDirFile(name)
|
val extract = installDir.getChildFile(name)
|
||||||
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
|
decompressedStream().cleanPump(extract.newOutputStream())
|
||||||
} else if (entry.name.contains("vbmeta.img")) {
|
} else if (entry.name.contains("vbmeta.img")) {
|
||||||
val rawData = decompressedStream().readBytes()
|
val rawData = decompressedStream().readBytes()
|
||||||
// Valid vbmeta.img should be at least 256 bytes
|
// Valid vbmeta.img should be at least 256 bytes
|
||||||
@@ -219,8 +215,8 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val boot = installDirFile("boot.img")
|
val boot = installDir.getChildFile("boot.img")
|
||||||
val recovery = installDirFile("recovery.img")
|
val recovery = installDir.getChildFile("recovery.img")
|
||||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||||
// Install to recovery
|
// Install to recovery
|
||||||
srcBoot = recovery
|
srcBoot = recovery
|
||||||
@@ -234,7 +230,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
"./magiskboot cleanup",
|
"./magiskboot cleanup",
|
||||||
"rm -f new-boot.img",
|
"rm -f new-boot.img",
|
||||||
"cd /").sh()
|
"cd /").sh()
|
||||||
SuFileInputStream.open(boot).use {
|
boot.newInputStream().use {
|
||||||
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
|
||||||
it.copyTo(tarOut)
|
it.copyTo(tarOut)
|
||||||
}
|
}
|
||||||
@@ -280,9 +276,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
processTar(src, outFile!!.uri.outputStream())
|
processTar(src, outFile!!.uri.outputStream())
|
||||||
} else {
|
} else {
|
||||||
// raw image
|
// raw image
|
||||||
srcBoot = installDirFile("boot.img")
|
srcBoot = installDir.getChildFile("boot.img")
|
||||||
console.add("- Copying image to cache")
|
console.add("- Copying image to cache")
|
||||||
src.cleanPump(SuFileOutputStream.open(srcBoot))
|
src.cleanPump(srcBoot.newOutputStream())
|
||||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||||
outFile!!.uri.outputStream()
|
outFile!!.uri.outputStream()
|
||||||
}
|
}
|
||||||
@@ -302,12 +298,12 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
// Output file
|
// Output file
|
||||||
try {
|
try {
|
||||||
val newBoot = installDirFile("new-boot.img")
|
val newBoot = installDir.getChildFile("new-boot.img")
|
||||||
if (outStream is TarOutputStream) {
|
if (outStream is TarOutputStream) {
|
||||||
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
|
||||||
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
|
||||||
}
|
}
|
||||||
SuFileInputStream.open(newBoot).cleanPump(outStream)
|
newBoot.newInputStream().cleanPump(outStream)
|
||||||
newBoot.delete()
|
newBoot.delete()
|
||||||
|
|
||||||
console.add("")
|
console.add("")
|
||||||
@@ -331,9 +327,9 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
|
|
||||||
private fun patchBoot(): Boolean {
|
private fun patchBoot(): Boolean {
|
||||||
var isSigned = false
|
var isSigned = false
|
||||||
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
|
if (!srcBoot.isCharacter) {
|
||||||
try {
|
try {
|
||||||
SuFileInputStream.open(srcBoot).use {
|
srcBoot.newInputStream().use {
|
||||||
if (SignBoot.verifySignature(it, null)) {
|
if (SignBoot.verifySignature(it, null)) {
|
||||||
isSigned = true
|
isSigned = true
|
||||||
console.add("- Boot image is signed with AVB 1.0")
|
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) {
|
if (!useRootDir) {
|
||||||
// Create output files before hand
|
// Create output files before hand
|
||||||
newBoot.createNewFile()
|
newBoot.createNewFile()
|
||||||
@@ -370,7 +366,7 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
console.add("- Signing boot image with verity keys")
|
console.add("- Signing boot image with verity keys")
|
||||||
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
val signed = File.createTempFile("signed", ".img", context.cacheDir)
|
||||||
try {
|
try {
|
||||||
val src = SuFileInputStream.open(newBoot).buffered()
|
val src = newBoot.newInputStream().buffered()
|
||||||
val out = signed.outputStream().buffered()
|
val out = signed.outputStream().buffered()
|
||||||
withStreams(src, out) { _, _ ->
|
withStreams(src, out) { _, _ ->
|
||||||
SignBoot.doSignature(null, null, src, out, "/boot")
|
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 String.fsh() = ShellUtils.fastCmd(shell, this)
|
||||||
private fun Array<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 fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
|
||||||
|
|
||||||
protected suspend fun secondSlot() =
|
protected fun secondSlot() =
|
||||||
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
|
||||||
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
|
||||||
@@ -425,20 +421,15 @@ abstract class MagiskInstallImpl protected constructor(
|
|||||||
protected abstract suspend fun operations(): Boolean
|
protected abstract suspend fun operations(): Boolean
|
||||||
|
|
||||||
open suspend fun exec(): Boolean {
|
open suspend fun exec(): Boolean {
|
||||||
synchronized(Companion) {
|
if (haveActiveSession.getAndSet(true))
|
||||||
if (haveActiveSession)
|
|
||||||
return false
|
return false
|
||||||
haveActiveSession = true
|
|
||||||
}
|
|
||||||
val result = withContext(Dispatchers.IO) { operations() }
|
val result = withContext(Dispatchers.IO) { operations() }
|
||||||
synchronized(Companion) {
|
haveActiveSession.set(false)
|
||||||
haveActiveSession = false
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var haveActiveSession = false
|
private var haveActiveSession = AtomicBoolean(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +454,7 @@ abstract class MagiskInstaller(
|
|||||||
console: MutableList<String>,
|
console: MutableList<String>,
|
||||||
logs: MutableList<String>
|
logs: MutableList<String>
|
||||||
) : MagiskInstaller(console, logs) {
|
) : MagiskInstaller(console, logs) {
|
||||||
override suspend fun operations() = doPatchFile(uri)
|
override suspend fun operations() = patchFile(uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
class SecondSlot(
|
class SecondSlot(
|
||||||
|
@@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream
|
|||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder.LITTLE_ENDIAN
|
import java.nio.ByteOrder.LITTLE_ENDIAN
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class AXML(b: ByteArray) {
|
class AXML(b: ByteArray) {
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
|
||||||
object BiometricHelper {
|
object BiometricHelper {
|
||||||
|
|
||||||
|
@@ -1,23 +1,17 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
package com.topjohnwu.magisk.core.utils
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Base64OutputStream
|
import android.util.Base64OutputStream
|
||||||
import com.topjohnwu.magisk.core.Config
|
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.x500.X500Name
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -29,13 +23,11 @@ private interface CertKeyProvider {
|
|||||||
val key: PrivateKey
|
val key: PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
class Keygen : CertKeyProvider {
|
||||||
class Keygen(context: Context) : CertKeyProvider {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ALIAS = "magisk"
|
private const val ALIAS = "magisk"
|
||||||
private val PASSWORD get() = "magisk".toCharArray()
|
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 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
|
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 start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
|
||||||
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
|
||||||
|
|
||||||
override val cert get() = provider.cert
|
private val ks = init()
|
||||||
override val key get() = provider.key
|
override val cert = ks.getCertificate(ALIAS) as X509Certificate
|
||||||
|
override val key = ks.getKey(ALIAS, PASSWORD) as PrivateKey
|
||||||
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 fun init(): KeyStore {
|
private fun init(): KeyStore {
|
||||||
val raw = Config.keyStoreRaw
|
val raw = Config.keyStoreRaw
|
||||||
@@ -93,12 +45,8 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
if (raw.isEmpty()) {
|
if (raw.isEmpty()) {
|
||||||
ks.load(null)
|
ks.load(null)
|
||||||
} else {
|
} else {
|
||||||
GZIPInputStream(Base64.decode(raw,
|
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||||
BASE64_FLAG
|
ks.load(it, PASSWORD)
|
||||||
).inputStream()).use {
|
|
||||||
ks.load(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,22 +57,19 @@ class Keygen(context: Context) : CertKeyProvider {
|
|||||||
// Generate new private key and certificate
|
// Generate new private key and certificate
|
||||||
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||||
val dname = X500Name(DNAME)
|
val dname = X500Name(DNAME)
|
||||||
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
val builder = X509v3CertificateBuilder(
|
||||||
start.time, end.time, dname, kp.public)
|
dname, BigInteger(160, Random()),
|
||||||
|
start.time, end.time, Locale.ROOT, dname,
|
||||||
|
SubjectPublicKeyInfo.getInstance(kp.public.encoded)
|
||||||
|
)
|
||||||
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
val signer = JcaContentSignerBuilder("SHA1WithRSA").build(kp.private)
|
||||||
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
// Store them into keystore
|
// Store them into keystore
|
||||||
ks.setKeyEntry(
|
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||||
ALIAS, kp.private,
|
|
||||||
PASSWORD, arrayOf(cert))
|
|
||||||
val bytes = ByteArrayOutputStream()
|
val bytes = ByteArrayOutputStream()
|
||||||
GZIPOutputStream(Base64OutputStream(bytes,
|
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||||
BASE64_FLAG
|
ks.store(it, PASSWORD)
|
||||||
)).use {
|
|
||||||
ks.store(it,
|
|
||||||
PASSWORD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Config.keyStoreRaw = bytes.toString("UTF-8")
|
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||||
|
|
||||||
|
@@ -6,13 +6,13 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.core.ActivityTracker
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.createNewResources
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
var currentLocale: Locale = Locale.getDefault()
|
var currentLocale: Locale = Locale.getDefault()
|
||||||
|
|
||||||
@@ -28,6 +28,11 @@ withContext(Dispatchers.Default) {
|
|||||||
// Create a completely new resource to prevent cross talk over active configs
|
// Create a completely new resource to prevent cross talk over active configs
|
||||||
val res = createNewResources()
|
val res = createNewResources()
|
||||||
|
|
||||||
|
fun changeLocale(locale: Locale) {
|
||||||
|
res.configuration.setLocale(locale)
|
||||||
|
res.updateConfiguration(res.configuration, res.displayMetrics)
|
||||||
|
}
|
||||||
|
|
||||||
val locales = ArrayList<String>().apply {
|
val locales = ArrayList<String>().apply {
|
||||||
// Add default locale
|
// Add default locale
|
||||||
add("en")
|
add("en")
|
||||||
@@ -41,13 +46,13 @@ withContext(Dispatchers.Default) {
|
|||||||
}.map {
|
}.map {
|
||||||
Locale.forLanguageTag(it)
|
Locale.forLanguageTag(it)
|
||||||
}.distinctBy {
|
}.distinctBy {
|
||||||
res.setLocale(it)
|
changeLocale(it)
|
||||||
res.getString(compareId)
|
res.getString(compareId)
|
||||||
}.sortedWith { a, b ->
|
}.sortedWith { a, b ->
|
||||||
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setLocale(defaultLocale)
|
changeLocale(defaultLocale)
|
||||||
val defName = res.getString(R.string.system_default)
|
val defName = res.getString(R.string.system_default)
|
||||||
|
|
||||||
val names = ArrayList<String>(locales.size + 1)
|
val names = ArrayList<String>(locales.size + 1)
|
||||||
@@ -71,11 +76,6 @@ fun Resources.setConfig(config: Configuration) {
|
|||||||
|
|
||||||
fun Resources.syncLocale() = setConfig(configuration)
|
fun Resources.syncLocale() = setConfig(configuration)
|
||||||
|
|
||||||
fun Resources.setLocale(locale: Locale) {
|
|
||||||
configuration.setLocale(locale)
|
|
||||||
updateConfiguration(configuration, displayMetrics)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshLocale() {
|
fun refreshLocale() {
|
||||||
val localeConfig = Config.locale
|
val localeConfig = Config.locale
|
||||||
currentLocale = when {
|
currentLocale = when {
|
||||||
@@ -84,4 +84,5 @@ fun refreshLocale() {
|
|||||||
}
|
}
|
||||||
Locale.setDefault(currentLocale)
|
Locale.setDefault(currentLocale)
|
||||||
AppContext.resources.syncLocale()
|
AppContext.resources.syncLocale()
|
||||||
|
ActivityTracker.foreground?.recreate()
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.net.toFile
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.core.Config
|
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.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@@ -1,54 +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.Shell
|
|
||||||
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) {
|
|
||||||
// Always log full stack trace with Timber
|
|
||||||
Timber.plant(Timber.DebugTree())
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler { _, e ->
|
|
||||||
Timber.e(e)
|
|
||||||
exitProcess(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val className: String? = stub?.javaClass?.name
|
|
||||||
|
|
||||||
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: Shell.Task? = 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
|
||||||
|
}
|
||||||
|
}
|
@@ -19,8 +19,9 @@ import java.util.jar.JarFile
|
|||||||
class ShellInit : Shell.Initializer() {
|
class ShellInit : Shell.Initializer() {
|
||||||
override fun onInit(context: Context, shell: Shell): Boolean {
|
override fun onInit(context: Context, shell: Shell): Boolean {
|
||||||
if (shell.isRoot) {
|
if (shell.isRoot) {
|
||||||
RootRegistry.bindTask?.let { shell.execTask(it) }
|
Info.isRooted = true
|
||||||
RootRegistry.bindTask = null
|
RootUtils.bindTask?.let { shell.execTask(it) }
|
||||||
|
RootUtils.bindTask = null
|
||||||
}
|
}
|
||||||
shell.newJob().apply {
|
shell.newJob().apply {
|
||||||
add("export ASH_STANDALONE=1")
|
add("export ASH_STANDALONE=1")
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
|
||||||
|
|
||||||
class 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 == Activity.RESULT_OK
|
|
||||||
}
|
|
@@ -1,7 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.core.utils
|
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.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -31,12 +29,12 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
|
|||||||
else
|
else
|
||||||
entry.name
|
entry.name
|
||||||
|
|
||||||
var dest = File(folder, name)
|
val dest = File(folder, name)
|
||||||
if (!dest.parentFile!!.exists() && !dest.parentFile!!.mkdirs()) {
|
dest.parentFile!!.let {
|
||||||
dest = SuFile(folder, name)
|
if (!it.exists())
|
||||||
dest.parentFile!!.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
|
dest.outputStream().use { out -> zin.copyTo(out) }
|
||||||
}
|
}
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
throw IOException(e)
|
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.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.*
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.cardview.widget.CardView
|
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.chip.Chip
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.topjohnwu.magisk.R
|
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.superuser.internal.UiThreadHandler
|
||||||
import com.topjohnwu.widget.IndeterminateCheckBox
|
import com.topjohnwu.widget.IndeterminateCheckBox
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -289,3 +287,13 @@ fun TextView.setTextColorAttr(attr: Int) {
|
|||||||
context.theme.resolveAttribute(attr, tv, true)
|
context.theme.resolveAttribute(attr, tv, true)
|
||||||
setTextColor(tv.data)
|
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.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param callback The callback that controls the behavior of the DiffObservableList.
|
* @param callback The callback that controls the behavior of the DiffObservableList.
|
||||||
|
@@ -1,35 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.databinding
|
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() =
|
fun <T : AnyDiffRvItem> diffListOf() =
|
||||||
DiffObservableList(DiffRvItem.callback<T>())
|
DiffObservableList(DiffRvItem.callback<T>())
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> diffListOf(newItems: List<T>) =
|
|
||||||
DiffObservableList(DiffRvItem.callback<T>()).also { it.update(newItems) }
|
|
||||||
|
|
||||||
fun <T : AnyDiffRvItem> filterableListOf() =
|
fun <T : AnyDiffRvItem> filterableListOf() =
|
||||||
FilterableDiffObservableList(DiffRvItem.callback<T>())
|
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
|
package com.topjohnwu.magisk.databinding
|
||||||
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.databinding.PropertyChangeRegistry
|
import androidx.databinding.PropertyChangeRegistry
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.BR
|
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
|
||||||
|
|
||||||
abstract class RvItem {
|
abstract class RvItem {
|
||||||
|
|
||||||
abstract val layoutRes: Int
|
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> {
|
interface RvContainer<E> {
|
||||||
val item: E
|
val item: E
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ViewAwareRvItem {
|
||||||
|
fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView)
|
||||||
|
}
|
||||||
|
|
||||||
interface ComparableRv<T> : Comparable<T> {
|
interface ComparableRv<T> : Comparable<T> {
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
fun comparableEqual(o: Any?) =
|
fun comparableEqual(o: Any?) =
|
||||||
@@ -77,13 +65,3 @@ abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
|
|||||||
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
abstract class ObservableRvItem : RvItem(), ObservableHost {
|
||||||
override var callbacks: PropertyChangeRegistry? = null
|
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 androidx.core.content.getSystemService
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import com.topjohnwu.magisk.ktx.reboot as systemReboot
|
import com.topjohnwu.magisk.ktx.reboot as systemReboot
|
||||||
|
|
||||||
object RebootEvent {
|
object RebootEvent {
|
||||||
@@ -20,7 +19,7 @@ object RebootEvent {
|
|||||||
R.id.action_reboot_bootloader -> systemReboot("bootloader")
|
R.id.action_reboot_bootloader -> systemReboot("bootloader")
|
||||||
R.id.action_reboot_download -> systemReboot("download")
|
R.id.action_reboot_download -> systemReboot("download")
|
||||||
R.id.action_reboot_edl -> systemReboot("edl")
|
R.id.action_reboot_edl -> systemReboot("edl")
|
||||||
R.id.action_reboot_recovery -> Shell.cmd("/system/bin/reboot recovery").submit()
|
R.id.action_reboot_recovery -> systemReboot("recovery")
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@@ -1,19 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.events
|
package com.topjohnwu.magisk.events
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.topjohnwu.magisk.MainDirections
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.arch.*
|
import com.topjohnwu.magisk.arch.*
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.base.ContentResultCallback
|
||||||
import com.topjohnwu.magisk.utils.TextHolder
|
import com.topjohnwu.magisk.utils.TextHolder
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.utils.asText
|
import com.topjohnwu.magisk.utils.asText
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
|
|
||||||
@@ -52,16 +46,12 @@ class RecreateEvent : ViewEvent(), ActivityExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MagiskInstallFileEvent(
|
class GetContentEvent(
|
||||||
private val callback: (Uri) -> Unit
|
private val type: String,
|
||||||
|
private val callback: ContentResultCallback
|
||||||
) : ViewEvent(), ActivityExecutor {
|
) : ViewEvent(), ActivityExecutor {
|
||||||
override fun invoke(activity: UIActivity<*>) {
|
override fun invoke(activity: UIActivity<*>) {
|
||||||
try {
|
activity.getContent(type, callback)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,20 +73,6 @@ 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(
|
class SnackbarEvent(
|
||||||
private val msg: TextHolder,
|
private val msg: TextHolder,
|
||||||
private val length: Int = Snackbar.LENGTH_SHORT,
|
private val length: Int = Snackbar.LENGTH_SHORT,
|
||||||
|
@@ -3,6 +3,7 @@ package com.topjohnwu.magisk.events.dialog
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.arch.UIActivity
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
@@ -33,6 +34,6 @@ class DarkThemeDialog : DialogEvent() {
|
|||||||
|
|
||||||
private fun selectTheme(mode: Int, activity: Activity) {
|
private fun selectTheme(mode: Int, activity: Activity) {
|
||||||
Config.darkTheme = mode
|
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.R
|
||||||
import com.topjohnwu.magisk.core.Info
|
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.DownloadService
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
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 com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import androidx.annotation.CallSuper
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
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 com.topjohnwu.magisk.view.MagiskDialog
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
package com.topjohnwu.magisk.events.dialog
|
package com.topjohnwu.magisk.events.dialog
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
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.Action
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
|
|
||||||
class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
|
||||||
|
@@ -7,11 +7,10 @@ import android.content.Context
|
|||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
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.Configuration
|
||||||
import android.content.res.Resources
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.drawable.AdaptiveIconDrawable
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
@@ -19,40 +18,24 @@ import android.graphics.drawable.BitmapDrawable
|
|||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.text.PrecomputedText
|
import android.os.Process
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewTreeObserver
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
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.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
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.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.transition.AutoTransition
|
import androidx.transition.AutoTransition
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
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.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 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 java.io.File
|
||||||
|
import kotlin.Array
|
||||||
|
import kotlin.String
|
||||||
import java.lang.reflect.Array as JArray
|
import java.lang.reflect.Array as JArray
|
||||||
|
|
||||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||||
@@ -79,8 +62,6 @@ val Context.deviceProtectedContext: Context get() =
|
|||||||
createDeviceProtectedStorageContext()
|
createDeviceProtectedStorageContext()
|
||||||
} else { this }
|
} else { this }
|
||||||
|
|
||||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
|
||||||
|
|
||||||
fun Intent.startActivityWithRoot() {
|
fun Intent.startActivityWithRoot() {
|
||||||
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||||
val cmd = toCommand(args).joinToString(" ")
|
val cmd = toCommand(args).joinToString(" ")
|
||||||
@@ -185,16 +166,8 @@ fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<S
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
|
||||||
|
|
||||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
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 {
|
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (labelRes > 0) {
|
if (labelRes > 0) {
|
||||||
@@ -209,37 +182,6 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
|||||||
return loadLabel(pm).toString()
|
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 {
|
fun Context.unwrap(): Context {
|
||||||
var context = this
|
var context = this
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -262,21 +204,6 @@ fun Activity.hideKeyboard() {
|
|||||||
view.clearFocus()
|
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() {
|
fun ViewGroup.startAnimations() {
|
||||||
val transition = AutoTransition()
|
val transition = AutoTransition()
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
@@ -308,3 +235,33 @@ fun getProperty(key: String, def: String): String {
|
|||||||
}
|
}
|
||||||
return def
|
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
|
package com.topjohnwu.magisk.ktx
|
||||||
|
|
||||||
import androidx.collection.SparseArrayCompat
|
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.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
import java.text.DateFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.ZipEntry
|
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 <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 =
|
fun Class<*>.reflectField(name: String): Field =
|
||||||
getDeclaredField(name).apply { isAccessible = true }
|
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,6 +8,10 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
fun reboot(reason: String = if (Config.recovery) "recovery" else "") {
|
||||||
|
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()
|
Shell.cmd("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 org.bouncycastle.util.encoders.Base64;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FilterOutputStream;
|
import java.io.FilterOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.signing;
|
package com.topjohnwu.magisk.signing;
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@@ -33,7 +32,6 @@ import java.security.cert.CertificateFactory;
|
|||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Keep
|
|
||||||
public class SignBoot {
|
public class SignBoot {
|
||||||
|
|
||||||
private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
|
private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
|
||||||
|
@@ -13,14 +13,14 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import com.topjohnwu.magisk.MainDirections
|
import com.topjohnwu.magisk.MainDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseMainActivity
|
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
|
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||||
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
|
||||||
import com.topjohnwu.magisk.di.viewModel
|
|
||||||
import com.topjohnwu.magisk.ktx.startAnimations
|
import com.topjohnwu.magisk.ktx.startAnimations
|
||||||
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
@@ -30,11 +30,16 @@ import java.io.File
|
|||||||
|
|
||||||
class MainViewModel : BaseViewModel()
|
class MainViewModel : BaseViewModel()
|
||||||
|
|
||||||
class MainActivity : BaseMainActivity<ActivityMainMd2Binding>() {
|
class MainActivity : SplashActivity<ActivityMainMd2Binding>() {
|
||||||
|
|
||||||
override val layoutRes = R.layout.activity_main_md2
|
override val layoutRes = R.layout.activity_main_md2
|
||||||
override val viewModel by viewModel<MainViewModel>()
|
override val viewModel by viewModel<MainViewModel>()
|
||||||
override val navHostId: Int = R.id.main_nav_host
|
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?
|
override val snackbarAnchorView: View?
|
||||||
get() {
|
get() {
|
||||||
val fragmentAnchor = currentFragment?.snackbarAnchorView
|
val fragmentAnchor = currentFragment?.snackbarAnchorView
|
||||||
@@ -84,7 +89,7 @@ class MainActivity : BaseMainActivity<ActivityMainMd2Binding>() {
|
|||||||
}
|
}
|
||||||
binding.mainNavigation.menu.apply {
|
binding.mainNavigation.menu.apply {
|
||||||
findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
|
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 =
|
val section =
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -9,12 +9,15 @@ import androidx.databinding.ViewDataBinding
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.StubApk
|
||||||
|
import com.topjohnwu.magisk.arch.NavigationActivity
|
||||||
import com.topjohnwu.magisk.core.Config
|
import com.topjohnwu.magisk.core.Config
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.JobService
|
import com.topjohnwu.magisk.core.JobService
|
||||||
|
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||||
import com.topjohnwu.magisk.di.ServiceLocator
|
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.view.MagiskDialog
|
import com.topjohnwu.magisk.view.MagiskDialog
|
||||||
@@ -23,16 +26,17 @@ import com.topjohnwu.magisk.view.Shortcuts
|
|||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
@SuppressLint("CustomSplashScreen")
|
||||||
|
abstract class SplashActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var doPreload = true
|
private var skipSplash = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setTheme(Theme.selected.themeRes)
|
setTheme(Theme.selected.themeRes)
|
||||||
|
|
||||||
if (isRunningAsStub && doPreload) {
|
if (isRunningAsStub && !skipSplash) {
|
||||||
// Manually apply splash theme for stub
|
// Manually apply splash theme for stub
|
||||||
theme.applyStyle(R.style.StubSplashTheme, true)
|
theme.applyStyle(R.style.StubSplashTheme, true)
|
||||||
}
|
}
|
||||||
@@ -41,30 +45,21 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
|
|||||||
|
|
||||||
if (!isRunningAsStub) {
|
if (!isRunningAsStub) {
|
||||||
val splashScreen = installSplashScreen()
|
val splashScreen = installSplashScreen()
|
||||||
splashScreen.setKeepOnScreenCondition { doPreload }
|
splashScreen.setKeepOnScreenCondition { !skipSplash }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doPreload) {
|
if (skipSplash) {
|
||||||
Shell.getShell(null) {
|
showMainUI(savedInstanceState)
|
||||||
|
} else {
|
||||||
|
Shell.getShell(Shell.EXECUTOR) {
|
||||||
if (isRunningAsStub && !it.isRoot) {
|
if (isRunningAsStub && !it.isRoot) {
|
||||||
showInvalidStateMessage()
|
showInvalidStateMessage()
|
||||||
return@getShell
|
return@getShell
|
||||||
}
|
}
|
||||||
preLoad()
|
preLoad(savedInstanceState)
|
||||||
runOnUiThread {
|
|
||||||
doPreload = false
|
|
||||||
if (isRunningAsStub) {
|
|
||||||
// Re-launch main activity without splash theme
|
|
||||||
relaunch()
|
|
||||||
} else {
|
|
||||||
showMainUI(savedInstanceState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
showMainUI(savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun showMainUI(savedInstanceState: Bundle?)
|
abstract fun showMainUI(savedInstanceState: Bundle?)
|
||||||
|
|
||||||
@@ -82,7 +77,7 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
|
|||||||
showInvalidStateMessage()
|
showInvalidStateMessage()
|
||||||
} else {
|
} else {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
HideAPK.restore(this@BaseMainActivity)
|
HideAPK.restore(this@SplashActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,17 +88,44 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun preLoad() {
|
private fun preLoad(savedState: Bundle?) {
|
||||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
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)
|
Config.load(prevPkg)
|
||||||
handleRepackage(prevPkg)
|
handleRepackage(prevPkg)
|
||||||
|
if (prevPkg != null) {
|
||||||
|
runOnUiThread {
|
||||||
|
// Relaunch the process after package migration
|
||||||
|
StubApk.restartProcess(this)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Notifications.setup(this)
|
Notifications.setup(this)
|
||||||
JobService.schedule(this)
|
JobService.schedule(this)
|
||||||
Shortcuts.setupDynamic(this)
|
Shortcuts.setupDynamic(this)
|
||||||
|
|
||||||
// Pre-fetch network services
|
// Pre-fetch network services
|
||||||
ServiceLocator.networkService
|
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?) {
|
private fun handleRepackage(pkg: String?) {
|
||||||
@@ -114,13 +136,10 @@ abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<
|
|||||||
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
Shell.cmd("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (Config.suManager.isNotEmpty())
|
if (!Const.Version.atLeast_25_0() && Config.suManager.isNotEmpty())
|
||||||
Config.suManager = ""
|
Config.suManager = ""
|
||||||
pkg ?: return
|
pkg ?: return
|
||||||
if (!Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) {
|
Shell.cmd("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||||
// Uninstall through Android API
|
|
||||||
uninstallAndWait(pkg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@@ -10,8 +10,8 @@ import androidx.appcompat.widget.SearchView
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseFragment
|
import com.topjohnwu.magisk.arch.BaseFragment
|
||||||
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentDenyMd2Binding
|
||||||
import com.topjohnwu.magisk.di.viewModel
|
|
||||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
import com.topjohnwu.magisk.ktx.hideKeyboard
|
||||||
import rikka.recyclerview.addEdgeSpacing
|
import rikka.recyclerview.addEdgeSpacing
|
||||||
import rikka.recyclerview.addItemSpacing
|
import rikka.recyclerview.addItemSpacing
|
||||||
@@ -35,7 +35,7 @@ class DenyListFragment : BaseFragment<FragmentDenyMd2Binding>() {
|
|||||||
|
|
||||||
binding.appList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
binding.appList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
|
if (newState != RecyclerView.SCROLL_STATE_IDLE) activity?.hideKeyboard()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -2,20 +2,22 @@ package com.topjohnwu.magisk.ui.deny
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.databinding.Bindable
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||||
|
import com.topjohnwu.magisk.core.di.AppContext
|
||||||
|
import com.topjohnwu.magisk.databinding.bindExtra
|
||||||
import com.topjohnwu.magisk.databinding.filterableListOf
|
import com.topjohnwu.magisk.databinding.filterableListOf
|
||||||
import com.topjohnwu.magisk.databinding.itemBindingOf
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.di.AppContext
|
|
||||||
import com.topjohnwu.magisk.ktx.concurrentMap
|
import com.topjohnwu.magisk.ktx.concurrentMap
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import java.util.*
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.toCollection
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class DenyListViewModel : BaseViewModel() {
|
class DenyListViewModel : AsyncLoadViewModel() {
|
||||||
|
|
||||||
var isShowSystem = false
|
var isShowSystem = false
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -36,20 +38,17 @@ class DenyListViewModel : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val items = filterableListOf<DenyListRvItem>()
|
val items = filterableListOf<DenyListRvItem>()
|
||||||
val itemBinding = itemBindingOf<DenyListRvItem> {
|
val extraBindings = bindExtra {
|
||||||
it.bindExtra(BR.viewModel, this)
|
it.put(BR.viewModel, this)
|
||||||
}
|
|
||||||
val itemInternalBinding = itemBindingOf<ProcessRvItem> {
|
|
||||||
it.bindExtra(BR.viewModel, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
var loading = true
|
||||||
|
private set(value) = set(value, field, { field = it }, BR.loading)
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
override fun refresh() = viewModelScope.launch {
|
override suspend fun doLoadWork() {
|
||||||
if (!Utils.showSuperUser()) {
|
loading = true
|
||||||
state = State.LOADING_FAILED
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
state = State.LOADING
|
|
||||||
val (apps, diff) = withContext(Dispatchers.Default) {
|
val (apps, diff) = withContext(Dispatchers.Default) {
|
||||||
val pm = AppContext.packageManager
|
val pm = AppContext.packageManager
|
||||||
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
||||||
@@ -84,6 +83,6 @@ class DenyListViewModel : BaseViewModel() {
|
|||||||
|
|
||||||
(it.isChecked || (filterSystem() && filterOS())) && filterQuery()
|
(it.isChecked || (filterSystem() && filterOS())) && filterQuery()
|
||||||
}
|
}
|
||||||
state = State.LOADED
|
loading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,19 +7,18 @@ import androidx.databinding.ViewDataBinding
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.databinding.DiffRvItem
|
import com.topjohnwu.magisk.databinding.DiffRvItem
|
||||||
import com.topjohnwu.magisk.databinding.LenientRvItem
|
|
||||||
import com.topjohnwu.magisk.databinding.RvContainer
|
import com.topjohnwu.magisk.databinding.RvContainer
|
||||||
|
import com.topjohnwu.magisk.databinding.ViewAwareRvItem
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class ConsoleItem(
|
class ConsoleItem(
|
||||||
override val item: String
|
override val item: String
|
||||||
) : DiffRvItem<ConsoleItem>(), LenientRvItem,
|
) : DiffRvItem<ConsoleItem>(), ViewAwareRvItem, RvContainer<String> {
|
||||||
RvContainer<String> {
|
|
||||||
override val layoutRes = R.layout.item_console_md2
|
override val layoutRes = R.layout.item_console_md2
|
||||||
|
|
||||||
private var parentWidth = -1
|
private var parentWidth = -1
|
||||||
|
|
||||||
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
override fun onBind(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
||||||
if (parentWidth < 0)
|
if (parentWidth < 0)
|
||||||
parentWidth = (recyclerView.parent as View).width
|
parentWidth = (recyclerView.parent as View).width
|
||||||
|
|
||||||
|
@@ -6,20 +6,24 @@ import android.content.pm.ActivityInfo
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.navigation.NavDeepLinkBuilder
|
import androidx.navigation.NavDeepLinkBuilder
|
||||||
import com.topjohnwu.magisk.MainDirections
|
import com.topjohnwu.magisk.MainDirections
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseFragment
|
import com.topjohnwu.magisk.arch.BaseFragment
|
||||||
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.cmp
|
import com.topjohnwu.magisk.core.cmp
|
||||||
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
|
||||||
import com.topjohnwu.magisk.di.viewModel
|
|
||||||
import com.topjohnwu.magisk.ui.MainActivity
|
import com.topjohnwu.magisk.ui.MainActivity
|
||||||
|
|
||||||
class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
||||||
|
|
||||||
override val layoutRes = R.layout.fragment_flash_md2
|
override val layoutRes = R.layout.fragment_flash_md2
|
||||||
override val viewModel by viewModel<FlashViewModel>()
|
override val viewModel by viewModel<FlashViewModel>()
|
||||||
|
override val snackbarView: View get() = binding.snackbarContainer
|
||||||
|
override val snackbarAnchorView: View?
|
||||||
|
get() = if (binding.restartBtn.isShown) binding.restartBtn else super.snackbarAnchorView
|
||||||
|
|
||||||
private var defaultOrientation = -1
|
private var defaultOrientation = -1
|
||||||
|
|
||||||
@@ -33,8 +37,20 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
|||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
activity?.setTitle(R.string.flash_screen_title)
|
activity?.setTitle(R.string.flash_screen_title)
|
||||||
|
|
||||||
viewModel.subtitle.observe(this) {
|
viewModel.state.observe(this) {
|
||||||
activity?.supportActionBar?.setSubtitle(it)
|
activity?.supportActionBar?.setSubtitle(
|
||||||
|
when (it) {
|
||||||
|
FlashViewModel.State.FLASHING -> R.string.flashing
|
||||||
|
FlashViewModel.State.SUCCESS -> R.string.done
|
||||||
|
FlashViewModel.State.FAILED -> R.string.failure
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (it == FlashViewModel.State.SUCCESS && viewModel.showReboot) {
|
||||||
|
binding.restartBtn.apply {
|
||||||
|
if (!this.isVisible) this.show()
|
||||||
|
if (!this.isFocused) this.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,8 +67,10 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
|||||||
|
|
||||||
defaultOrientation = activity?.requestedOrientation ?: -1
|
defaultOrientation = activity?.requestedOrientation ?: -1
|
||||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||||
|
if (savedInstanceState == null) {
|
||||||
viewModel.startFlashing()
|
viewModel.startFlashing()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@@ -63,7 +81,7 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onKeyEvent(event: KeyEvent): Boolean {
|
override fun onKeyEvent(event: KeyEvent): Boolean {
|
||||||
return when(event.keyCode) {
|
return when (event.keyCode) {
|
||||||
KeyEvent.KEYCODE_VOLUME_UP,
|
KeyEvent.KEYCODE_VOLUME_UP,
|
||||||
KeyEvent.KEYCODE_VOLUME_DOWN -> true
|
KeyEvent.KEYCODE_VOLUME_DOWN -> true
|
||||||
else -> false
|
else -> false
|
||||||
@@ -71,7 +89,8 @@ class FlashFragment : BaseFragment<FragmentFlashMd2Binding>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean {
|
override fun onBackPressed(): Boolean {
|
||||||
if (viewModel.loading) return true
|
if (viewModel.flashing.value == true)
|
||||||
|
return true
|
||||||
return super.onBackPressed()
|
return super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ import android.view.MenuItem
|
|||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Transformations
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
@@ -14,29 +15,32 @@ import com.topjohnwu.magisk.core.tasks.FlashZip
|
|||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.databinding.RvBindingAdapter
|
|
||||||
import com.topjohnwu.magisk.databinding.diffListOf
|
import com.topjohnwu.magisk.databinding.diffListOf
|
||||||
import com.topjohnwu.magisk.databinding.itemBindingOf
|
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.ktx.*
|
import com.topjohnwu.magisk.ktx.reboot
|
||||||
|
import com.topjohnwu.magisk.ktx.synchronized
|
||||||
|
import com.topjohnwu.magisk.ktx.timeFormatStandard
|
||||||
|
import com.topjohnwu.magisk.ktx.toTime
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class FlashViewModel : BaseViewModel() {
|
class FlashViewModel : BaseViewModel() {
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
FLASHING, SUCCESS, FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _state = MutableLiveData(State.FLASHING)
|
||||||
|
val state: LiveData<State> get() = _state
|
||||||
|
val flashing = Transformations.map(state) { it == State.FLASHING }
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var showReboot = Shell.rootAccess()
|
var showReboot = Info.isRooted
|
||||||
set(value) = set(value, field, { field = it }, BR.showReboot)
|
set(value) = set(value, field, { field = it }, BR.showReboot)
|
||||||
|
|
||||||
private val _subtitle = MutableLiveData(R.string.flashing)
|
|
||||||
val subtitle get() = _subtitle as LiveData<Int>
|
|
||||||
|
|
||||||
val adapter = RvBindingAdapter<ConsoleItem>()
|
|
||||||
val items = diffListOf<ConsoleItem>()
|
val items = diffListOf<ConsoleItem>()
|
||||||
val itemBinding = itemBindingOf<ConsoleItem>()
|
|
||||||
lateinit var args: FlashFragmentArgs
|
lateinit var args: FlashFragmentArgs
|
||||||
|
|
||||||
private val logItems = mutableListOf<String>().synchronized()
|
private val logItems = mutableListOf<String>().synchronized()
|
||||||
@@ -54,7 +58,8 @@ class FlashViewModel : BaseViewModel() {
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = when (action) {
|
val result = when (action) {
|
||||||
Const.Value.FLASH_ZIP -> {
|
Const.Value.FLASH_ZIP -> {
|
||||||
FlashZip(uri!!, outItems, logItems).exec()
|
uri ?: return@launch
|
||||||
|
FlashZip(uri, outItems, logItems).exec()
|
||||||
}
|
}
|
||||||
Const.Value.UNINSTALL -> {
|
Const.Value.UNINSTALL -> {
|
||||||
showReboot = false
|
showReboot = false
|
||||||
@@ -84,11 +89,7 @@ class FlashViewModel : BaseViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onResult(success: Boolean) {
|
private fun onResult(success: Boolean) {
|
||||||
state = if (success) State.LOADED else State.LOADING_FAILED
|
_state.value = if (success) State.SUCCESS else State.FAILED
|
||||||
when {
|
|
||||||
success -> _subtitle.postValue(R.string.done)
|
|
||||||
else -> _subtitle.postValue(R.string.failure)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMenuItemClicked(item: MenuItem): Boolean {
|
fun onMenuItemClicked(item: MenuItem): Boolean {
|
||||||
@@ -100,7 +101,9 @@ class FlashViewModel : BaseViewModel() {
|
|||||||
|
|
||||||
private fun savePressed() = withExternalRW {
|
private fun savePressed() = withExternalRW {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val name = "magisk_install_log_%s.log".format(now.toTime(timeFormatStandard))
|
val name = "magisk_install_log_%s.log".format(
|
||||||
|
System.currentTimeMillis().toTime(timeFormatStandard)
|
||||||
|
)
|
||||||
val file = MediaStoreUtils.getFile(name, true)
|
val file = MediaStoreUtils.getFile(name, true)
|
||||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||||
synchronized(logItems) {
|
synchronized(logItems) {
|
||||||
|
@@ -4,42 +4,63 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.databinding.RvItem
|
import com.topjohnwu.magisk.databinding.RvItem
|
||||||
|
|
||||||
sealed class DeveloperItem {
|
interface Dev {
|
||||||
|
|
||||||
abstract val items: List<IconLink>
|
|
||||||
abstract val name: Int
|
|
||||||
|
|
||||||
object Main : DeveloperItem() {
|
|
||||||
override val items =
|
|
||||||
listOf(
|
|
||||||
IconLink.Twitter.Main,
|
|
||||||
IconLink.Patreon,
|
|
||||||
IconLink.PayPal.Main,
|
|
||||||
IconLink.Github
|
|
||||||
)
|
|
||||||
override val name get() = R.string.topjohnwu
|
|
||||||
}
|
|
||||||
|
|
||||||
object App : DeveloperItem() {
|
|
||||||
override val items =
|
|
||||||
listOf<IconLink>(
|
|
||||||
IconLink.Twitter.App,
|
|
||||||
IconLink.PayPal.App
|
|
||||||
)
|
|
||||||
override val name get() = R.string.diareuse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface Dev {
|
|
||||||
val name: String
|
val name: String
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface MainDev: Dev {
|
private interface JohnImpl : Dev {
|
||||||
override val name get() = "topjohnwu"
|
override val name get() = "topjohnwu"
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface AppDev: Dev {
|
private interface VvbImpl : Dev {
|
||||||
override val name get() = "diareuse"
|
override val name get() = "vvb2060"
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface YUImpl : Dev {
|
||||||
|
override val name get() = "yujincheng08"
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface RikkaImpl : Dev {
|
||||||
|
override val name get() = "RikkaW"
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DeveloperItem : Dev {
|
||||||
|
|
||||||
|
abstract val items: List<IconLink>
|
||||||
|
val handle get() = "@${name}"
|
||||||
|
|
||||||
|
object John : DeveloperItem(), JohnImpl {
|
||||||
|
override val items =
|
||||||
|
listOf(
|
||||||
|
object : IconLink.Twitter(), JohnImpl {},
|
||||||
|
IconLink.Github.Project
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Vvb : DeveloperItem(), VvbImpl {
|
||||||
|
override val items =
|
||||||
|
listOf<IconLink>(
|
||||||
|
object : IconLink.Twitter(), VvbImpl {},
|
||||||
|
object : IconLink.Github.User(), VvbImpl {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object YU : DeveloperItem(), YUImpl {
|
||||||
|
override val items =
|
||||||
|
listOf<IconLink>(
|
||||||
|
object : IconLink.Twitter() { override val name = "shanasaimoe" },
|
||||||
|
object : IconLink.Github.User(), YUImpl {},
|
||||||
|
object : IconLink.Sponsor(), YUImpl {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
object Rikka : DeveloperItem(), RikkaImpl {
|
||||||
|
override val items =
|
||||||
|
listOf<IconLink>(
|
||||||
|
object : IconLink.Twitter() { override val name = "rikkawww" },
|
||||||
|
object : IconLink.Github.User(), RikkaImpl {}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class IconLink : RvItem() {
|
sealed class IconLink : RvItem() {
|
||||||
@@ -50,14 +71,12 @@ sealed class IconLink : RvItem() {
|
|||||||
|
|
||||||
override val layoutRes get() = R.layout.item_icon_link
|
override val layoutRes get() = R.layout.item_icon_link
|
||||||
|
|
||||||
sealed class PayPal : IconLink(), Dev {
|
abstract class PayPal : IconLink(), Dev {
|
||||||
override val icon get() = R.drawable.ic_paypal
|
override val icon get() = R.drawable.ic_paypal
|
||||||
override val title get() = R.string.paypal
|
override val title get() = R.string.paypal
|
||||||
override val link get() = "https://paypal.me/$name"
|
override val link get() = "https://paypal.me/$name"
|
||||||
|
|
||||||
object App : PayPal(), AppDev
|
object Project : PayPal() {
|
||||||
|
|
||||||
object Main : PayPal() {
|
|
||||||
override val name: String get() = "magiskdonate"
|
override val name: String get() = "magiskdonate"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,19 +87,28 @@ sealed class IconLink : RvItem() {
|
|||||||
override val link get() = Const.Url.PATREON_URL
|
override val link get() = Const.Url.PATREON_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Twitter : IconLink(), Dev {
|
abstract class Twitter : IconLink(), Dev {
|
||||||
override val icon get() = R.drawable.ic_twitter
|
override val icon get() = R.drawable.ic_twitter
|
||||||
override val title get() = R.string.twitter
|
override val title get() = R.string.twitter
|
||||||
override val link get() = "https://twitter.com/$name"
|
override val link get() = "https://twitter.com/$name"
|
||||||
|
|
||||||
object App : Twitter(), AppDev
|
|
||||||
|
|
||||||
object Main : Twitter(), MainDev
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Github : IconLink() {
|
abstract class Github : IconLink() {
|
||||||
override val icon get() = R.drawable.ic_github
|
override val icon get() = R.drawable.ic_github
|
||||||
override val title get() = R.string.home_item_source
|
override val title get() = R.string.github
|
||||||
|
|
||||||
|
abstract class User : Github(), Dev {
|
||||||
|
override val link get() = "https://github.com/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
object Project : Github() {
|
||||||
override val link get() = Const.Url.SOURCE_CODE_URL
|
override val link get() = Const.Url.SOURCE_CODE_URL
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Sponsor : IconLink(), Dev {
|
||||||
|
override val icon get() = R.drawable.ic_favorite
|
||||||
|
override val title get() = R.string.github
|
||||||
|
override val link get() = "https://github.com/sponsors/$name"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,11 +6,11 @@ import android.widget.ImageView
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseFragment
|
import com.topjohnwu.magisk.arch.BaseFragment
|
||||||
|
import com.topjohnwu.magisk.arch.viewModel
|
||||||
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.DownloadService
|
import com.topjohnwu.magisk.core.download.DownloadService
|
||||||
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
import com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
|
||||||
import com.topjohnwu.magisk.di.viewModel
|
|
||||||
import com.topjohnwu.magisk.events.RebootEvent
|
import com.topjohnwu.magisk.events.RebootEvent
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
|
|
||||||
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>() {
|
class HomeFragment : BaseFragment<FragmentHomeMd2Binding>() {
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class HomeFragment : BaseFragment<FragmentHomeMd2Binding>() {
|
|||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
inflater.inflate(R.menu.menu_home_md2, menu)
|
inflater.inflate(R.menu.menu_home_md2, menu)
|
||||||
if (!Shell.rootAccess())
|
if (!Info.isRooted)
|
||||||
menu.removeItem(R.id.action_reboot)
|
menu.removeItem(R.id.action_reboot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@ package com.topjohnwu.magisk.ui.home
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import androidx.lifecycle.viewModelScope
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.*
|
import com.topjohnwu.magisk.arch.*
|
||||||
@@ -11,8 +11,8 @@ import com.topjohnwu.magisk.core.Config
|
|||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.core.download.Subject
|
import com.topjohnwu.magisk.core.download.Subject
|
||||||
import com.topjohnwu.magisk.core.download.Subject.App
|
import com.topjohnwu.magisk.core.download.Subject.App
|
||||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||||
import com.topjohnwu.magisk.databinding.itemBindingOf
|
import com.topjohnwu.magisk.databinding.bindExtra
|
||||||
import com.topjohnwu.magisk.databinding.set
|
import com.topjohnwu.magisk.databinding.set
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
||||||
@@ -22,22 +22,18 @@ import com.topjohnwu.magisk.ktx.await
|
|||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.utils.asText
|
import com.topjohnwu.magisk.utils.asText
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.tatarka.bindingcollectionadapter2.BR
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
enum class MagiskState {
|
|
||||||
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
|
|
||||||
}
|
|
||||||
|
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
private val svc: NetworkService
|
private val svc: NetworkService
|
||||||
) : BaseViewModel() {
|
) : AsyncLoadViewModel() {
|
||||||
|
|
||||||
|
enum class State {
|
||||||
|
LOADING, INVALID, OUTDATED, UP_TO_DATE
|
||||||
|
}
|
||||||
|
|
||||||
val magiskTitleBarrierIds =
|
val magiskTitleBarrierIds =
|
||||||
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
|
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
|
||||||
val magiskDetailBarrierIds =
|
|
||||||
intArrayOf(R.id.home_magisk_installed_version, R.id.home_device_details_ramdisk)
|
|
||||||
val appTitleBarrierIds =
|
val appTitleBarrierIds =
|
||||||
intArrayOf(R.id.home_manager_icon, R.id.home_manager_title, R.id.home_manager_button)
|
intArrayOf(R.id.home_manager_icon, R.id.home_manager_title, R.id.home_manager_button)
|
||||||
|
|
||||||
@@ -45,21 +41,21 @@ class HomeViewModel(
|
|||||||
var isNoticeVisible = Config.safetyNotice
|
var isNoticeVisible = Config.safetyNotice
|
||||||
set(value) = set(value, field, { field = it }, BR.noticeVisible)
|
set(value) = set(value, field, { field = it }, BR.noticeVisible)
|
||||||
|
|
||||||
val stateMagisk
|
val magiskState
|
||||||
get() = when {
|
get() = when {
|
||||||
!Info.env.isActive -> MagiskState.NOT_INSTALLED
|
!Info.env.isActive -> State.INVALID
|
||||||
Info.env.versionCode < BuildConfig.VERSION_CODE -> MagiskState.OBSOLETE
|
Info.env.versionCode < BuildConfig.VERSION_CODE -> State.OUTDATED
|
||||||
else -> MagiskState.UP_TO_DATE
|
else -> State.UP_TO_DATE
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var stateManager = MagiskState.LOADING
|
var appState = State.LOADING
|
||||||
set(value) = set(value, field, { field = it }, BR.stateManager)
|
set(value) = set(value, field, { field = it }, BR.appState)
|
||||||
|
|
||||||
val magiskInstalledVersion
|
val magiskInstalledVersion
|
||||||
get() = Info.env.run {
|
get() = Info.env.run {
|
||||||
if (isActive)
|
if (isActive)
|
||||||
"$versionString ($versionCode)".asText()
|
("$versionString ($versionCode)" + if (isDebug) " (D)" else "").asText()
|
||||||
else
|
else
|
||||||
R.string.not_available.asText()
|
R.string.not_available.asText()
|
||||||
}
|
}
|
||||||
@@ -70,46 +66,41 @@ class HomeViewModel(
|
|||||||
|
|
||||||
val managerInstalledVersion
|
val managerInstalledVersion
|
||||||
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" +
|
get() = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})" +
|
||||||
Info.stub?.let { " (${it.version})" }.orEmpty()
|
Info.stub?.let { " (${it.version})" }.orEmpty() +
|
||||||
|
if (BuildConfig.DEBUG) " (D)" else ""
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var stateManagerProgress = 0
|
var stateManagerProgress = 0
|
||||||
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
|
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
|
||||||
|
|
||||||
val itemBinding = itemBindingOf<IconLink> {
|
val extraBindings = bindExtra {
|
||||||
it.bindExtra(BR.viewModel, this)
|
it.put(BR.viewModel, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var checkedEnv = false
|
private var checkedEnv = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh() = viewModelScope.launch {
|
override suspend fun doLoadWork() {
|
||||||
state = State.LOADING
|
appState = State.LOADING
|
||||||
Info.getRemote(svc)?.apply {
|
Info.getRemote(svc)?.apply {
|
||||||
state = State.LOADED
|
appState = when {
|
||||||
|
BuildConfig.VERSION_CODE < magisk.versionCode -> State.OUTDATED
|
||||||
stateManager = when {
|
else -> State.UP_TO_DATE
|
||||||
BuildConfig.VERSION_CODE < magisk.versionCode -> MagiskState.OBSOLETE
|
|
||||||
else -> MagiskState.UP_TO_DATE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
|
||||||
managerRemoteVersion =
|
managerRemoteVersion =
|
||||||
"${magisk.version} (${magisk.versionCode}) (${stub.versionCode})".asText()
|
("${magisk.version} (${magisk.versionCode}) (${stub.versionCode})" +
|
||||||
|
if (isDebug) " (D)" else "").asText()
|
||||||
} ?: run {
|
} ?: run {
|
||||||
state = State.LOADING_FAILED
|
appState = State.INVALID
|
||||||
managerRemoteVersion = R.string.not_available.asText()
|
managerRemoteVersion = R.string.not_available.asText()
|
||||||
}
|
}
|
||||||
ensureEnv()
|
ensureEnv()
|
||||||
}
|
}
|
||||||
|
|
||||||
val showTest = false
|
override fun onNetworkChanged(network: Boolean) = startLoading()
|
||||||
|
|
||||||
fun onTestPressed() = object : ViewEvent(), ActivityExecutor {
|
|
||||||
override fun invoke(activity: UIActivity<*>) {
|
|
||||||
/* Entry point to trigger test events within the app */
|
|
||||||
}
|
|
||||||
}.publish()
|
|
||||||
|
|
||||||
fun onProgressUpdate(progress: Float, subject: Subject) {
|
fun onProgressUpdate(progress: Float, subject: Subject) {
|
||||||
if (subject is App)
|
if (subject is App)
|
||||||
@@ -122,14 +113,14 @@ class HomeViewModel(
|
|||||||
|
|
||||||
fun onDeletePressed() = UninstallDialog().publish()
|
fun onDeletePressed() = UninstallDialog().publish()
|
||||||
|
|
||||||
fun onManagerPressed() = when (state) {
|
fun onManagerPressed() = when (appState) {
|
||||||
State.LOADED -> withExternalRW {
|
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
||||||
|
State.INVALID -> SnackbarEvent(R.string.no_connection).publish()
|
||||||
|
else -> withExternalRW {
|
||||||
withInstallPermission {
|
withInstallPermission {
|
||||||
ManagerInstallDialog().publish()
|
ManagerInstallDialog().publish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
|
||||||
else -> SnackbarEvent(R.string.no_connection).publish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onMagiskPressed() = withExternalRW {
|
fun onMagiskPressed() = withExternalRW {
|
||||||
@@ -142,7 +133,7 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun ensureEnv() {
|
private suspend fun ensureEnv() {
|
||||||
if (MagiskState.NOT_INSTALLED == stateMagisk || checkedEnv) return
|
if (magiskState == State.INVALID || checkedEnv) return
|
||||||
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
|
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
|
||||||
if (!Shell.cmd(cmd).await().isSuccess) {
|
if (!Shell.cmd(cmd).await().isSuccess) {
|
||||||
EnvFixDialog(this).publish()
|
EnvFixDialog(this).publish()
|
||||||
@@ -150,4 +141,10 @@ class HomeViewModel(
|
|||||||
checkedEnv = true
|
checkedEnv = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showTest = false
|
||||||
|
fun onTestPressed() = object : ViewEvent(), ActivityExecutor {
|
||||||
|
override fun invoke(activity: UIActivity<*>) {
|
||||||
|
/* Entry point to trigger test events within the app */
|
||||||
|
}
|
||||||
|
}.publish()
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user