mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 13:17:26 +00:00
Compare commits
316 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
59fd38bbf8 | ||
![]() |
06dc6df270 | ||
![]() |
ff8460b361 | ||
![]() |
674d272eaa | ||
![]() |
c3e00c279d | ||
![]() |
175d920c94 | ||
![]() |
04920883ea | ||
![]() |
5e44b0b9d5 | ||
![]() |
23c1a1dab8 | ||
![]() |
f5d054b93c | ||
![]() |
d25ae5e0a9 | ||
![]() |
c42a51dcbb | ||
![]() |
da3fd92b31 | ||
![]() |
4a45ba3c14 | ||
![]() |
dbc8bed234 | ||
![]() |
f8b4190a11 | ||
![]() |
479972e3ae | ||
![]() |
3ea28b0afb | ||
![]() |
2b3cc28966 | ||
![]() |
751642b39a | ||
![]() |
d6c2c821a4 | ||
![]() |
dfc65b95f7 | ||
![]() |
b45d922463 | ||
![]() |
f87ee3fcf9 | ||
![]() |
e0927cd763 | ||
![]() |
21099eabfa | ||
![]() |
abbd2e6b72 | ||
![]() |
5b7ddbbb01 | ||
![]() |
6352fbb3b2 | ||
![]() |
d3f49334e2 | ||
![]() |
c4356171b3 | ||
![]() |
5c5625911d | ||
![]() |
6a10cc9c55 | ||
![]() |
6b317f918e | ||
![]() |
08b528dc4f | ||
![]() |
fc886a5a47 | ||
![]() |
0cb90e2e55 | ||
![]() |
64113a69b4 | ||
![]() |
544bb7459c | ||
![]() |
578a50b464 | ||
![]() |
3d4081d0af | ||
![]() |
b763b81f56 | ||
![]() |
947dae4900 | ||
![]() |
debd1d7d54 | ||
![]() |
cba0d04000 | ||
![]() |
695e7e6da0 | ||
![]() |
4cd4bfa1d7 | ||
![]() |
16b400964b | ||
![]() |
cf2d02c0dd | ||
![]() |
0fcd0de0d1 | ||
![]() |
748a35774f | ||
![]() |
a52a3e38ed | ||
![]() |
ee0cef06a6 | ||
![]() |
0e5a113a0c | ||
![]() |
a1ccd44013 | ||
![]() |
4d91e50d6d | ||
![]() |
120668c7bc | ||
![]() |
d81ccde569 | ||
![]() |
e8581b4adb | ||
![]() |
19906575a3 | ||
![]() |
9329094a4e | ||
![]() |
b44f5122fd | ||
![]() |
17981730a4 | ||
![]() |
53de6da26c | ||
![]() |
3e30ccdeee | ||
![]() |
baaaf7d5de | ||
![]() |
45d8d139a9 | ||
![]() |
fe644e10d0 | ||
![]() |
f383d11d10 | ||
![]() |
ef1b928532 | ||
![]() |
6e46d394b1 | ||
![]() |
f109038d12 | ||
![]() |
e31e687602 | ||
![]() |
86bfb22d4c | ||
![]() |
3f057367e3 | ||
![]() |
3d7ed5820e | ||
![]() |
0118f2efa7 | ||
![]() |
15312e4709 | ||
![]() |
bf1568a73a | ||
![]() |
13a2520ea5 | ||
![]() |
f53238f206 | ||
![]() |
9375748d9b | ||
![]() |
201df54e79 | ||
![]() |
0b54fe477b | ||
![]() |
4119e6669e | ||
![]() |
d33e5226b3 | ||
![]() |
d73f39c706 | ||
![]() |
087b451e17 | ||
![]() |
86481c74ff | ||
![]() |
5b937fb1fa | ||
![]() |
ff828116bc | ||
![]() |
ee39616a8b | ||
![]() |
cdb53ca049 | ||
![]() |
8cf475f708 | ||
![]() |
0cb449e1d6 | ||
![]() |
e6adb7abca | ||
![]() |
cfad7dd317 | ||
![]() |
dd35224f92 | ||
![]() |
1283590eeb | ||
![]() |
dca3fe396f | ||
![]() |
8d87eae11b | ||
![]() |
fd7eaacae0 | ||
![]() |
fba33cbbe9 | ||
![]() |
950ffcd790 | ||
![]() |
c178299013 | ||
![]() |
5d17c1f588 | ||
![]() |
a75c00d94e | ||
![]() |
cd19517414 | ||
![]() |
155f39aab5 | ||
![]() |
4514d0b467 | ||
![]() |
6f4a938a31 | ||
![]() |
1303ea95dd | ||
![]() |
727fe1bd15 | ||
![]() |
64ebc977e9 | ||
![]() |
e89c50d934 | ||
![]() |
c859ddfb8f | ||
![]() |
a6126c5eda | ||
![]() |
85d9bd9106 | ||
![]() |
39e9622205 | ||
![]() |
021994c9f3 | ||
![]() |
2e7ce2a769 | ||
![]() |
84f0ff2fad | ||
![]() |
e6561e5f84 | ||
![]() |
5fa452aa74 | ||
![]() |
2225ccb146 | ||
![]() |
5aafc78847 | ||
![]() |
0d03833cff | ||
![]() |
a797d5d396 | ||
![]() |
f2494374f8 | ||
![]() |
48395ba860 | ||
![]() |
5ba5f5f94e | ||
![]() |
42ce6fd334 | ||
![]() |
f5c3ee3ae1 | ||
![]() |
3c7ece1605 | ||
![]() |
870efc49ea | ||
![]() |
085ede6d93 | ||
![]() |
4ef19d17da | ||
![]() |
223913c30a | ||
![]() |
010e4de4e1 | ||
![]() |
41134466ed | ||
![]() |
8f07747452 | ||
![]() |
eb5ce5be1e | ||
![]() |
71d855e836 | ||
![]() |
33b7ab593c | ||
![]() |
8706d834b4 | ||
![]() |
7cfab33ebb | ||
![]() |
1ababc8c7f | ||
![]() |
1f75e63c37 | ||
![]() |
cb3f9b9740 | ||
![]() |
9784353223 | ||
![]() |
7d93ca5c73 | ||
![]() |
ac20063e86 | ||
![]() |
debaec32af | ||
![]() |
0e9b71e7a9 | ||
![]() |
85f5ff3c14 | ||
![]() |
3d81f167ea | ||
![]() |
fb70a2e52d | ||
![]() |
460e85a1b5 | ||
![]() |
539b64bd57 | ||
![]() |
90e38a06a2 | ||
![]() |
09ab910630 | ||
![]() |
c15f80b33f | ||
![]() |
b2e6ba3c4a | ||
![]() |
b16f696b0e | ||
![]() |
9adfb382e8 | ||
![]() |
44368383f4 | ||
![]() |
d1ff7e0ffe | ||
![]() |
42e7db8d13 | ||
![]() |
0c17ea5755 | ||
![]() |
cdaff5b39c | ||
![]() |
2b1b970e78 | ||
![]() |
0aebc0a8e3 | ||
![]() |
c3a89f589e | ||
![]() |
971cd73fb3 | ||
![]() |
1947860d61 | ||
![]() |
55aaa421e8 | ||
![]() |
a8932706d8 | ||
![]() |
a97972aac0 | ||
![]() |
094c3d559a | ||
![]() |
6fb032b3c2 | ||
![]() |
8ca188f4d4 | ||
![]() |
746a1d8d59 | ||
![]() |
63c5e00d86 | ||
![]() |
9d2e5d6665 | ||
![]() |
f6045bf8b5 | ||
![]() |
e83f40d5c5 | ||
![]() |
e5118418b2 | ||
![]() |
7cd814d917 | ||
![]() |
78282c1a49 | ||
![]() |
fd4214ccf3 | ||
![]() |
0785945635 | ||
![]() |
967bdeae7b | ||
![]() |
452db51669 | ||
![]() |
5875ced367 | ||
![]() |
fbac6bcfd0 | ||
![]() |
0dcd3ece9d | ||
![]() |
224fff89e3 | ||
![]() |
22e73644f9 | ||
![]() |
6a0f6ab319 | ||
![]() |
88a394836f | ||
![]() |
f822c1c2e4 | ||
![]() |
1d16d980b3 | ||
![]() |
501b18f986 | ||
![]() |
21ed759e53 | ||
![]() |
8d50dfd93c | ||
![]() |
51e40dd98c | ||
![]() |
b2048379af | ||
![]() |
011539f6f1 | ||
![]() |
5457c3803f | ||
![]() |
b3d777bb6c | ||
![]() |
12e00c3054 | ||
![]() |
40b683111c | ||
![]() |
9542ca773f | ||
![]() |
8af832a496 | ||
![]() |
6836130fda | ||
![]() |
724893879f | ||
![]() |
736729f5ef | ||
![]() |
aa47966347 | ||
![]() |
d64d12afe8 | ||
![]() |
1f8df419c4 | ||
![]() |
7ba8202af5 | ||
![]() |
d7b691cf59 | ||
![]() |
7058d5e4cd | ||
![]() |
52fd508fea | ||
![]() |
41045b62dc | ||
![]() |
188ea2644a | ||
![]() |
4c8f357978 | ||
![]() |
4bb2fd6ba6 | ||
![]() |
33c9f74508 | ||
![]() |
f53fe67372 | ||
![]() |
51ff724691 | ||
![]() |
291bf93f9d | ||
![]() |
5fcd629f16 | ||
![]() |
ab90901793 | ||
![]() |
4f206fd918 | ||
![]() |
7233285437 | ||
![]() |
8e348a11c2 | ||
![]() |
085ea6d0a1 | ||
![]() |
aaf88b1895 | ||
![]() |
4f4a9412a3 | ||
![]() |
a92e039363 | ||
![]() |
33aa4ca4b7 | ||
![]() |
05658cafc7 | ||
![]() |
ff3710de66 | ||
![]() |
db8dd9f186 | ||
![]() |
e8b73ba6d1 | ||
![]() |
f1112fdf37 | ||
![]() |
a48c4f9e05 | ||
![]() |
19a521d2e9 | ||
![]() |
dd6e55ac31 | ||
![]() |
b1e63f0f14 | ||
![]() |
b0e49a4cc8 | ||
![]() |
1e94517a72 | ||
![]() |
98f60216ac | ||
![]() |
e29b712108 | ||
![]() |
a462435f2f | ||
![]() |
911b8273fe | ||
![]() |
09935e591a | ||
![]() |
4a212dba35 | ||
![]() |
aac9e85e04 | ||
![]() |
bb67a837d3 | ||
![]() |
6cde695194 | ||
![]() |
a1a1ac0bbb | ||
![]() |
9ec8bc2166 | ||
![]() |
28cd6a75e7 | ||
![]() |
4cc7aced15 | ||
![]() |
1058aeb04f | ||
![]() |
cfec0db947 | ||
![]() |
120bd6cd68 | ||
![]() |
9aef06d1b8 | ||
![]() |
e6e9dd751c | ||
![]() |
5dd677756f | ||
![]() |
b77c590910 | ||
![]() |
7e5f2822ae | ||
![]() |
12bbc7fd6b | ||
![]() |
bf9ac8252b | ||
![]() |
4a3f5dc619 | ||
![]() |
ca156befbd | ||
![]() |
4db41e2ac4 | ||
![]() |
982a43fce1 | ||
![]() |
dd76a74e1c | ||
![]() |
70cb52b2c7 | ||
![]() |
5c7f69acaa | ||
![]() |
f1d9015e5f | ||
![]() |
e8d900c58e | ||
![]() |
a6241ae912 | ||
![]() |
4a697ca2ec | ||
![]() |
58bec7f2c9 | ||
![]() |
213f84985c | ||
![]() |
074b1f8c61 | ||
![]() |
326eee8c83 | ||
![]() |
00bff4912e | ||
![]() |
0ce1720516 | ||
![]() |
ee407472cf | ||
![]() |
f341f3b2dd | ||
![]() |
8513946e09 | ||
![]() |
8ebd9c8927 | ||
![]() |
1d54c5144e | ||
![]() |
e40d4318fa | ||
![]() |
7756e10779 | ||
![]() |
3e58d502d0 | ||
![]() |
1c8846dc57 | ||
![]() |
2f320c7239 | ||
![]() |
e799918ab6 | ||
![]() |
86c4928e0f | ||
![]() |
0293eb5c51 | ||
![]() |
1ee75b6aa6 | ||
![]() |
4b30b224b5 | ||
![]() |
16b232d2a3 | ||
![]() |
3f3b1f5b1d | ||
![]() |
cec017b7bf | ||
![]() |
3123cc1059 | ||
![]() |
caa9df86bc | ||
![]() |
f417389a7a | ||
![]() |
662a5c8ea6 | ||
![]() |
7edfbfb764 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -11,7 +11,7 @@
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
chromeos/** binary
|
||||
tools/** binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -19,3 +19,9 @@
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
@@ -10,7 +10,7 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that is already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by opening an issue on GitHub or directly in the thread.
|
||||
**Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that are already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by [opening an issue on GitHub](https://github.com/topjohnwu/Magisk/issues) or directly in the thread.
|
||||
|
||||
## Building Environment Requirements
|
||||
|
||||
|
@@ -17,8 +17,8 @@ android {
|
||||
applicationId 'com.topjohnwu.magisk'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled true
|
||||
versionName configProps['appVersion']
|
||||
versionCode configProps['appVersionCode'] as Integer
|
||||
versionName props['appVersion']
|
||||
versionCode props['appVersionCode'] as Integer
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -39,10 +39,14 @@ android {
|
||||
exclude '/META-INF/*.kotlin_module'
|
||||
exclude '/META-INF/rxkotlin.properties'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/**'
|
||||
exclude '/org/bouncycastle/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
@@ -51,21 +55,33 @@ androidExtensions {
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':net')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
|
||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.github.skoumalcz:teanity:0.3.3'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||
|
||||
def vMarkwon = '3.0.1'
|
||||
implementation "ru.noties.markwon:core:${vMarkwon}"
|
||||
implementation "ru.noties.markwon:html:${vMarkwon}"
|
||||
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
def vLibsu = '2.5.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}"
|
||||
|
||||
def vBAdapt = '3.1.1'
|
||||
def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter'
|
||||
implementation "${bindingAdapter}:${vBAdapt}"
|
||||
implementation "${bindingAdapter}-recyclerview:${vBAdapt}"
|
||||
|
||||
def vMarkwon = '4.1.1'
|
||||
implementation "io.noties.markwon:core:${vMarkwon}"
|
||||
implementation "io.noties.markwon:html:${vMarkwon}"
|
||||
implementation "io.noties.markwon:image:${vMarkwon}"
|
||||
implementation 'com.caverock:androidsvg:1.4'
|
||||
|
||||
def vLibsu = '2.5.1'
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
@@ -74,12 +90,13 @@ dependencies {
|
||||
implementation "org.koin:koin-android:${vKoin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||
|
||||
def vRetrofit = "2.5.0"
|
||||
def vRetrofit = '2.6.2'
|
||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||
|
||||
def vOkHttp = "3.12.0"
|
||||
def vOkHttp = '3.12.6'
|
||||
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
@@ -90,13 +107,27 @@ dependencies {
|
||||
implementation "se.ansman.kotshi:api:${vKotshi}"
|
||||
kapt "se.ansman.kotshi:compiler:${vKotshi}"
|
||||
|
||||
modules {
|
||||
module('androidx.room:room-runtime') {
|
||||
replacedBy('com.github.topjohnwu:room-runtime')
|
||||
}
|
||||
}
|
||||
def vRoom = "2.2.0"
|
||||
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
||||
kapt "androidx.room:room-compiler:${vRoom}"
|
||||
|
||||
def vNav = "2.1.0"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$vNav"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$vNav"
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta05'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.0.1'
|
||||
implementation 'androidx.transition:transition:1.2.0-alpha01'
|
||||
implementation 'androidx.work:work-runtime:2.2.0'
|
||||
implementation 'androidx.transition:transition:1.2.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha06'
|
||||
implementation 'androidx.core:core-ktx:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0-beta01'
|
||||
}
|
||||
|
14
app/proguard-rules.pro
vendored
14
app/proguard-rules.pro
vendored
@@ -16,13 +16,10 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Retrofit classes
|
||||
-keep,allowobfuscation class com.topjohnwu.magisk.data.network.*
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback {
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback {
|
||||
void onResponse(int);
|
||||
}
|
||||
|
||||
@@ -32,16 +29,13 @@
|
||||
}
|
||||
|
||||
# DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
|
||||
|
||||
# BootSigner
|
||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
||||
public *** debug(...);
|
||||
}
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
|
@@ -3,14 +3,15 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name="a.e"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/MagiskTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
@@ -41,9 +42,9 @@
|
||||
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:exported="false"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:theme="@style/MagiskTheme.SU" />
|
||||
|
||||
<!-- Receiver -->
|
||||
@@ -65,7 +66,8 @@
|
||||
|
||||
<!-- Service -->
|
||||
|
||||
<service android:name="a.j" />
|
||||
<service android:name="a.j"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
<meta-data
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
import com.topjohnwu.magisk.model.download.DownloadService;
|
||||
|
||||
public class j extends DownloadModuleService {
|
||||
public class j extends DownloadService {
|
||||
/* stub */
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.topjohnwu.magisk.base.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
|
||||
/* Wrapper class to workaround Proguard -keep class * extends Worker */
|
||||
@@ -22,7 +22,7 @@ public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
try {
|
||||
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
|
||||
.getActualTypeArguments()[0]).newInstance();
|
||||
base.setActualWorker(this);
|
||||
base.attachWorker(this);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
|
@@ -1,32 +1,41 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.room.Room
|
||||
import androidx.work.impl.WorkDatabase
|
||||
import androidx.work.impl.WorkDatabase_Impl
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
||||
import com.topjohnwu.magisk.di.ActivityTracker
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
|
||||
open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
open class App : Application() {
|
||||
|
||||
lateinit var protectedContext: Context
|
||||
|
||||
@Volatile
|
||||
private var foreground: Activity? = null
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootUtils::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
RepoDatabase::class.java -> RepoDatabase_Impl()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
@@ -39,19 +48,7 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
modules(koinModules)
|
||||
}
|
||||
|
||||
protectedContext = baseContext
|
||||
self = this
|
||||
deContext = base
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
protectedContext = base.createDeviceProtectedStorageContext()
|
||||
deContext = protectedContext
|
||||
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
|
||||
}
|
||||
|
||||
registerActivityLifecycleCallbacks(this)
|
||||
|
||||
Networking.init(base)
|
||||
registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
@@ -59,61 +56,4 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
//region ActivityLifecycleCallbacks
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
//endregion
|
||||
|
||||
private val Context.defaultPrefsName get() = "${packageName}_preferences"
|
||||
|
||||
companion object {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection")
|
||||
@JvmStatic
|
||||
lateinit var self: App
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection; replace with protectedContext")
|
||||
@JvmStatic
|
||||
lateinit var deContext: Context
|
||||
|
||||
@Deprecated("Use Rx or similar")
|
||||
@JvmField
|
||||
var THREAD_POOL: ThreadPoolExecutor
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootUtils::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
@JvmStatic
|
||||
fun foreground(): Activity? {
|
||||
val app: App by inject()
|
||||
return app.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
@@ -16,12 +16,11 @@ object ClassMap {
|
||||
FlashActivity::class.java to a.f::class.java,
|
||||
UpdateCheckService::class.java to a.g::class.java,
|
||||
GeneralReceiver::class.java to a.h::class.java,
|
||||
DownloadModuleService::class.java to a.j::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
operator fun get(c: Class<*>): Class<*>? {
|
||||
return map.getOrElse(c) { null } //as? Class<T>
|
||||
operator fun <T : Class<*>>get(c: Class<*>): T {
|
||||
return map.getOrElse(c) { throw IllegalArgumentException() } as T
|
||||
}
|
||||
}
|
||||
|
@@ -1,399 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import static com.topjohnwu.magisk.ConfigLeanback.getPrefs;
|
||||
import static com.topjohnwu.magisk.utils.XAndroidKt.getPackageName;
|
||||
|
||||
public final class Config {
|
||||
|
||||
private static final ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
public static int magiskVersionCode = -1;
|
||||
// Current status
|
||||
public static String magiskVersionString = "";
|
||||
// Update Info
|
||||
public static String remoteMagiskVersionString = "";
|
||||
public static int remoteMagiskVersionCode = -1;
|
||||
public static String magiskLink = "";
|
||||
public static String magiskNoteLink = "";
|
||||
public static String magiskMD5 = "";
|
||||
public static String remoteManagerVersionString = "";
|
||||
public static int remoteManagerVersionCode = -1;
|
||||
public static String managerLink = "";
|
||||
public static String managerNoteLink = "";
|
||||
public static String uninstallerLink = "";
|
||||
|
||||
// Install flags
|
||||
public static boolean keepVerity = false;
|
||||
public static boolean keepEnc = false;
|
||||
public static boolean recovery = false;
|
||||
|
||||
public static int suLogTimeout = 14;
|
||||
|
||||
public static class Key {
|
||||
// su configs
|
||||
public static final String ROOT_ACCESS = "root_access";
|
||||
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
|
||||
public static final String SU_MNT_NS = "mnt_ns";
|
||||
public static final String SU_MANAGER = "requester";
|
||||
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
|
||||
public static final String SU_AUTO_RESPONSE = "su_auto_response";
|
||||
public static final String SU_NOTIFICATION = "su_notification";
|
||||
public static final String SU_REAUTH = "su_reauth";
|
||||
public static final String SU_FINGERPRINT = "su_fingerprint";
|
||||
|
||||
// prefs
|
||||
public static final String CHECK_UPDATES = "check_update";
|
||||
public static final String UPDATE_CHANNEL = "update_channel";
|
||||
public static final String CUSTOM_CHANNEL = "custom_channel";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DARK_THEME = "dark_theme";
|
||||
public static final String ETAG_KEY = "ETag";
|
||||
public static final String REPO_ORDER = "repo_order";
|
||||
public static final String SHOW_SYSTEM_APP = "show_system";
|
||||
|
||||
// system state
|
||||
public static final String UPDATE_SERVICE_VER = "update_service_version";
|
||||
public static final String MAGISKHIDE = "magiskhide";
|
||||
public static final String COREONLY = "disable";
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public static final int DEFAULT_CHANNEL = -1;
|
||||
public static final int STABLE_CHANNEL = 0;
|
||||
public static final int BETA_CHANNEL = 1;
|
||||
public static final int CUSTOM_CHANNEL = 2;
|
||||
public static final int CANARY_CHANNEL = 3;
|
||||
public static final int CANARY_DEBUG_CHANNEL = 4;
|
||||
public static final int ROOT_ACCESS_DISABLED = 0;
|
||||
public static final int ROOT_ACCESS_APPS_ONLY = 1;
|
||||
public static final int ROOT_ACCESS_ADB_ONLY = 2;
|
||||
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
|
||||
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
|
||||
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
|
||||
public static final int MULTIUSER_MODE_USER = 2;
|
||||
public static final int NAMESPACE_MODE_GLOBAL = 0;
|
||||
public static final int NAMESPACE_MODE_REQUESTER = 1;
|
||||
public static final int NAMESPACE_MODE_ISOLATE = 2;
|
||||
public static final int NO_NOTIFICATION = 0;
|
||||
public static final int NOTIFICATION_TOAST = 1;
|
||||
public static final int SU_PROMPT = 0;
|
||||
public static final int SU_AUTO_DENY = 1;
|
||||
public static final int SU_AUTO_ALLOW = 2;
|
||||
public static final int[] TIMEOUT_LIST = {0, -1, 10, 20, 30, 60};
|
||||
public static final int ORDER_NAME = 0;
|
||||
public static final int ORDER_DATE = 1;
|
||||
}
|
||||
|
||||
private static boolean magiskHide = false;
|
||||
|
||||
public static void loadMagiskInfo() {
|
||||
try {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
||||
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static void export() {
|
||||
// Flush prefs to disk
|
||||
getPrefs().edit().apply();
|
||||
Context context = ConfigLeanback.getProtectedContext();
|
||||
File xml = new File(context.getFilesDir().getParent() + "/shared_prefs",
|
||||
getPackageName() + "_preferences.xml");
|
||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||
}
|
||||
|
||||
private static final int PREF_INT = 0;
|
||||
private static final int PREF_STR_INT = 1;
|
||||
private static final int PREF_BOOL = 2;
|
||||
private static final int PREF_STR = 3;
|
||||
private static final int DB_INT = 4;
|
||||
private static final int DB_BOOL = 5;
|
||||
private static final int DB_STR = 6;
|
||||
|
||||
private static int getConfigType(String key) {
|
||||
switch (key) {
|
||||
case Key.REPO_ORDER:
|
||||
return PREF_INT;
|
||||
|
||||
case Key.SU_REQUEST_TIMEOUT:
|
||||
case Key.SU_AUTO_RESPONSE:
|
||||
case Key.SU_NOTIFICATION:
|
||||
case Key.UPDATE_CHANNEL:
|
||||
return PREF_STR_INT;
|
||||
|
||||
case Key.DARK_THEME:
|
||||
case Key.SU_REAUTH:
|
||||
case Key.CHECK_UPDATES:
|
||||
case Key.MAGISKHIDE:
|
||||
case Key.COREONLY:
|
||||
case Key.SHOW_SYSTEM_APP:
|
||||
return PREF_BOOL;
|
||||
|
||||
case Key.CUSTOM_CHANNEL:
|
||||
case Key.LOCALE:
|
||||
case Key.ETAG_KEY:
|
||||
return PREF_STR;
|
||||
|
||||
case Key.ROOT_ACCESS:
|
||||
case Key.SU_MNT_NS:
|
||||
case Key.SU_MULTIUSER_MODE:
|
||||
return DB_INT;
|
||||
|
||||
case Key.SU_FINGERPRINT:
|
||||
return DB_BOOL;
|
||||
|
||||
case Key.SU_MANAGER:
|
||||
return DB_STR;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
SharedPreferences pref = getPrefs();
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
||||
if (config.exists()) {
|
||||
try {
|
||||
SuFileInputStream is = new SuFileInputStream(config);
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(is, "UTF-8");
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG)
|
||||
continue;
|
||||
String key = parser.getAttributeValue(null, "name");
|
||||
String value = parser.getAttributeValue(null, "value");
|
||||
switch (parser.getName()) {
|
||||
case "string":
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
editor.putString(key, parser.nextText());
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
break;
|
||||
case "boolean":
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean");
|
||||
editor.putBoolean(key, Boolean.parseBoolean(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean");
|
||||
break;
|
||||
case "int":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putInt(key, Integer.parseInt(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
case "long":
|
||||
parser.require(XmlPullParser.START_TAG, null, "long");
|
||||
editor.putLong(key, Long.parseLong(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "long");
|
||||
break;
|
||||
case "float":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putFloat(key, Float.parseFloat(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
default:
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
editor.remove(Key.ETAG_KEY);
|
||||
editor.apply();
|
||||
editor = pref.edit();
|
||||
config.delete();
|
||||
}
|
||||
|
||||
// Set defaults if not set
|
||||
setDefs(pref, editor);
|
||||
|
||||
// These settings are from actual device state
|
||||
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
|
||||
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T get(String key) {
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
return (T) (Integer) getPrefs().getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(getPrefs(), key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) getPrefs().getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) getPrefs().getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) ConfigLeanback.get(key, (Integer) getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (ConfigLeanback.get(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) ConfigLeanback.get(key, getDef(key));
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void set(String key, Object val) {
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
getPrefs().edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
getPrefs().edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
getPrefs().edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
getPrefs().edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
ConfigLeanback.put(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
ConfigLeanback.put(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
ConfigLeanback.put(key, (String) val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(String key) {
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case PREF_STR_INT:
|
||||
case PREF_BOOL:
|
||||
case PREF_STR:
|
||||
getPrefs().edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
ConfigLeanback.delete(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
ConfigLeanback.put(key, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
/* Set default configurations */
|
||||
|
||||
// prefs int
|
||||
defs.put(Key.REPO_ORDER, Value.ORDER_DATE);
|
||||
|
||||
// prefs string int
|
||||
defs.put(Key.SU_REQUEST_TIMEOUT, 10);
|
||||
defs.put(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
|
||||
defs.put(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
|
||||
defs.put(Key.UPDATE_CHANNEL, Utils.isCanary() ?
|
||||
Value.CANARY_DEBUG_CHANNEL : Value.DEFAULT_CHANNEL);
|
||||
|
||||
// prefs bool
|
||||
defs.put(Key.CHECK_UPDATES, true);
|
||||
defs.put(Key.DARK_THEME, true);
|
||||
//defs.put(Key.SU_REAUTH, false);
|
||||
//defs.put(Key.SHOW_SYSTEM_APP, false);
|
||||
|
||||
// prefs string
|
||||
defs.put(Key.CUSTOM_CHANNEL, "");
|
||||
defs.put(Key.LOCALE, "");
|
||||
//defs.put(Key.ETAG_KEY, null);
|
||||
|
||||
// db int
|
||||
defs.put(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
|
||||
defs.put(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
|
||||
defs.put(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
|
||||
|
||||
// db bool
|
||||
//defs.put(Key.SU_FINGERPRINT, false);
|
||||
|
||||
// db strings
|
||||
//defs.put(Key.SU_MANAGER, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private static <T> T getDef(String key) throws IllegalArgumentException {
|
||||
Object val = defs.get(key);
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case DB_INT:
|
||||
case PREF_STR_INT:
|
||||
return val != null ? (T) val : (T) (Integer) 0;
|
||||
case DB_BOOL:
|
||||
case PREF_BOOL:
|
||||
return val != null ? (T) val : (T) (Boolean) false;
|
||||
case DB_STR:
|
||||
case PREF_STR:
|
||||
return (T) val;
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
|
||||
for (String key : defs.keySet()) {
|
||||
Object value = defs.get(key);
|
||||
int type = getConfigType(key);
|
||||
switch (type) {
|
||||
case DB_INT:
|
||||
editor.putString(key, String.valueOf(ConfigLeanback.get(key, (Integer) value)));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, ConfigLeanback.get(key, String.valueOf(value)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = ConfigLeanback.get(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) value : bs != 0);
|
||||
continue;
|
||||
}
|
||||
if (pref.contains(key))
|
||||
continue;
|
||||
switch (type) {
|
||||
case PREF_INT:
|
||||
editor.putInt(key, (Integer) value);
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
editor.putString(key, String.valueOf(value));
|
||||
break;
|
||||
case PREF_STR:
|
||||
editor.putString(key, (String) value);
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
editor.putBoolean(key, (Boolean) value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
208
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
208
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
@@ -0,0 +1,208 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.util.Xml
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
|
||||
override val stringDao: StringDao by inject()
|
||||
override val settingsDao: SettingsDao by inject()
|
||||
override val context: Context by inject(Protected)
|
||||
|
||||
object Key {
|
||||
// db configs
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
// prefs
|
||||
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val CHECK_UPDATES = "check_update"
|
||||
const val UPDATE_CHANNEL = "update_channel"
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
const val REPO_ORDER = "repo_order"
|
||||
const val SHOW_SYSTEM_APP = "show_system"
|
||||
const val DOWNLOAD_PATH = "download_path"
|
||||
|
||||
// system state
|
||||
const val MAGISKHIDE = "magiskhide"
|
||||
const val COREONLY = "disable"
|
||||
}
|
||||
|
||||
object Value {
|
||||
// Update channels
|
||||
const val DEFAULT_CHANNEL = -1
|
||||
const val STABLE_CHANNEL = 0
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val CANARY_DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
const val ROOT_ACCESS_APPS_ONLY = 1
|
||||
const val ROOT_ACCESS_ADB_ONLY = 2
|
||||
const val ROOT_ACCESS_APPS_AND_ADB = 3
|
||||
|
||||
// su multiuser
|
||||
const val MULTIUSER_MODE_OWNER_ONLY = 0
|
||||
const val MULTIUSER_MODE_OWNER_MANAGED = 1
|
||||
const val MULTIUSER_MODE_USER = 2
|
||||
|
||||
// su mnt ns
|
||||
const val NAMESPACE_MODE_GLOBAL = 0
|
||||
const val NAMESPACE_MODE_REQUESTER = 1
|
||||
const val NAMESPACE_MODE_ISOLATE = 2
|
||||
|
||||
// su notification
|
||||
const val NO_NOTIFICATION = 0
|
||||
const val NOTIFICATION_TOAST = 1
|
||||
|
||||
// su auto response
|
||||
const val SU_PROMPT = 0
|
||||
const val SU_AUTO_DENY = 1
|
||||
const val SU_AUTO_ALLOW = 2
|
||||
|
||||
// su timeout
|
||||
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
|
||||
|
||||
// repo order
|
||||
const val ORDER_NAME = 0
|
||||
const val ORDER_DATE = 1
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
|
||||
else Value.DEFAULT_CHANNEL
|
||||
|
||||
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
|
||||
var darkTheme by preference(Key.DARK_THEME, true)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
var magiskHide by preference(Key.MAGISKHIDE, true)
|
||||
var coreOnly by preference(Key.COREONLY, false)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
|
||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||
var locale by preference(Key.LOCALE, "")
|
||||
|
||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
|
||||
// Always return a path in external storage where we can write
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
fun initialize() = prefs.edit {
|
||||
parsePrefs(this)
|
||||
|
||||
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||
|
||||
// Get actual state
|
||||
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
|
||||
// Write database configs
|
||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||
}
|
||||
|
||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config).buffered()
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
fun value() = parser.getAttributeValue(null, "value")!!
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value().toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value().toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value().toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value().toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
config.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().commit()
|
||||
val xml = File(
|
||||
"${get<Context>(Protected).filesDir.parent}/shared_prefs",
|
||||
"${packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
}
|
@@ -1,52 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.data.repository.SettingRepository
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
|
||||
object ConfigLeanback {
|
||||
|
||||
@JvmStatic
|
||||
val protectedContext: Context by inject(Protected)
|
||||
@JvmStatic
|
||||
val prefs: SharedPreferences by inject()
|
||||
|
||||
private val settingRepo: SettingRepository by inject()
|
||||
private val stringRepo: StringRepository by inject()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun put(key: String, value: Int) {
|
||||
settingRepo.put(key, value).subscribeK()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun get(key: String, defaultValue: Int): Int =
|
||||
settingRepo.fetch(key, defaultValue).blockingGet()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun put(key: String, value: String?) {
|
||||
val task = value?.let { stringRepo.put(key, it) } ?: stringRepo.delete(key)
|
||||
task.subscribeK()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun get(key: String, defaultValue: String?): String =
|
||||
stringRepo.fetch(key, defaultValue.orEmpty()).blockingGet()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun delete(key: String) {
|
||||
settingRepo.delete(key).subscribeK()
|
||||
}
|
||||
|
||||
}
|
@@ -1,44 +1,26 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Environment
|
||||
import android.os.Process
|
||||
|
||||
import java.io.File
|
||||
|
||||
object Const {
|
||||
|
||||
const val DEBUG_TAG = "MagiskManager"
|
||||
|
||||
// APK content
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
const val SU_KEYSTORE_KEY = "su_key"
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
@JvmField
|
||||
val EXTERNAL_PATH: File =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
@JvmField
|
||||
var MAGISK_DISABLE_FILE: File = File("xxx")
|
||||
|
||||
var MAGISK_DISABLE_FILE = File("xxx")
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
|
||||
// Versions
|
||||
const val UPDATE_SERVICE_VER = 1
|
||||
const val SNET_EXT_VER = 12
|
||||
const val SNET_EXT_VER = 13
|
||||
const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||
|
||||
@JvmField
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
// Generic
|
||||
// Misc
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
|
||||
init {
|
||||
EXTERNAL_PATH.mkdirs()
|
||||
}
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object MagiskVersion {
|
||||
const val MIN_SUPPORT = 18000
|
||||
@@ -59,36 +41,28 @@ object Const {
|
||||
}
|
||||
|
||||
object Url {
|
||||
@Deprecated("This shouldn't be used. There's literally no need for it")
|
||||
const val REPO_URL =
|
||||
"https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d"
|
||||
const val FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s"
|
||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||
const val MODULE_INSTALLER =
|
||||
"https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh"
|
||||
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
||||
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
@JvmField
|
||||
val SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk")
|
||||
@JvmField
|
||||
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
|
||||
|
||||
private fun getRaw(where: String, name: String) =
|
||||
"https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s".format(where, name)
|
||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
|
||||
}
|
||||
|
||||
object Key {
|
||||
// others
|
||||
const val LINK_KEY = "Link"
|
||||
const val IF_NONE_MATCH = "If-None-Match"
|
||||
const val ETAG_KEY = "ETag"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val INTENT_SET_NAME = "filename"
|
||||
const val INTENT_SET_LINK = "link"
|
||||
const val INTENT_SET_APP = "app_json"
|
||||
const val FLASH_ACTION = "action"
|
||||
const val FLASH_DATA = "additional_data"
|
||||
const val DISMISS_ID = "dismiss_id"
|
||||
const val BROADCAST_MANAGER_UPDATE = "manager_update"
|
||||
const val BROADCAST_REBOOT = "reboot"
|
||||
}
|
||||
@@ -101,5 +75,4 @@ object Const {
|
||||
const val UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,20 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Process
|
||||
|
||||
object Constants {
|
||||
|
||||
// Paths
|
||||
val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
val USER_ID get() = Process.myUid() / 100000
|
||||
|
||||
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
|
||||
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
|
||||
|
||||
const val GITHUB_URL = "https://github.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/"
|
||||
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
|
||||
|
||||
}
|
26
app/src/main/java/com/topjohnwu/magisk/Info.kt
Normal file
26
app/src/main/java/com/topjohnwu/magisk/Info.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
|
||||
object Info {
|
||||
|
||||
var magiskVersionCode = -1
|
||||
|
||||
var magiskVersionString = ""
|
||||
|
||||
var remote = UpdateInfo()
|
||||
|
||||
var keepVerity = false
|
||||
var keepEnc = false
|
||||
var recovery = false
|
||||
|
||||
fun loadMagiskInfo() {
|
||||
runCatching {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
||||
magiskVersionCode = ShellUtils.fastCmd("magisk -V").toInt()
|
||||
Config.magiskHide = Shell.su("magiskhide --status").exec().isSuccess
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.KConfig.UpdateChannel.STABLE
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
|
||||
object KConfig : PreferenceModel() {
|
||||
|
||||
override val context: Context by inject(Protected)
|
||||
override val fileName: String = "${context.packageName}_preferences"
|
||||
|
||||
private var internalUpdateChannel by preference(Config.Key.UPDATE_CHANNEL, STABLE.id.toString())
|
||||
var useCustomTabs by preference("useCustomTabs", true)
|
||||
@JvmStatic
|
||||
var customUpdateChannel by preference(Config.Key.CUSTOM_CHANNEL, "")
|
||||
|
||||
@JvmStatic
|
||||
var updateChannel: UpdateChannel
|
||||
get() = UpdateChannel.byId(internalUpdateChannel.toIntOrNull() ?: STABLE.id)
|
||||
set(value) {
|
||||
internalUpdateChannel = value.id.toString()
|
||||
}
|
||||
|
||||
internal const val DEFAULT_CHANNEL = "topjohnwu/magisk_files"
|
||||
|
||||
enum class UpdateChannel(val id: Int) {
|
||||
|
||||
STABLE(Config.Value.STABLE_CHANNEL),
|
||||
BETA(Config.Value.BETA_CHANNEL),
|
||||
CANARY(Config.Value.CANARY_CHANNEL),
|
||||
CANARY_DEBUG(Config.Value.CANARY_DEBUG_CHANNEL),
|
||||
CUSTOM(Config.Value.CUSTOM_CHANNEL);
|
||||
|
||||
companion object {
|
||||
fun byId(id: Int) = when (id) {
|
||||
Config.Value.STABLE_CHANNEL -> STABLE
|
||||
Config.Value.BETA_CHANNEL -> BETA
|
||||
Config.Value.CUSTOM_CHANNEL -> CUSTOM
|
||||
Config.Value.CANARY_CHANNEL -> CANARY
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> CANARY_DEBUG
|
||||
else -> STABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
123
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
123
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
@@ -0,0 +1,123 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.extensions.set
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
AppCompatActivity(), EventHandler {
|
||||
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected abstract val viewModel: ViewModel
|
||||
protected open val snackbarView get() = binding.root
|
||||
protected open val navHostId: Int = 0
|
||||
protected open val defaultPosition: Int = 0
|
||||
|
||||
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
|
||||
|
||||
init {
|
||||
val theme = if (Config.darkTheme) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
// Force applying our preferred local
|
||||
config?.setLocale(currentLocale)
|
||||
super.applyOverrideConfiguration(config)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(LocaleManager.getLocaleContext(base))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.viewEvents.observe(this, viewEventObserver)
|
||||
|
||||
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseActivity
|
||||
}
|
||||
}
|
||||
|
||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
val ungranted = permissions.filter {
|
||||
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
if (ungranted.isEmpty()) {
|
||||
request.onSuccess()
|
||||
} else {
|
||||
val requestCode = Random.nextInt(256, 512)
|
||||
resultCallbacks[requestCode] = { result, _ ->
|
||||
if (result > 0)
|
||||
request.onSuccess()
|
||||
else
|
||||
request.onFailure()
|
||||
}
|
||||
ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), requestCode)
|
||||
}
|
||||
}
|
||||
|
||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
||||
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
var success = true
|
||||
for (res in grantResults) {
|
||||
if (res != PackageManager.PERMISSION_GRANTED) {
|
||||
success = false
|
||||
break
|
||||
}
|
||||
}
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, if (success) 1 else -1, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@BaseActivity, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) {
|
||||
resultCallbacks[requestCode] = listener
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
}
|
50
app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
Normal file
50
app/src/main/java/com/topjohnwu/magisk/base/BaseFragment.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||
import com.topjohnwu.magisk.model.events.EventHandler
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
|
||||
abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
|
||||
Fragment(), EventHandler {
|
||||
|
||||
protected val activity get() = requireActivity() as BaseActivity<*, *>
|
||||
protected lateinit var binding: Binding
|
||||
protected abstract val layoutRes: Int
|
||||
protected abstract val viewModel: ViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.viewEvents.observe(this, viewEventObserver)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply {
|
||||
setVariable(BR.viewModel, viewModel)
|
||||
lifecycleOwner = this@BaseFragment
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onEventDispatched(event: ViewEvent) {
|
||||
super.onEventDispatched(event)
|
||||
activity.onEventDispatched(event)
|
||||
}
|
||||
|
||||
open fun onBackPressed(): Boolean = false
|
||||
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.preference.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
protected val prefs: SharedPreferences by inject()
|
||||
protected val activity get() = requireActivity() as BaseActivity<*, *>
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun setAllPreferencesToAvoidHavingExtraSpace(preference: Preference) {
|
||||
preference.isIconSpaceReserved = false
|
||||
if (preference is PreferenceGroup)
|
||||
for (i in 0 until preference.preferenceCount)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference.getPreference(i))
|
||||
}
|
||||
|
||||
override fun setPreferenceScreen(preferenceScreen: PreferenceScreen?) {
|
||||
if (preferenceScreen != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preferenceScreen)
|
||||
super.setPreferenceScreen(preferenceScreen)
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> =
|
||||
object : PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onPreferenceHierarchyChange(preference: Preference?) {
|
||||
if (preference != null)
|
||||
setAllPreferencesToAvoidHavingExtraSpace(preference)
|
||||
super.onPreferenceHierarchyChange(preference)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.topjohnwu.magisk.base
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
import android.net.Uri
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.work.Data
|
||||
import androidx.work.ListenableWorker
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.*
|
||||
|
||||
abstract class DelegateWorker {
|
||||
|
||||
private lateinit var worker: ListenableWorker
|
||||
|
||||
val applicationContext: Context
|
||||
get() = worker.applicationContext
|
||||
|
||||
val id: UUID
|
||||
get() = worker.id
|
||||
|
||||
val inputData: Data
|
||||
get() = worker.inputData
|
||||
|
||||
val tags: Set<String>
|
||||
get() = worker.tags
|
||||
|
||||
val triggeredContentUris: List<Uri>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentUris
|
||||
|
||||
val triggeredContentAuthorities: List<String>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentAuthorities
|
||||
|
||||
val network: Network?
|
||||
@RequiresApi(28)
|
||||
get() = worker.network
|
||||
|
||||
val runAttemptCount: Int
|
||||
get() = worker.runAttemptCount
|
||||
|
||||
val isStopped: Boolean
|
||||
get() = worker.isStopped
|
||||
|
||||
abstract fun doWork(): ListenableWorker.Result
|
||||
|
||||
fun onStopped() {}
|
||||
|
||||
fun attachWorker(w: ListenableWorker) {
|
||||
worker = w
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||
return worker.startWork()
|
||||
}
|
||||
}
|
@@ -1,16 +1,29 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import com.skoumal.teanity.extensions.doOnSubscribeUi
|
||||
import com.skoumal.teanity.viewmodel.LoadingViewModel
|
||||
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
|
||||
|
||||
abstract class MagiskViewModel : LoadingViewModel() {
|
||||
abstract class BaseViewModel(
|
||||
initialState: State = State.LOADING
|
||||
) : LoadingViewModel(initialState) {
|
||||
|
||||
val isConnected = KObservableField(false)
|
||||
|
||||
init {
|
||||
ReactiveNetwork.observeNetworkConnectivity(get())
|
||||
.subscribeK { isConnected.value = it.available() }
|
||||
.add()
|
||||
}
|
||||
|
||||
fun withView(action: Activity.() -> Unit) {
|
||||
ViewActionEvent(action).publish()
|
@@ -0,0 +1,78 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import io.reactivex.*
|
||||
|
||||
abstract class LoadingViewModel(defaultState: State = State.LOADING) :
|
||||
StatefulViewModel<LoadingViewModel.State>(defaultState) {
|
||||
|
||||
val loading @Bindable get() = state == State.LOADING
|
||||
val loaded @Bindable get() = state == State.LOADED
|
||||
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoading() {
|
||||
state = State.LOADING
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoaded() {
|
||||
state = State.LOADED
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
|
||||
ReplaceWith("state = State.LOADING_FAILED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
|
||||
DeprecationLevel.WARNING
|
||||
)
|
||||
fun setLoadingFailed() {
|
||||
state = State.LOADING_FAILED
|
||||
}
|
||||
|
||||
override fun notifyStateChanged() {
|
||||
notifyPropertyChanged(BR.loading)
|
||||
notifyPropertyChanged(BR.loaded)
|
||||
notifyPropertyChanged(BR.loadingFailed)
|
||||
}
|
||||
|
||||
enum class State {
|
||||
LOADED, LOADING, LOADING_FAILED
|
||||
}
|
||||
|
||||
//region Rx
|
||||
protected fun <T> Observable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Single<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Maybe<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun <T> Flowable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
|
||||
protected fun Completable.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
|
||||
doOnSubscribe { viewModel.state = State.LOADING }
|
||||
.doOnError { viewModel.state = State.LOADING_FAILED }
|
||||
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
|
||||
//endregion
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
/**
|
||||
* Copy of [android.databinding.BaseObservable] which extends [ViewModel]
|
||||
*/
|
||||
abstract class ObservableViewModel : TeanityViewModel(), Observable {
|
||||
|
||||
@Transient
|
||||
private var callbacks: PropertyChangeRegistry? = null
|
||||
|
||||
@Synchronized
|
||||
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
if (callbacks == null) {
|
||||
callbacks = PropertyChangeRegistry()
|
||||
}
|
||||
callbacks?.add(callback)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
|
||||
callbacks?.remove(callback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that all properties of this instance have changed.
|
||||
*/
|
||||
@Synchronized
|
||||
fun notifyChange() {
|
||||
callbacks?.notifyCallbacks(this, 0, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listeners that a specific property has changed. The getter for the property
|
||||
* that changes should be marked with [android.databinding.Bindable] to generate a field in
|
||||
* `BR` to be used as `fieldId`.
|
||||
*
|
||||
* @param fieldId The generated BR id for the Bindable field.
|
||||
*/
|
||||
fun notifyPropertyChanged(fieldId: Int) {
|
||||
callbacks?.notifyCallbacks(this, fieldId, null)
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
abstract class StatefulViewModel<State : Enum<*>>(
|
||||
val defaultState: State
|
||||
) : ObservableViewModel() {
|
||||
|
||||
var state: State = defaultState
|
||||
set(value) {
|
||||
field = value
|
||||
notifyStateChanged()
|
||||
}
|
||||
|
||||
open fun notifyStateChanged() = Unit
|
||||
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.topjohnwu.magisk.base.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.magisk.model.events.SimpleViewEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
abstract class TeanityViewModel : ViewModel() {
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
private val _viewEvents = MutableLiveData<ViewEvent>()
|
||||
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
fun <Event : ViewEvent> Event.publish() {
|
||||
_viewEvents.value = this
|
||||
}
|
||||
|
||||
fun Int.publish() {
|
||||
_viewEvents.value = SimpleViewEvent(this)
|
||||
}
|
||||
|
||||
fun Disposable.add() {
|
||||
disposables.add(this)
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
@@ -12,7 +11,7 @@ class LogDao : BaseDao() {
|
||||
override val table = DatabaseDefinition.Table.LOG
|
||||
|
||||
fun deleteOutdated(
|
||||
suTimeout: Long = Config.suLogTimeout * TimeUnit.DAYS.toMillis(1)
|
||||
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
lessThan("time", suTimeout.toString())
|
||||
|
@@ -2,12 +2,13 @@ package com.topjohnwu.magisk.data.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.utils.now
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@@ -47,12 +48,7 @@ class PolicyDao(
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.map { it.firstOrNull()?.toPolicy(context.packageManager) }
|
||||
.doOnError {
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
}
|
||||
}.map { it.first().toPolicySafe() }
|
||||
|
||||
fun update(policy: MagiskPolicy) = query<Replace> {
|
||||
values(policy.toMap())
|
||||
@@ -60,10 +56,20 @@ class PolicyDao(
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Constants.USER_ID)
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.flattenAsFlowable { it }
|
||||
.map { it.toPolicy(context.packageManager) }
|
||||
.toList()
|
||||
}.map { it.mapNotNull { it.toPolicySafe() } }
|
||||
|
||||
|
||||
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
|
||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
||||
Timber.e(it)
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
val uid = getOrElse("uid") { null } ?: return null
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Dao
|
||||
abstract class RepoDao {
|
||||
|
||||
val repoIDList get() = getRepoID().map { it.id }
|
||||
|
||||
val repos: List<Repo> get() = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> getReposNameOrder()
|
||||
else -> getReposDateOrder()
|
||||
}
|
||||
|
||||
var etagKey: String
|
||||
set(etag) = addEtagRaw(RepoEtag(0, etag))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
fun clear() {
|
||||
clearRepos()
|
||||
clearEtag()
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE")
|
||||
protected abstract fun getReposNameOrder(): List<Repo>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun addRepo(repo: Repo)
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id")
|
||||
abstract fun getRepo(id: String): Repo?
|
||||
|
||||
@Query("SELECT id FROM repos")
|
||||
protected abstract fun getRepoID(): List<RepoID>
|
||||
|
||||
@Delete
|
||||
abstract fun removeRepo(repo: Repo)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id = :id")
|
||||
abstract fun removeRepo(id: String)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id IN (:idList)")
|
||||
abstract fun removeRepos(idList: Collection<String>)
|
||||
|
||||
@Query("SELECT * FROM etag")
|
||||
protected abstract fun etagRaw(): RepoEtag?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||
|
||||
@Query("DELETE FROM repos")
|
||||
protected abstract fun clearRepos()
|
||||
|
||||
@Query("DELETE FROM etag")
|
||||
protected abstract fun clearEtag()
|
||||
}
|
||||
|
||||
data class RepoID(
|
||||
@PrimaryKey val id: String
|
||||
)
|
||||
|
||||
@Entity(tableName = "etag")
|
||||
data class RepoEtag(
|
||||
@PrimaryKey val id: Int,
|
||||
val key: String
|
||||
)
|
||||
|
@@ -0,0 +1,11 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
}
|
@@ -1,118 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Deprecated
|
||||
public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final int DATABASE_VER = 5;
|
||||
private static final String TABLE_NAME = "repos";
|
||||
|
||||
private final SQLiteDatabase mDb;
|
||||
|
||||
@Deprecated
|
||||
public RepoDatabaseHelper(Context context) {
|
||||
super(context, "repo.db", null, DATABASE_VER);
|
||||
mDb = getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion != newVersion) {
|
||||
// Nuke old DB and create new table
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
|
||||
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
|
||||
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))");
|
||||
Config.remove(Config.Key.ETAG_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(String id) {
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(Repo repo) {
|
||||
removeRepo(repo.getId());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(Iterable<String> list) {
|
||||
for (String id : list) {
|
||||
if (id == null) continue;
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void addRepo(Repo repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.getContentValues());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Repo getRepo(String id) {
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[]{id}, null, null, null)) {
|
||||
if (c.moveToNext()) {
|
||||
return new Repo(c);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Cursor getRawCursor() {
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Cursor getRepoCursor() {
|
||||
String orderBy = null;
|
||||
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
|
||||
case Config.Value.ORDER_NAME:
|
||||
orderBy = "name COLLATE NOCASE";
|
||||
break;
|
||||
case Config.Value.ORDER_DATE:
|
||||
orderBy = "last_update DESC";
|
||||
}
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Set<String> getRepoIDSet() {
|
||||
HashSet<String> set = new HashSet<>(300);
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (c.moveToNext()) {
|
||||
set.add(c.getString(c.getColumnIndex("id")));
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
@@ -10,11 +10,12 @@ class SettingsDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: Int) = query<Insert> {
|
||||
values(key to value.toString())
|
||||
fun put(key: String, value: Int) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: Int = -1) = query<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
|
||||
|
||||
|
@@ -10,8 +10,8 @@ class StringDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: String) = query<Insert> {
|
||||
values(key to value)
|
||||
fun put(key: String, value: String) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: String = "") = query<Select> {
|
||||
|
@@ -32,11 +32,7 @@ class Delete : MagiskQueryBuilder {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln(condition)
|
||||
.toString()
|
||||
return listOf(requestType, table, condition).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +61,7 @@ class Select : MagiskQueryBuilder {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln(condition)
|
||||
.appendln(orderField)
|
||||
.toString()
|
||||
.replace("\n", " ")
|
||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,23 +74,25 @@ open class Insert : MagiskQueryBuilder {
|
||||
override lateinit var table: String
|
||||
|
||||
private val keys get() = _values.keys.joinToString(",")
|
||||
private val values get() = _values.values.joinToString(",") { "\"$it\"" }
|
||||
private var _values: Map<String, String> = mapOf()
|
||||
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, String>) {
|
||||
fun values(vararg pairs: Pair<String, Any>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, String>) {
|
||||
fun values(values: Map<String, Any>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln("($keys) VALUES($values)")
|
||||
.toString()
|
||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,63 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.KConfig
|
||||
import com.topjohnwu.magisk.model.entity.MagiskConfig
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Streaming
|
||||
import retrofit2.http.Url
|
||||
|
||||
|
||||
interface GithubRawApiServices {
|
||||
|
||||
//region topjohnwu/magisk_files
|
||||
|
||||
@GET("$MAGISK_FILES/master/stable.json")
|
||||
fun fetchConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/release.json")
|
||||
fun fetchCanaryConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
|
||||
fun fetchCanaryDebugConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET
|
||||
fun fetchCustomConfig(@Url url: String): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Constants.SNET_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Constants.BOOTCTL_REVISION): Single<ResponseBody>
|
||||
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* This method shall be used exclusively for fetching files from urls from previous requests.
|
||||
* Him, who uses it in a wrong way, shall die in an eternal flame.
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||
|
||||
|
||||
companion object {
|
||||
private const val REVISION = "revision"
|
||||
private const val MODULE = "module"
|
||||
private const val FILE = "file"
|
||||
|
||||
|
||||
private const val MAGISK_FILES = KConfig.DEFAULT_CHANNEL
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.magisk.tasks.GithubRepoInfo
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.adapter.rxjava2.Result
|
||||
import retrofit2.http.*
|
||||
|
||||
interface GithubRawServices {
|
||||
|
||||
//region topjohnwu/magisk_files
|
||||
|
||||
@GET("$MAGISK_FILES/master/stable.json")
|
||||
fun fetchStableUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/canary/release.json")
|
||||
fun fetchCanaryUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/canary/debug.json")
|
||||
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET
|
||||
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
|
||||
@Streaming
|
||||
fun fetchInstaller(): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<String>
|
||||
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* This method shall be used exclusively for fetching files from urls from previous requests.
|
||||
* Him, who uses it in a wrong way, shall die in an eternal flame.
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||
|
||||
@GET
|
||||
fun fetchString(@Url url: String): Single<String>
|
||||
|
||||
|
||||
companion object {
|
||||
private const val REVISION = "revision"
|
||||
private const val MODULE = "module"
|
||||
private const val FILE = "file"
|
||||
|
||||
|
||||
private const val MAGISK_FILES = "topjohnwu/magisk_files"
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface GithubApiServices {
|
||||
|
||||
@GET("repos")
|
||||
fun fetchRepos(@Query("page") page: Int,
|
||||
@Header(Const.Key.IF_NONE_MATCH) etag: String,
|
||||
@Query("sort") sort: String = "pushed",
|
||||
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
|
||||
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
|
||||
class AppRepository(private val policyDao: PolicyDao) {
|
||||
|
||||
fun deleteOutdated() = policyDao.deleteOutdated()
|
||||
fun delete(packageName: String) = policyDao.delete(packageName)
|
||||
fun delete(uid: Int) = policyDao.delete(uid)
|
||||
fun fetch(uid: Int) = policyDao.fetch(uid)
|
||||
fun fetchAll() = policyDao.fetchAll()
|
||||
fun update(policy: MagiskPolicy) = policyDao.update(policy)
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface DBConfig {
|
||||
val settingsDao: SettingsDao
|
||||
val stringDao: StringDao
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Int
|
||||
) = DBSettingsValue(name, default)
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) = DBBoolSettings(name, default)
|
||||
|
||||
fun dbStrings(
|
||||
name: String,
|
||||
default: String,
|
||||
sync: Boolean = false
|
||||
) = DBStringsValue(name, default, sync)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
||||
private var value: Int? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
||||
if (value == null)
|
||||
value = thisRef.settingsDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
thisRef.settingsDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
class DBBoolSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||
|
||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
||||
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean
|
||||
= base.getValue(thisRef, property) != 0
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val sync: Boolean
|
||||
) : ReadWriteProperty<DBConfig, String> {
|
||||
|
||||
private var value: String? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
||||
if (value == null)
|
||||
value = thisRef.stringDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
if (value.isEmpty()) {
|
||||
if (sync) {
|
||||
thisRef.stringDao.delete(name).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.delete(name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
} else {
|
||||
if (sync) {
|
||||
thisRef.stringDao.put(name, value).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +1,12 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.data.database.LogDao
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@@ -20,9 +18,8 @@ class LogRepository(
|
||||
.map { it.sortByDescending { it.date.time }; it }
|
||||
.map { it.wrap() }
|
||||
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Constants.MAGISK_LOG}".suRaw()
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { Timber.i(it.toString()); it }
|
||||
|
||||
fun clearLogs() = logDao.deleteAll()
|
||||
fun clearOutdated() = logDao.deleteOutdated()
|
||||
|
@@ -1,86 +1,49 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.KConfig
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.model.entity.Version
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.magisk.utils.writeToFile
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.BiFunction
|
||||
|
||||
class MagiskRepository(
|
||||
private val context: Context,
|
||||
private val apiRaw: GithubRawApiServices,
|
||||
private val packageManager: PackageManager
|
||||
private val apiRaw: GithubRawServices,
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun fetchMagisk() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.magisk.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
|
||||
fun fetchSafetynet() = apiRaw.fetchSafetynet()
|
||||
|
||||
fun fetchManager() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.app.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_APK) }
|
||||
|
||||
fun fetchUninstaller() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.uninstaller.link) }
|
||||
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) }
|
||||
|
||||
fun fetchSafetynet() = apiRaw
|
||||
.fetchSafetynet()
|
||||
.map { it.writeToFile(context, FILE_SAFETY_NET_APK) }
|
||||
|
||||
fun fetchBootctl() = apiRaw
|
||||
.fetchBootctl()
|
||||
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
|
||||
|
||||
|
||||
fun fetchConfig() = when (KConfig.updateChannel) {
|
||||
KConfig.UpdateChannel.STABLE -> apiRaw.fetchConfig()
|
||||
KConfig.UpdateChannel.BETA -> apiRaw.fetchBetaConfig()
|
||||
KConfig.UpdateChannel.CANARY -> apiRaw.fetchCanaryConfig()
|
||||
KConfig.UpdateChannel.CANARY_DEBUG -> apiRaw.fetchCanaryDebugConfig()
|
||||
KConfig.UpdateChannel.CUSTOM -> apiRaw.fetchCustomConfig(KConfig.customUpdateChannel)
|
||||
}
|
||||
.doOnSuccess {
|
||||
Config.remoteMagiskVersionCode = it.magisk.versionCode.toIntOrNull() ?: -1
|
||||
Config.magiskLink = it.magisk.link
|
||||
Config.magiskNoteLink = it.magisk.note
|
||||
Config.magiskMD5 = it.magisk.hash
|
||||
Config.remoteManagerVersionCode = it.app.versionCode.toIntOrNull() ?: -1
|
||||
Config.remoteManagerVersionString = it.app.version
|
||||
Config.managerLink = it.app.link
|
||||
Config.managerNoteLink = it.app.note
|
||||
Config.uninstallerLink = it.uninstaller.link
|
||||
}
|
||||
|
||||
|
||||
fun fetchMagiskVersion(): Single<Version> = Single.zip(
|
||||
fetchMagiskVersionName(),
|
||||
fetchMagiskVersionCode(),
|
||||
BiFunction { versionName, versionCode ->
|
||||
Version(versionName, versionCode)
|
||||
fun fetchUpdate() = when (Config.updateChannel) {
|
||||
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
|
||||
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
|
||||
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
|
||||
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}.flatMap {
|
||||
// If remote version is lower than current installed, try switching to beta
|
||||
if (it.magisk.versionCode < Info.magiskVersionCode
|
||||
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
|
||||
Config.updateChannel = Config.Value.BETA_CHANNEL
|
||||
apiRaw.fetchBetaUpdate()
|
||||
} else {
|
||||
Single.just(it)
|
||||
}
|
||||
)
|
||||
|
||||
}.doOnSuccess { Info.remote = it }
|
||||
|
||||
fun fetchApps() =
|
||||
Single.fromCallable { packageManager.getInstalledApplications(0) }
|
||||
.flattenAsFlowable { it }
|
||||
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
||||
.map {
|
||||
val label = Utils.getAppLabel(it, packageManager)
|
||||
val label = it.getLabel(packageManager)
|
||||
val icon = it.loadIcon(packageManager)
|
||||
HideAppInfo(it, label, icon)
|
||||
}
|
||||
@@ -93,30 +56,14 @@ class MagiskRepository(
|
||||
.map { HideTarget(it) }
|
||||
.toList()
|
||||
|
||||
private fun fetchMagiskVersionName() = "magisk -v".suRaw()
|
||||
.map { it.first() }
|
||||
.map { it.substring(0 until it.indexOf(":")) }
|
||||
.onErrorReturn { "Unknown" }
|
||||
|
||||
private fun fetchMagiskVersionCode() = "magisk -V".suRaw()
|
||||
.map { it.first() }
|
||||
.map { it.toIntOrNull() ?: -1 }
|
||||
.onErrorReturn { -1 }
|
||||
|
||||
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
|
||||
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
|
||||
|
||||
private val Boolean.state get() = if (this) "add" else "rm"
|
||||
|
||||
companion object {
|
||||
const val FILE_MAGISK_ZIP = "magisk.zip"
|
||||
const val FILE_MAGISK_APK = "magisk.apk"
|
||||
const val FILE_UNINSTALLER_ZIP = "uninstaller.zip"
|
||||
const val FILE_SAFETY_NET_APK = "safetynet.apk"
|
||||
const val FILE_BOOTCTL_SH = "bootctl"
|
||||
|
||||
private val blacklist = listOf(
|
||||
let { val app: App by inject(); app }.packageName,
|
||||
private val blacklist by lazy { listOf(
|
||||
packageName,
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
@@ -124,7 +71,7 @@ class MagiskRepository(
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
)
|
||||
) }
|
||||
}
|
||||
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
|
||||
class SettingRepository(private val settingsDao: SettingsDao) {
|
||||
|
||||
fun fetch(key: String, default: Int) = settingsDao.fetch(key, default)
|
||||
fun put(key: String, value: Int) = settingsDao.put(key, value)
|
||||
fun delete(key: String) = settingsDao.delete(key)
|
||||
|
||||
}
|
@@ -1,11 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
class StringRepository(private val stringDao: StringDao) {
|
||||
class StringRepository(
|
||||
private val api: GithubRawServices
|
||||
) {
|
||||
|
||||
fun fetch(key: String, default: String) = stringDao.fetch(key, default)
|
||||
fun put(key: String, value: String) = stringDao.put(key, value)
|
||||
fun delete(key: String) = stringDao.delete(key)
|
||||
fun getString(url: String) = api.fetchString(url)
|
||||
|
||||
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
|
||||
|
||||
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.databinding.BindingAdapter
|
||||
|
||||
@BindingAdapter("gone")
|
||||
fun setGone(view: View, gone: Boolean) {
|
||||
view.isGone = gone
|
||||
}
|
||||
|
||||
@BindingAdapter("invisible")
|
||||
fun setInvisible(view: View, invisible: Boolean) {
|
||||
view.isInvisible = invisible
|
||||
}
|
||||
|
||||
@BindingAdapter("goneUnless")
|
||||
fun setGoneUnless(view: View, goneUnless: Boolean) {
|
||||
setGone(view, goneUnless.not())
|
||||
}
|
||||
|
||||
@BindingAdapter("invisibleUnless")
|
||||
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
|
||||
setInvisible(view, invisibleUnless.not())
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import androidx.databinding.BindingAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.extensions.startEndToLeftRight
|
||||
import com.topjohnwu.magisk.extensions.toPx
|
||||
import com.topjohnwu.magisk.utils.KItemDecoration
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@BindingAdapter(
|
||||
"dividerColor",
|
||||
"dividerHorizontal",
|
||||
"dividerSize",
|
||||
"dividerAfterLast",
|
||||
"dividerMarginStart",
|
||||
"dividerMarginEnd",
|
||||
"dividerMarginTop",
|
||||
"dividerMarginBottom",
|
||||
requireAll = false
|
||||
)
|
||||
fun setDivider(
|
||||
view: RecyclerView,
|
||||
color: Int,
|
||||
horizontal: Boolean,
|
||||
_size: Float,
|
||||
_afterLast: Boolean?,
|
||||
marginStartF: Float,
|
||||
marginEndF: Float,
|
||||
marginTopF: Float,
|
||||
marginBottomF: Float
|
||||
) {
|
||||
val orientation = if (horizontal) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
|
||||
val size = if (_size > 0) _size.roundToInt() else 1.toPx()
|
||||
val (width, height) = if (horizontal) size to 1 else 1 to size
|
||||
val afterLast = _afterLast ?: true
|
||||
|
||||
val marginStart = marginStartF.roundToInt()
|
||||
val marginEnd = marginEndF.roundToInt()
|
||||
val marginTop = marginTopF.roundToInt()
|
||||
val marginBottom = marginBottomF.roundToInt()
|
||||
val (marginLeft, marginRight) = view.context.startEndToLeftRight(marginStart, marginEnd)
|
||||
|
||||
val drawable = GradientDrawable().apply {
|
||||
setSize(width, height)
|
||||
shape = GradientDrawable.RECTANGLE
|
||||
setColor(color)
|
||||
}.let {
|
||||
InsetDrawable(it, marginLeft, marginTop, marginRight, marginBottom)
|
||||
}
|
||||
|
||||
val decoration = KItemDecoration(view.context, orientation)
|
||||
.setDeco(drawable)
|
||||
.apply { showAfterLast = afterLast }
|
||||
view.addItemDecoration(decoration)
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
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)
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
package com.topjohnwu.magisk.databinding
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
abstract class RvItem {
|
||||
|
||||
abstract val layoutRes: Int
|
||||
|
||||
@CallSuper
|
||||
open fun bind(binding: ItemBinding<*>) {
|
||||
binding.set(BR.item, layoutRes)
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is useful if you want to manipulate your views directly.
|
||||
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
|
||||
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
|
||||
*/
|
||||
open fun onBindingBound(binding: ViewDataBinding) {}
|
||||
}
|
||||
|
||||
abstract class ComparableRvItem<in T> : RvItem() {
|
||||
|
||||
abstract fun itemSameAs(other: T): Boolean
|
||||
abstract fun contentSameAs(other: T): Boolean
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T)
|
||||
|
||||
companion object {
|
||||
val callback = object : DiffObservableList.Callback<ComparableRvItem<*>> {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericItemSameAs(newItem)
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ComparableRvItem<*>,
|
||||
newItem: ComparableRvItem<*>
|
||||
) = oldItem.genericContentSameAs(newItem)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,18 +1,61 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.skoumal.teanity.rxbus.RxBus
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
||||
|
||||
val applicationModule = module {
|
||||
single { RxBus() }
|
||||
factory { get<Context>().resources }
|
||||
factory { get<Context>() as App }
|
||||
factory { get<Context>().packageManager }
|
||||
factory(Protected) { get<App>().protectedContext }
|
||||
factory(Protected) { createDEContext(get()) }
|
||||
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
||||
}
|
||||
single { ActivityTracker() }
|
||||
factory { get<ActivityTracker>().foreground ?: NullActivity }
|
||||
}
|
||||
|
||||
private fun createDEContext(context: Context): Context {
|
||||
return if (Build.VERSION.SDK_INT >= 24)
|
||||
context.createDeviceProtectedStorageContext()
|
||||
else context
|
||||
}
|
||||
|
||||
class ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
||||
|
||||
@SuppressLint("Registered")
|
||||
object NullActivity : Activity()
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.data.database.*
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
@@ -10,6 +12,12 @@ val databaseModule = module {
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
single { RepoDatabaseHelper(get()) }
|
||||
single { UpdateRepos(get()) }
|
||||
single { createRepoDatabase(get()) }
|
||||
single { get<RepoDatabase>().repoDao() }
|
||||
single { RepoUpdater(get(), get()) }
|
||||
}
|
||||
|
||||
fun createRepoDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val miscModule = module {
|
||||
|
||||
// define miscs here
|
||||
|
||||
}
|
@@ -5,6 +5,5 @@ val koinModules = listOf(
|
||||
networkingModule,
|
||||
databaseModule,
|
||||
repositoryModule,
|
||||
viewModelModules,
|
||||
miscModule
|
||||
viewModelModules
|
||||
)
|
||||
|
@@ -1,6 +0,0 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
@@ -1,69 +1,83 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.html.HtmlPlugin
|
||||
import io.noties.markwon.image.ImagesPlugin
|
||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
||||
|
||||
val networkingModule = module {
|
||||
single { createOkHttpClient() }
|
||||
single { createConverterFactory() }
|
||||
single { createCallAdapterFactory() }
|
||||
single { createRetrofit(get(), get(), get()) }
|
||||
single { createApiService<GithubRawApiServices>(get(), Constants.GITHUB_RAW_API_URL) }
|
||||
single { createOkHttpClient(get()) }
|
||||
single { createRetrofit(get()) }
|
||||
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||
single { createMarkwon(get(), get()) }
|
||||
}
|
||||
|
||||
fun createOkHttpClient(): OkHttpClient {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
@Suppress("DEPRECATION")
|
||||
fun createOkHttpClient(context: Context): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(httpLoggingInterceptor)
|
||||
.build()
|
||||
if (!Networking.init(context)) {
|
||||
builder.sslSocketFactory(NoSSLv3SocketFactory())
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun createConverterFactory(): Converter.Factory {
|
||||
fun createMoshiConverterFactory(): MoshiConverterFactory {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(JsonAdapterFactory.INSTANCE)
|
||||
.add(KotshiJsonAdapterFactory)
|
||||
.build()
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
fun createCallAdapterFactory(): CallAdapter.Factory {
|
||||
return RxJava2CallAdapterFactory.create()
|
||||
}
|
||||
|
||||
fun createRetrofit(
|
||||
okHttpClient: OkHttpClient,
|
||||
converterFactory: Converter.Factory,
|
||||
callAdapterFactory: CallAdapter.Factory
|
||||
): Retrofit.Builder {
|
||||
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(converterFactory)
|
||||
.addCallAdapterFactory(callAdapterFactory)
|
||||
.addConverterFactory(ScalarsConverterFactory.create())
|
||||
.addConverterFactory(createMoshiConverterFactory())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(okHttpClient)
|
||||
}
|
||||
|
||||
@KotshiJsonAdapterFactory
|
||||
abstract class JsonAdapterFactory : JsonAdapter.Factory {
|
||||
companion object {
|
||||
val INSTANCE: JsonAdapterFactory = KotshiJsonAdapterFactory
|
||||
}
|
||||
}
|
||||
abstract class JsonAdapterFactory : JsonAdapter.Factory
|
||||
|
||||
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
|
||||
return retrofitBuilder
|
||||
.baseUrl(baseUrl)
|
||||
.build()
|
||||
.create(T::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create {
|
||||
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.data.repository.*
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val repositoryModule = module {
|
||||
single { MagiskRepository(get(), get(), get()) }
|
||||
single { MagiskRepository(get(), get()) }
|
||||
single { LogRepository(get()) }
|
||||
single { AppRepository(get()) }
|
||||
single { SettingRepository(get()) }
|
||||
single { StringRepository(get()) }
|
||||
}
|
||||
|
@@ -20,6 +20,8 @@ val viewModelModules = module {
|
||||
viewModel { HideViewModel(get(), get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { LogViewModel(get(), get()) }
|
||||
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) }
|
||||
viewModel { (action: String, file: Uri, additional: Uri) ->
|
||||
FlashViewModel(action, file, additional, get())
|
||||
}
|
||||
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||
}
|
||||
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.ObservableBoolean
|
||||
import androidx.databinding.ObservableField
|
||||
import androidx.databinding.ObservableInt
|
||||
|
||||
fun <T> ObservableField<T>.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (T?) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ObservableInt.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (Int) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ObservableBoolean.addOnPropertyChangedCallback(
|
||||
removeAfterChanged: Boolean = false,
|
||||
callback: (Boolean) -> Unit
|
||||
) {
|
||||
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||
callback(get())
|
||||
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
inline fun <T> ObservableField<T>.update(block: (T?) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
||||
|
||||
inline fun <T> ObservableField<T>.updateNonNull(block: (T) -> Unit) {
|
||||
update {
|
||||
it ?: return@update
|
||||
block(it)
|
||||
}
|
||||
}
|
||||
|
||||
inline fun ObservableInt.update(block: (Int) -> Unit) {
|
||||
set(get().apply(block))
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun Int.toDp(): Int = ceil(this / Resources.getSystem().displayMetrics.density).roundToInt()
|
||||
|
||||
fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).roundToInt()
|
@@ -0,0 +1,6 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
fun ui(body: () -> Unit) = Handler(Looper.getMainLooper()).post(body)
|
201
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
201
app/src/main/java/com/topjohnwu/magisk/extensions/RxJava.kt
Normal file
@@ -0,0 +1,201 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.databinding.ObservableField
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import io.reactivex.*
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposables
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import androidx.databinding.Observable as BindingObservable
|
||||
|
||||
fun <T> Observable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Flowable<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Single<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun <T> Maybe<T>.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Maybe<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
fun Completable.applySchedulers(
|
||||
subscribeOn: Scheduler = Schedulers.io(),
|
||||
observeOn: Scheduler = AndroidSchedulers.mainThread()
|
||||
): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn)
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
typealias OnCompleteListener = () -> Unit
|
||||
typealias OnSuccessListener<T> = (T) -> Unit
|
||||
typealias OnErrorListener = (Throwable) -> Unit
|
||||
|
||||
/*=== ALIASES FOR OBSERVABLES ===*/
|
||||
|
||||
fun <T> Observable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Single<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError)
|
||||
|
||||
fun <T> Maybe<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun <T> Flowable<T>.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {},
|
||||
onNext: OnSuccessListener<T> = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onNext, onError, onComplete)
|
||||
|
||||
fun Completable.subscribeK(
|
||||
onError: OnErrorListener = { it.printStackTrace() },
|
||||
onComplete: OnCompleteListener = {}
|
||||
) = applySchedulers()
|
||||
.subscribe(onComplete, onError)
|
||||
|
||||
|
||||
fun <T> Observable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Single<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun <T> Maybe<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnSuccessUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
.doOnComplete { field.value = field.value }
|
||||
|
||||
fun <T> Flowable<out T>.updateBy(
|
||||
field: KObservableField<T?>
|
||||
) = doOnNextUi { field.value = it }
|
||||
.doOnErrorUi { field.value = null }
|
||||
|
||||
fun Completable.updateBy(
|
||||
field: KObservableField<Boolean>
|
||||
) = doOnCompleteUi { field.value = true }
|
||||
.doOnErrorUi { field.value = false }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
fun Completable.doOnSubscribeUi(body: () -> Unit) =
|
||||
doOnSubscribe { ui { body() } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
|
||||
doOnError { ui { body(it) } }
|
||||
|
||||
|
||||
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
|
||||
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
|
||||
doOnNext { ui { body(it) } }
|
||||
|
||||
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
|
||||
doOnSuccess { ui { body(it) } }
|
||||
|
||||
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
|
||||
fun Completable.doOnCompleteUi(body: () -> Unit) =
|
||||
doOnComplete { ui { body() } }
|
||||
|
||||
|
||||
fun <T, R> Observable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Single<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Maybe<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flattenAsFlowable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T, R> Flowable<List<T>>.mapList(
|
||||
transformer: (T) -> R
|
||||
) = flatMapIterable { it }
|
||||
.map(transformer)
|
||||
.toList()
|
||||
|
||||
fun <T> ObservableField<T>.toObservable(): Observable<T> {
|
||||
val observableField = this
|
||||
return Observable.create { emitter ->
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
|
||||
val callback = object : BindingObservable.OnPropertyChangedCallback() {
|
||||
override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) {
|
||||
observableField.get()?.let { emitter.onNext(it) }
|
||||
}
|
||||
}
|
||||
observableField.addOnPropertyChangedCallback(callback)
|
||||
emitter.setDisposable(Disposables.fromAction {
|
||||
observableField.removeOnPropertyChangedCallback(callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : Any> T.toSingle() = Single.just(this)
|
||||
|
||||
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
|
||||
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })
|
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
126
app/src/main/java/com/topjohnwu/magisk/extensions/Snackbar.kt
Normal file
@@ -0,0 +1,126 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
||||
fun AppCompatActivity.snackbar(
|
||||
view: View,
|
||||
@StringRes messageRes: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
snackbar(view, getString(messageRes), length, f)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.snackbar(
|
||||
view: View,
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) = Snackbar.make(view, message, length)
|
||||
.apply(f)
|
||||
.show()
|
||||
|
||||
fun Fragment.snackbar(
|
||||
view: View,
|
||||
@StringRes messageRes: Int,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) {
|
||||
snackbar(view, getString(messageRes), length, f)
|
||||
}
|
||||
|
||||
fun Fragment.snackbar(
|
||||
view: View,
|
||||
message: String,
|
||||
length: Int = Snackbar.LENGTH_SHORT,
|
||||
f: Snackbar.() -> Unit = {}
|
||||
) = Snackbar.make(view, message, length)
|
||||
.apply(f)
|
||||
.show()
|
||||
|
||||
fun Snackbar.action(init: KSnackbar.() -> Unit) = apply {
|
||||
val config = KSnackbar().apply(init)
|
||||
|
||||
setAction(config.title(context), config.onClickListener)
|
||||
|
||||
when {
|
||||
config.hasValidColor -> setActionTextColor(config.color(context) ?: return@apply)
|
||||
config.hasValidColorStateList -> setActionTextColor(config.colorStateList(context) ?: return@apply)
|
||||
}
|
||||
}
|
||||
|
||||
class KSnackbar {
|
||||
var colorRes: Int = -1
|
||||
var colorStateListRes: Int = -1
|
||||
|
||||
var title: CharSequence = ""
|
||||
var titleRes: Int = -1
|
||||
|
||||
internal var onClickListener: (View) -> Unit = {}
|
||||
internal val hasValidColor get() = colorRes != -1
|
||||
internal val hasValidColorStateList get() = colorStateListRes != -1
|
||||
|
||||
fun onClicked(listener: (View) -> Unit) {
|
||||
onClickListener = listener
|
||||
}
|
||||
|
||||
internal fun title(context: Context) = if (title.isBlank()) context.getString(titleRes) else title
|
||||
internal fun colorStateList(context: Context) = context.colorStateListCompat(colorStateListRes)
|
||||
internal fun color(context: Context) = context.colorCompat(colorRes)
|
||||
}
|
||||
|
||||
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
|
||||
fun Snackbar.action(
|
||||
@StringRes actionRes: Int,
|
||||
@ColorRes colorRes: Int? = null,
|
||||
listener: (View) -> Unit
|
||||
) {
|
||||
view.resources.getString(actionRes)
|
||||
colorRes?.let { ContextCompat.getColor(view.context, colorRes) }
|
||||
action {}
|
||||
}
|
||||
|
||||
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
|
||||
fun Snackbar.action(action: String, @ColorInt color: Int? = null, listener: (View) -> Unit) {
|
||||
setAction(action, listener)
|
||||
color?.let { setActionTextColor(color) }
|
||||
}
|
||||
|
||||
fun Snackbar.textColorRes(@ColorRes colorRes: Int) {
|
||||
textColor(context.colorCompat(colorRes) ?: return)
|
||||
}
|
||||
|
||||
fun Snackbar.textColor(@ColorInt color: Int) {
|
||||
val tv = view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
|
||||
tv.setTextColor(color)
|
||||
}
|
||||
|
||||
fun Snackbar.backgroundColorRes(@ColorRes colorRes: Int) {
|
||||
backgroundColor(context.colorCompat(colorRes) ?: return)
|
||||
}
|
||||
|
||||
fun Snackbar.backgroundColor(@ColorInt color: Int) {
|
||||
ViewCompat.setBackgroundTintList(
|
||||
view,
|
||||
ColorStateList.valueOf(color)
|
||||
)
|
||||
}
|
||||
|
||||
fun Snackbar.alert() {
|
||||
textColor(0xF44336)
|
||||
}
|
||||
|
||||
fun Snackbar.success() {
|
||||
textColor(0x4CAF50)
|
||||
}
|
159
app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt
Normal file
159
app/src/main/java/com/topjohnwu/magisk/extensions/XAndroid.kt
Normal file
@@ -0,0 +1,159 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.utils.FileProvider
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
val packageName: String get() = get<Context>().packageName
|
||||
|
||||
val PackageInfo.processes
|
||||
get() = activities?.processNames.orEmpty() +
|
||||
services?.processNames.orEmpty() +
|
||||
receivers?.processNames.orEmpty() +
|
||||
providers?.processNames.orEmpty()
|
||||
|
||||
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
|
||||
|
||||
val ApplicationInfo.packageInfo: PackageInfo?
|
||||
get() {
|
||||
val pm: PackageManager by inject()
|
||||
|
||||
return try {
|
||||
val request = GET_ACTIVITIES or
|
||||
GET_SERVICES or
|
||||
GET_RECEIVERS or
|
||||
GET_PROVIDERS
|
||||
pm.getPackageInfo(packageName, request)
|
||||
} catch (e1: Exception) {
|
||||
try {
|
||||
pm.activities(packageName).apply {
|
||||
services = pm.services(packageName)
|
||||
receivers = pm.receivers(packageName)
|
||||
providers = pm.providers(packageName)
|
||||
}
|
||||
} catch (e2: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Uri.fileName: String
|
||||
get() {
|
||||
var name: String? = null
|
||||
get<Context>().contentResolver.query(this, null, null, null, null)?.use { c ->
|
||||
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1) {
|
||||
c.moveToFirst()
|
||||
name = c.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
if (name == null && path != null) {
|
||||
val idx = path!!.lastIndexOf('/')
|
||||
name = path!!.substring(idx + 1)
|
||||
}
|
||||
return name.orEmpty()
|
||||
}
|
||||
|
||||
fun PackageManager.activities(packageName: String) =
|
||||
getPackageInfo(packageName, GET_ACTIVITIES)
|
||||
|
||||
fun PackageManager.services(packageName: String) =
|
||||
getPackageInfo(packageName, GET_SERVICES).services
|
||||
|
||||
fun PackageManager.receivers(packageName: String) =
|
||||
getPackageInfo(packageName, GET_RECEIVERS).receivers
|
||||
|
||||
fun PackageManager.providers(packageName: String) =
|
||||
getPackageInfo(packageName, GET_PROVIDERS).providers
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.readUri(uri: Uri) =
|
||||
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun File.provide(context: Context = get()): Uri {
|
||||
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
|
||||
}
|
||||
|
||||
fun File.mv(destination: File) {
|
||||
inputStream().writeTo(destination)
|
||||
deleteRecursively()
|
||||
}
|
||||
|
||||
fun String.toFile() = File(this)
|
||||
|
||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
||||
|
||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||
|
||||
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
|
||||
val out = mutableListOf<Result>()
|
||||
while (moveToNext()) out.add(transformer(this))
|
||||
return out
|
||||
}
|
||||
|
||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
runCatching {
|
||||
if (labelRes > 0) {
|
||||
val res = pm.getResourcesForApplication(this)
|
||||
val config = Configuration()
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, res.displayMetrics)
|
||||
return res.getString(labelRes)
|
||||
}
|
||||
}
|
||||
|
||||
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) = ContextCompat.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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
|
||||
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())
|
@@ -0,0 +1,8 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
|
||||
fun KObservableField<Boolean>.toggle() {
|
||||
value = !value
|
||||
}
|
103
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
103
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.NoSuchElementException
|
||||
|
||||
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
var entry: ZipEntry? = nextEntry
|
||||
while (entry != null) {
|
||||
callback(entry)
|
||||
entry = nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun InputStream.writeTo(file: File) =
|
||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
||||
|
||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||
inStream: In,
|
||||
outStream: Out,
|
||||
withBoth: (In, Out) -> Unit
|
||||
) {
|
||||
inStream.use { reader ->
|
||||
outStream.use { writer ->
|
||||
withBoth(reader, writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> List<T>.firstMap(mapper: (T) -> R?): R {
|
||||
for (item: T in this) {
|
||||
return mapper(item) ?: continue
|
||||
}
|
||||
throw NoSuchElementException("Collection contains no element matching the predicate.")
|
||||
}
|
||||
|
||||
fun String.langTagToLocale(): Locale {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return Locale.forLanguageTag(this)
|
||||
} else {
|
||||
val tok = split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (tok.isEmpty()) {
|
||||
return Locale("")
|
||||
}
|
||||
val language = when (tok[0]) {
|
||||
"und" -> "" // Undefined
|
||||
"fil" -> "tl" // Filipino
|
||||
else -> tok[0]
|
||||
}
|
||||
if (language.length != 2 && language.length != 3)
|
||||
return Locale("")
|
||||
if (tok.size == 1)
|
||||
return Locale(language)
|
||||
val country = tok[1]
|
||||
|
||||
return if (country.length != 2 && country.length != 3) Locale(language)
|
||||
else Locale(language, country)
|
||||
}
|
||||
}
|
||||
|
||||
fun Locale.toLangTag(): String {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return toLanguageTag()
|
||||
} else {
|
||||
var language = language
|
||||
var country = country
|
||||
var variant = variant
|
||||
when {
|
||||
language.isEmpty() || !language.matches("\\p{Alpha}{2,8}".toRegex()) ->
|
||||
language = "und" // Follow the Locale#toLanguageTag() implementation
|
||||
language == "iw" -> language = "he" // correct deprecated "Hebrew"
|
||||
language == "in" -> language = "id" // correct deprecated "Indonesian"
|
||||
language == "ji" -> language = "yi" // correct deprecated "Yiddish"
|
||||
}
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}".toRegex())) {
|
||||
country = ""
|
||||
}
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}".toRegex())) {
|
||||
variant = ""
|
||||
}
|
||||
val tag = StringBuilder(language)
|
||||
if (country.isNotEmpty())
|
||||
tag.append('-').append(country)
|
||||
if (variant.isNotEmpty())
|
||||
tag.append('-').append(variant)
|
||||
return tag.toString()
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
@@ -6,12 +6,12 @@ import org.koin.core.qualifier.Qualifier
|
||||
|
||||
fun getKoin() = GlobalContext.get().koin
|
||||
|
||||
inline fun <reified T : Any> inject(
|
||||
inline fun <reified T> inject(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
) = lazy { get<T>(qualifier, parameters) }
|
||||
|
||||
inline fun <reified T : Any> get(
|
||||
inline fun <reified T> get(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
): T = getKoin().get(qualifier, parameters)
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.databinding.ObservableList
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import io.reactivex.disposables.Disposable
|
||||
|
||||
fun <T> MutableList<T>.update(newList: List<T>) {
|
||||
@@ -25,8 +25,8 @@ fun List<String>.toShellCmd(): String {
|
||||
}
|
||||
|
||||
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||
target: DiffObservableList<T2>,
|
||||
mapper: (List<T1>) -> List<T2>
|
||||
target: DiffObservableList<T2>,
|
||||
mapper: (List<T1>) -> List<T2>
|
||||
) = addOnListChangedCallback(object :
|
||||
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||
override fun onChanged(sender: ObservableList<T1>?) {
|
||||
@@ -76,4 +76,8 @@ fun <T1> ObservableList<T1>.copyNewInputInto(
|
||||
val addedValues = sender?.slice(positionStart until positionEnd).orEmpty()
|
||||
target.addAll(addedValues)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||
put(key, value)
|
||||
}
|
14
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
Normal file
14
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import java.io.File
|
||||
|
||||
fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
|
||||
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||
}
|
||||
|
||||
fun File.suOutputStream() = SuFileOutputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
|
||||
@@ -20,4 +20,8 @@ fun Int.res(vararg args: Any): String {
|
||||
return resources.getString(this, *args)
|
||||
}
|
||||
|
||||
fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
|
||||
fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
|
||||
|
||||
fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", "")
|
||||
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
|
||||
.replace("#", "").replace("@", "").replace("\\", "_")
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -13,8 +14,7 @@ fun String.toTime(format: DateFormat) = try {
|
||||
-1L
|
||||
}
|
||||
|
||||
private val locale get() = LocaleManager.locale
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", locale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", locale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", LocaleManager.locale) }
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
@@ -0,0 +1,37 @@
|
||||
package com.topjohnwu.magisk.model.binding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
class BindingAdapter : BindingRecyclerViewAdapter<ComparableRvItem<*>>() {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: ComparableRvItem<*>
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@@ -1,156 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.download;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
import com.topjohnwu.magisk.view.Notifications;
|
||||
import com.topjohnwu.magisk.view.ProgressNotification;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class DownloadModuleService extends Service {
|
||||
|
||||
private List<ProgressNotification> notifications;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
notifications = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Shell.EXECUTOR.execute(() -> {
|
||||
Repo repo = intent.getParcelableExtra("repo");
|
||||
boolean install = intent.getBooleanExtra("install", false);
|
||||
dlProcessInstall(repo, install);
|
||||
});
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onTaskRemoved(Intent rootIntent) {
|
||||
for (ProgressNotification n : notifications) {
|
||||
Notifications.mgr.cancel(n.hashCode());
|
||||
}
|
||||
notifications.clear();
|
||||
}
|
||||
|
||||
private synchronized void addNotification(ProgressNotification n) {
|
||||
if (notifications.isEmpty()) {
|
||||
// Start foreground
|
||||
startForeground(n.hashCode(), n.getNotification());
|
||||
}
|
||||
notifications.add(n);
|
||||
}
|
||||
|
||||
private synchronized void removeNotification(ProgressNotification n) {
|
||||
notifications.remove(n);
|
||||
if (notifications.isEmpty()) {
|
||||
// No more tasks, stop service
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
} else {
|
||||
// Pick another notification as our foreground notification
|
||||
n = notifications.get(0);
|
||||
startForeground(n.hashCode(), n.getNotification());
|
||||
}
|
||||
}
|
||||
|
||||
private void dlProcessInstall(Repo repo, boolean install) {
|
||||
File output = new File(Const.EXTERNAL_PATH, repo.getDownloadFilename());
|
||||
ProgressNotification progress = new ProgressNotification(output.getName());
|
||||
addNotification(progress);
|
||||
try {
|
||||
InputStream in = Networking.get(repo.getZipUrl())
|
||||
.setDownloadProgressListener(progress)
|
||||
.execForInputStream().getResult();
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(output));
|
||||
processZip(in, out);
|
||||
Intent intent = new Intent(this, ClassMap.get(FlashActivity.class));
|
||||
intent.setData(Uri.fromFile(output))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
|
||||
synchronized (getApplication()) {
|
||||
if (install && App.foreground() != null &&
|
||||
!(App.foreground() instanceof FlashActivity)) {
|
||||
/* Only start flashing if there is a foreground activity and the
|
||||
* user is not also flashing another module at the same time */
|
||||
App.foreground().startActivity(intent);
|
||||
} else {
|
||||
/* Or else we preset a notification notifying that we are done */
|
||||
PendingIntent pi = PendingIntent.getActivity(this, progress.hashCode(), intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
progress.dlDone(pi);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
progress.dlFail();
|
||||
}
|
||||
removeNotification(progress);
|
||||
}
|
||||
|
||||
private void processZip(InputStream in, OutputStream out)
|
||||
throws IOException {
|
||||
try (ZipInputStream zin = new ZipInputStream(in);
|
||||
ZipOutputStream zout = new ZipOutputStream(out)) {
|
||||
|
||||
// Inject latest module-installer.sh as update-binary
|
||||
zout.putNextEntry(new ZipEntry("META-INF/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/update-binary"));
|
||||
try (InputStream update_bin = Networking.get(Const.Url.MODULE_INSTALLER)
|
||||
.execForInputStream().getResult()) {
|
||||
ShellUtils.pump(update_bin, zout);
|
||||
}
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/updater-script"));
|
||||
zout.write("#MAGISK\n".getBytes("UTF-8"));
|
||||
|
||||
int off = -1;
|
||||
ZipEntry entry;
|
||||
while ((entry = zin.getNextEntry()) != null) {
|
||||
if (off < 0)
|
||||
off = entry.getName().indexOf('/') + 1;
|
||||
String path = entry.getName().substring(off);
|
||||
if (path.isEmpty())
|
||||
continue;
|
||||
if (path.startsWith("META-INF"))
|
||||
continue;
|
||||
zout.putNextEntry(new ZipEntry(path));
|
||||
if (!entry.isDirectory())
|
||||
ShellUtils.pump(zin, zout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.exists
|
||||
import com.topjohnwu.magisk.extensions.provide
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import org.koin.core.get
|
||||
import java.io.File
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
/* More of a facade for [RemoteFileService], but whatever... */
|
||||
@SuppressLint("Registered")
|
||||
open class DownloadService : RemoteFileService() {
|
||||
|
||||
private val context get() = this
|
||||
private val File.type
|
||||
get() = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(extension)
|
||||
?: "resource/folder"
|
||||
|
||||
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> onFinishedInternal(subject, id)
|
||||
is Module -> onFinishedInternal(subject, id)
|
||||
is Manager -> onFinishedInternal(subject, id)
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Magisk,
|
||||
id: Int
|
||||
) = when (val conf = subject.configuration) {
|
||||
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
|
||||
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
|
||||
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Module,
|
||||
id: Int
|
||||
) = when (subject.configuration) {
|
||||
is Flash -> FlashActivity.install(this, subject.file, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActionsInternal(subject)
|
||||
is Module -> addActionsInternal(subject)
|
||||
is Manager -> addActionsInternal(subject)
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
|
||||
= when (val conf = subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
|
||||
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
|
||||
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
|
||||
= when (subject.configuration) {
|
||||
Download -> this.apply {
|
||||
fileIntent(subject.file.parentFile!!)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
|
||||
fileIntent(subject.file)
|
||||
.takeIf { it.exists(get()) }
|
||||
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
|
||||
}
|
||||
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.setContentIntent(intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { setContentIntent(it) }
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { addAction(icon, getString(title), it) }
|
||||
|
||||
// ---
|
||||
|
||||
private fun fileIntent(file: File): Intent {
|
||||
return Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(file.provide(this), file.type)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
class Builder {
|
||||
lateinit var subject: DownloadSubject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||
val app = context.applicationContext
|
||||
val builder = Builder().apply(argBuilder)
|
||||
val intent = Intent(app, ClassMap[DownloadService::class.java])
|
||||
.putExtra(ARG_URL, builder.subject)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent)
|
||||
} else {
|
||||
app.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.content.ComponentName
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
private fun RemoteFileService.patchPackage(apk: File, id: Int) {
|
||||
if (packageName != BuildConfig.APPLICATION_ID) {
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_manager_title))
|
||||
.setContentText("")
|
||||
}
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
try {
|
||||
// Try using the new APK to patch itself
|
||||
val loader = DynamicClassLoader(apk)
|
||||
loader.loadClass("a.a")
|
||||
.getMethod("patchAPK", String::class.java, String::class.java, String::class.java)
|
||||
.invoke(null, apk.path, patched.path, packageName)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
// Fallback to use the current implementation
|
||||
PatchAPK.patch(apk.path, patched.path, packageName)
|
||||
}
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.restore_img_msg))
|
||||
.setContentText("")
|
||||
}
|
||||
Config.export()
|
||||
// Make it world readable
|
||||
apk.setReadable(true, false)
|
||||
if (Shell.su("pm install $apk").exec().isSuccess) {
|
||||
val component = ComponentName(BuildConfig.APPLICATION_ID,
|
||||
ClassMap.get<Class<*>>(SplashActivity::class.java).name)
|
||||
RootUtils.rmAndLaunch(packageName, component)
|
||||
}
|
||||
}
|
||||
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
|
||||
= when (subject.configuration) {
|
||||
is Upgrade -> patchPackage(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
|
||||
val input = ZipInputStream(buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
installer.copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||
|
||||
var off = -1
|
||||
var entry: ZipEntry? = zin.nextEntry
|
||||
while (entry != null) {
|
||||
if (off < 0) {
|
||||
off = entry.name.indexOf('/') + 1
|
||||
}
|
||||
|
||||
val path = entry.name.substring(off)
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import org.koin.core.KoinComponent
|
||||
import java.util.*
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class NotificationService : Service(), KoinComponent {
|
||||
|
||||
abstract val defaultNotification: NotificationCompat.Builder
|
||||
|
||||
private val manager by lazy { NotificationManagerCompat.from(this) }
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
private val notifications =
|
||||
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
body: (NotificationCompat.Builder) -> Unit = {}
|
||||
) {
|
||||
val notification = notifications.getOrPut(id) { defaultNotification }
|
||||
|
||||
notify(id, notification.also(body).build())
|
||||
|
||||
if (notifications.size == 1) {
|
||||
updateForeground()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun finishNotify(
|
||||
id: Int,
|
||||
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
|
||||
) : Int {
|
||||
val currentNotification = remove(id)?.run(editBody)
|
||||
|
||||
var newId = -1
|
||||
currentNotification?.let {
|
||||
newId = nextInt(Int.MAX_VALUE)
|
||||
notify(newId, it.build())
|
||||
}
|
||||
|
||||
if (!hasNotifications) {
|
||||
stopSelf()
|
||||
}
|
||||
return newId
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
manager.notify(id, notification)
|
||||
}
|
||||
|
||||
private fun cancel(id: Int) {
|
||||
manager.cancel(id)
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
cancel(id)
|
||||
updateForeground()
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications)
|
||||
startForeground(notifications.keys.first(), notifications.values.first().build())
|
||||
else
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.NullActivity
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import io.reactivex.Completable
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
private val service: GithubRawServices by inject()
|
||||
|
||||
override val defaultNotification: NotificationCompat.Builder
|
||||
get() = Notifications.progress(this, "")
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
||||
.onErrorResumeNext { download(subject) }
|
||||
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
finishNotify(subject.hashCode()) { notification ->
|
||||
notification.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
}) {
|
||||
val newId = finishNotify(subject)
|
||||
if (get<Activity>() !is NullActivity) {
|
||||
onFinished(subject, newId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||
check(subject is Magisk) { "Download cache is disabled" }
|
||||
|
||||
subject.file.also {
|
||||
check(it.exists() && ShellUtils.checkSum("MD5", it, subject.magisk.md5)) {
|
||||
"The given file does not match checksum"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||
.map { it.toStream(subject.hashCode()) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}.doOnComplete {
|
||||
if (subject is Manager)
|
||||
handleAPK(subject)
|
||||
}
|
||||
|
||||
private fun ResponseBody.toStream(id: Int): InputStream {
|
||||
val maxRaw = contentLength()
|
||||
val max = maxRaw / 1_000_000f
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it / 1_000_000f
|
||||
update(id) { notification ->
|
||||
if (maxRaw > 0) {
|
||||
notification
|
||||
.setProgress(maxRaw.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, max))
|
||||
} else {
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
||||
it.addActions(subject)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||
|
||||
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
: NotificationCompat.Builder
|
||||
|
||||
companion object {
|
||||
const val ARG_URL = "arg_url"
|
||||
}
|
||||
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
||||
|
||||
private String mId, mName, mVersion, mAuthor, mDescription;
|
||||
private int mVersionCode = -1;
|
||||
|
||||
protected BaseModule() {
|
||||
mId = mName = mVersion = mAuthor = mDescription = "";
|
||||
}
|
||||
|
||||
protected BaseModule(Cursor c) {
|
||||
mId = nonNull(c.getString(c.getColumnIndex("id")));
|
||||
mName = nonNull(c.getString(c.getColumnIndex("name")));
|
||||
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
|
||||
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
|
||||
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
|
||||
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
|
||||
}
|
||||
|
||||
protected BaseModule(Parcel p) {
|
||||
mId = p.readString();
|
||||
mName = p.readString();
|
||||
mVersion = p.readString();
|
||||
mAuthor = p.readString();
|
||||
mDescription = p.readString();
|
||||
mVersionCode = p.readInt();
|
||||
}
|
||||
|
||||
protected BaseModule(MagiskModule m) {
|
||||
mId = m.getId();
|
||||
mName = m.getName();
|
||||
mVersion = m.getVersion();
|
||||
mAuthor = m.getAuthor();
|
||||
mDescription = m.getDescription();
|
||||
mVersionCode = Integer.parseInt(m.getVersionCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull BaseModule module) {
|
||||
return getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mId);
|
||||
dest.writeString(mName);
|
||||
dest.writeString(mVersion);
|
||||
dest.writeString(mAuthor);
|
||||
dest.writeString(mDescription);
|
||||
dest.writeInt(mVersionCode);
|
||||
}
|
||||
|
||||
private String nonNull(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", mId);
|
||||
values.put("name", mName);
|
||||
values.put("version", mVersion);
|
||||
values.put("versionCode", mVersionCode);
|
||||
values.put("author", mAuthor);
|
||||
values.put("description", mDescription);
|
||||
return values;
|
||||
}
|
||||
|
||||
protected void parseProps(List<String> props) {
|
||||
parseProps(props.toArray(new String[0]));
|
||||
}
|
||||
|
||||
protected void parseProps(String[] props) throws NumberFormatException {
|
||||
for (String line : props) {
|
||||
String[] prop = line.split("=", 2);
|
||||
if (prop.length != 2)
|
||||
continue;
|
||||
|
||||
String key = prop[0].trim();
|
||||
String value = prop[1].trim();
|
||||
if (key.isEmpty() || key.charAt(0) == '#')
|
||||
continue;
|
||||
|
||||
switch (key) {
|
||||
case "id":
|
||||
mId = value;
|
||||
break;
|
||||
case "name":
|
||||
mName = value;
|
||||
break;
|
||||
case "version":
|
||||
mVersion = value;
|
||||
break;
|
||||
case "versionCode":
|
||||
mVersionCode = Integer.parseInt(value);
|
||||
break;
|
||||
case "author":
|
||||
mAuthor = value;
|
||||
break;
|
||||
case "description":
|
||||
mDescription = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return mVersion;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return mAuthor;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
mId = id;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
public int getVersionCode() {
|
||||
return mVersionCode;
|
||||
}
|
||||
|
||||
}
|
@@ -2,8 +2,8 @@ package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.utils.packageInfo
|
||||
import com.topjohnwu.magisk.utils.processes
|
||||
import com.topjohnwu.magisk.extensions.packageInfo
|
||||
import com.topjohnwu.magisk.extensions.processes
|
||||
|
||||
class HideAppInfo(
|
||||
val info: ApplicationInfo,
|
||||
|
@@ -1,11 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskApp(
|
||||
val version: String,
|
||||
val versionCode: String,
|
||||
val link: String,
|
||||
val note: String
|
||||
)
|
@@ -1,10 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskConfig(
|
||||
val app: MagiskApp,
|
||||
val uninstaller: MagiskLink,
|
||||
val magisk: MagiskFlashable
|
||||
)
|
@@ -1,13 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskFlashable(
|
||||
val version: String,
|
||||
val versionCode: String,
|
||||
val link: String,
|
||||
val note: String,
|
||||
@Json(name = "md5") val hash: String
|
||||
)
|
@@ -1,8 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskLink(
|
||||
val link: String
|
||||
)
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.topjohnwu.magisk.extensions.timeFormatTime
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.utils.timeFormatTime
|
||||
import com.topjohnwu.magisk.utils.toTime
|
||||
import java.util.*
|
||||
|
||||
data class MagiskLog(
|
||||
@@ -38,7 +38,6 @@ fun Map<String, String>.toLog(): MagiskLog {
|
||||
|
||||
fun Long.toDate() = Date(this)
|
||||
|
||||
|
||||
fun MagiskLog.toMap() = mapOf(
|
||||
"from_uid" to fromUid,
|
||||
"to_uid" to toUid,
|
||||
@@ -47,8 +46,8 @@ fun MagiskLog.toMap() = mapOf(
|
||||
"app_name" to appName,
|
||||
"command" to command,
|
||||
"action" to action,
|
||||
"time" to date
|
||||
).mapValues { it.toString() }
|
||||
"time" to date.time
|
||||
)
|
||||
|
||||
fun MagiskPolicy.toLog(
|
||||
toUid: Int,
|
||||
|
@@ -1,86 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import io.reactivex.Single
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.File
|
||||
|
||||
interface MagiskModule : Parcelable {
|
||||
val id: String
|
||||
val name: String
|
||||
val author: String
|
||||
val version: String
|
||||
val versionCode: String
|
||||
val description: String
|
||||
}
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repository(
|
||||
@PrimaryKey @NonNull
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
override val author: String,
|
||||
override val version: String,
|
||||
override val versionCode: String,
|
||||
override val description: String,
|
||||
val lastUpdate: Long
|
||||
) : MagiskModule
|
||||
|
||||
@Parcelize
|
||||
data class Module(
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
override val author: String,
|
||||
override val version: String,
|
||||
override val versionCode: String,
|
||||
override val description: String,
|
||||
val path: String
|
||||
) : MagiskModule
|
||||
|
||||
@AnyThread
|
||||
fun File.toModule(): Single<Module> {
|
||||
val path = "${Constants.MAGISK_PATH}/$name"
|
||||
return "dos2unix < $path/module.prop".su()
|
||||
.map { it.first().toModule(path) }
|
||||
}
|
||||
|
||||
fun Map<String, String>.toModule(path: String): Module {
|
||||
return Module(
|
||||
id = get("id").orEmpty(),
|
||||
name = get("name").orEmpty(),
|
||||
author = get("author").orEmpty(),
|
||||
version = get("version").orEmpty(),
|
||||
versionCode = get("versionCode").orEmpty(),
|
||||
description = get("description").orEmpty(),
|
||||
path = path
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun ResponseBody.toRepository(lastUpdate: Long) = string()
|
||||
.split(Regex("\\n"))
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { Pair(it[0], it[1]) }
|
||||
.toMap()
|
||||
.toRepository(lastUpdate)
|
||||
|
||||
@AnyThread
|
||||
fun Map<String, String>.toRepository(lastUpdate: Long) = Repository(
|
||||
id = get("id").orEmpty(),
|
||||
name = get("name").orEmpty(),
|
||||
author = get("author").orEmpty(),
|
||||
version = get("version").orEmpty(),
|
||||
versionCode = get("versionCode").orEmpty(),
|
||||
description = get("description").orEmpty(),
|
||||
lastUpdate = lastUpdate
|
||||
)
|
@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
|
||||
|
||||
|
||||
@@ -24,42 +25,14 @@ data class MagiskPolicy(
|
||||
|
||||
}
|
||||
|
||||
/*@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun ContentValues.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val uid = getAsInteger("uid")
|
||||
val packageName = getAsString("package_name")
|
||||
val info = pm.getApplicationInfo(packageName, 0)
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
return MagiskPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
policy = getAsInteger("policy"),
|
||||
until = getAsInteger("until").toLong(),
|
||||
logging = getAsInteger("logging") != 0,
|
||||
notification = getAsInteger("notification") != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
)
|
||||
}
|
||||
fun MagiskPolicy.toContentValues() = ContentValues().apply {
|
||||
put("uid", uid)
|
||||
put("uid", uid)
|
||||
put("package_name", packageName)
|
||||
put("policy", policy)
|
||||
put("until", until)
|
||||
put("logging", if (logging) 1 else 0)
|
||||
put("notification", if (notification) 1 else 0)
|
||||
}*/
|
||||
|
||||
fun MagiskPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to if (logging) 1 else 0,
|
||||
"notification" to if (notification) 1 else 0
|
||||
).mapValues { it.value.toString() }
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
@@ -78,7 +51,7 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -1,83 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
public class OldModule extends BaseModule {
|
||||
|
||||
public static final Parcelable.Creator<OldModule> CREATOR = new Creator<OldModule>() {
|
||||
/* It won't be used at any place */
|
||||
@Override
|
||||
public OldModule createFromParcel(Parcel source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OldModule[] newArray(int size) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
private final SuFile mRemoveFile;
|
||||
private final SuFile mDisableFile;
|
||||
private final SuFile mUpdateFile;
|
||||
private final boolean mUpdated;
|
||||
private boolean mEnable;
|
||||
private boolean mRemove;
|
||||
|
||||
public OldModule(String path) {
|
||||
|
||||
try {
|
||||
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
mRemoveFile = new SuFile(path, "remove");
|
||||
mDisableFile = new SuFile(path, "disable");
|
||||
mUpdateFile = new SuFile(path, "update");
|
||||
|
||||
if (getId().isEmpty()) {
|
||||
int sep = path.lastIndexOf('/');
|
||||
setId(path.substring(sep + 1));
|
||||
}
|
||||
|
||||
if (getName().isEmpty()) {
|
||||
setName(getId());
|
||||
}
|
||||
|
||||
mEnable = !mDisableFile.exists();
|
||||
mRemove = mRemoveFile.exists();
|
||||
mUpdated = mUpdateFile.exists();
|
||||
}
|
||||
|
||||
public void createDisableFile() {
|
||||
mEnable = !mDisableFile.createNewFile();
|
||||
}
|
||||
|
||||
public void removeDisableFile() {
|
||||
mEnable = mDisableFile.delete();
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mEnable;
|
||||
}
|
||||
|
||||
public void createRemoveFile() {
|
||||
mRemove = mRemoveFile.createNewFile();
|
||||
}
|
||||
|
||||
public void deleteRemoveFile() {
|
||||
mRemove = !mRemoveFile.delete();
|
||||
}
|
||||
|
||||
public boolean willBeRemoved() {
|
||||
return mRemove;
|
||||
}
|
||||
|
||||
public boolean isUpdated() {
|
||||
return mUpdated;
|
||||
}
|
||||
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
public class Policy implements Comparable<Policy>{
|
||||
public static final int INTERACTIVE = 0;
|
||||
public static final int DENY = 1;
|
||||
public static final int ALLOW = 2;
|
||||
|
||||
public int uid, policy = INTERACTIVE;
|
||||
public long until;
|
||||
public boolean logging = true, notification = true;
|
||||
public String packageName, appName;
|
||||
public ApplicationInfo info;
|
||||
|
||||
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||
String[] pkgs = pm.getPackagesForUid(uid);
|
||||
if (pkgs == null || pkgs.length == 0)
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
this.uid = uid;
|
||||
packageName = pkgs[0];
|
||||
info = pm.getApplicationInfo(packageName, 0);
|
||||
appName = Utils.getAppLabel(info, pm);
|
||||
}
|
||||
|
||||
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||
uid = values.getAsInteger("uid");
|
||||
packageName = values.getAsString("package_name");
|
||||
policy = values.getAsInteger("policy");
|
||||
until = values.getAsInteger("until");
|
||||
logging = values.getAsInteger("logging") != 0;
|
||||
notification = values.getAsInteger("notification") != 0;
|
||||
info = pm.getApplicationInfo(packageName, 0);
|
||||
if (info.uid != uid)
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
appName = info.loadLabel(pm).toString();
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("uid", uid);
|
||||
values.put("package_name", packageName);
|
||||
values.put("policy", policy);
|
||||
values.put("until", until);
|
||||
values.put("logging", logging ? 1 : 0);
|
||||
values.put("notification", notification ? 1 : 0);
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull Policy policy) {
|
||||
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
|
||||
}
|
||||
}
|
@@ -1,114 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class Repo extends BaseModule {
|
||||
|
||||
private Date mLastUpdate;
|
||||
|
||||
public Repo(String id) {
|
||||
setId(id);
|
||||
}
|
||||
|
||||
public Repo(Cursor c) {
|
||||
super(c);
|
||||
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
|
||||
}
|
||||
|
||||
public Repo(Parcel p) {
|
||||
super(p);
|
||||
mLastUpdate = new Date(p.readLong());
|
||||
}
|
||||
|
||||
public Repo(Repository repo) {
|
||||
super(repo);
|
||||
mLastUpdate = new Date(repo.getLastUpdate());
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
|
||||
|
||||
@Override
|
||||
public Repo createFromParcel(Parcel source) {
|
||||
return new Repo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repo[] newArray(int size) {
|
||||
return new Repo[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeLong(mLastUpdate.getTime());
|
||||
}
|
||||
|
||||
public void update() throws IllegalRepoException {
|
||||
String[] props = Utils.dlString(getPropUrl()).split("\\n");
|
||||
try {
|
||||
parseProps(props);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (getVersionCode() < 0) {
|
||||
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
|
||||
}
|
||||
}
|
||||
|
||||
public void update(Date lastUpdate) throws IllegalRepoException {
|
||||
mLastUpdate = lastUpdate;
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = super.getContentValues();
|
||||
values.put("last_update", mLastUpdate.getTime());
|
||||
return values;
|
||||
}
|
||||
|
||||
public String getZipUrl() {
|
||||
return String.format(Const.Url.ZIP_URL, getId());
|
||||
}
|
||||
|
||||
public String getPropUrl() {
|
||||
return getFileUrl("module.prop");
|
||||
}
|
||||
|
||||
public String getDetailUrl() {
|
||||
return getFileUrl("README.md");
|
||||
}
|
||||
|
||||
public String getFileUrl(String file) {
|
||||
return String.format(Const.Url.FILE_URL, getId(), file);
|
||||
}
|
||||
|
||||
public String getLastUpdateString() {
|
||||
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
|
||||
}
|
||||
|
||||
public Date getLastUpdate() {
|
||||
return mLastUpdate;
|
||||
}
|
||||
|
||||
public String getDownloadFilename() {
|
||||
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
|
||||
}
|
||||
|
||||
public class IllegalRepoException extends Exception {
|
||||
IllegalRepoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class SuLogEntry {
|
||||
|
||||
public int fromUid, toUid, fromPid;
|
||||
public String packageName, appName, command;
|
||||
public boolean action;
|
||||
public Date date;
|
||||
|
||||
public SuLogEntry(MagiskPolicy policy) {
|
||||
fromUid = policy.getUid();
|
||||
packageName = policy.getPackageName();
|
||||
appName = policy.getAppName();
|
||||
action = policy.getPolicy() == Policy.ALLOW;
|
||||
}
|
||||
|
||||
public SuLogEntry(ContentValues values) {
|
||||
fromUid = values.getAsInteger("from_uid");
|
||||
packageName = values.getAsString("package_name");
|
||||
appName = values.getAsString("app_name");
|
||||
fromPid = values.getAsInteger("from_pid");
|
||||
command = values.getAsString("command");
|
||||
toUid = values.getAsInteger("to_uid");
|
||||
action = values.getAsInteger("action") != 0;
|
||||
date = new Date(values.getAsLong("time"));
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("from_uid", fromUid);
|
||||
values.put("package_name", packageName);
|
||||
values.put("app_name", appName);
|
||||
values.put("from_pid", fromPid);
|
||||
values.put("command", command);
|
||||
values.put("to_uid", toUid);
|
||||
values.put("action", action ? 1 : 0);
|
||||
values.put("time", date.getTime());
|
||||
return values;
|
||||
}
|
||||
|
||||
public String getDateString() {
|
||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.getLocale()).format(date);
|
||||
}
|
||||
|
||||
public String getTimeString() {
|
||||
return new SimpleDateFormat("h:mm a", LocaleManager.getLocale()).format(date);
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class UpdateInfo(
|
||||
val app: ManagerJson = ManagerJson(),
|
||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||
val magisk: MagiskJson = MagiskJson()
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class UninstallerJson(
|
||||
val link: String = ""
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = "",
|
||||
val md5: String = ""
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonSerializable
|
||||
data class ManagerJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = ""
|
||||
) : Parcelable
|
@@ -1,3 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
data class Version(val version: String, val versionCode: Int)
|
@@ -0,0 +1,37 @@
|
||||
package com.topjohnwu.magisk.model.entity.internal
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
sealed class Configuration : Parcelable {
|
||||
|
||||
sealed class Flash : Configuration() {
|
||||
|
||||
@Parcelize
|
||||
object Primary : Flash()
|
||||
|
||||
@Parcelize
|
||||
object Secondary : Flash()
|
||||
|
||||
}
|
||||
|
||||
sealed class APK : Configuration() {
|
||||
|
||||
@Parcelize
|
||||
object Upgrade : APK()
|
||||
|
||||
@Parcelize
|
||||
object Restore : APK()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
object Download : Configuration()
|
||||
|
||||
@Parcelize
|
||||
object Uninstall : Configuration()
|
||||
|
||||
@Parcelize
|
||||
data class Patch(val fileUri: Uri) : Configuration()
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.topjohnwu.magisk.model.entity.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.extensions.cachedFile
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.entity.MagiskJson
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.File
|
||||
|
||||
sealed class DownloadSubject : Parcelable {
|
||||
|
||||
abstract val url: String
|
||||
abstract val file: File
|
||||
open val title: String get() = file.name
|
||||
|
||||
@Parcelize
|
||||
data class Module(
|
||||
val module: Repo,
|
||||
val configuration: Configuration
|
||||
) : DownloadSubject() {
|
||||
override val url: String get() = module.zipUrl
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
File(Config.downloadDirectory, module.downloadFilename)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Manager(
|
||||
val configuration: Configuration.APK
|
||||
) : DownloadSubject() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val manager: ManagerJson = Info.remote.app
|
||||
|
||||
override val title: String
|
||||
get() = "MagiskManager-v${manager.version}(${manager.versionCode})"
|
||||
|
||||
override val url: String
|
||||
get() = manager.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("manager.apk")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class Magisk : DownloadSubject() {
|
||||
|
||||
abstract val configuration: Configuration
|
||||
val magisk: MagiskJson = Info.remote.magisk
|
||||
|
||||
@Parcelize
|
||||
protected data class Flash(
|
||||
override val configuration: Configuration
|
||||
) : Magisk() {
|
||||
override val url: String get() = magisk.link
|
||||
override val title: String get() = "Magisk-v${magisk.version}(${magisk.versionCode})"
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("magisk.zip")
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Uninstall : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Uninstall
|
||||
override val url: String get() = Info.remote.uninstaller.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("uninstall.zip")
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Download : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Download
|
||||
override val url: String get() = magisk.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
File(Config.downloadDirectory, "Magisk-v${magisk.version}(${magisk.versionCode}).zip")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(configuration: Configuration) = when (configuration) {
|
||||
Configuration.Download -> Download()
|
||||
Configuration.Uninstall -> Uninstall()
|
||||
else -> Flash(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
val key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
||||
class Module(path: String) : BaseModule() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
|
||||
private val removeFile: SuFile = SuFile(path, "remove")
|
||||
private val disableFile: SuFile = SuFile(path, "disable")
|
||||
private val updateFile: SuFile = SuFile(path, "update")
|
||||
|
||||
val updated: Boolean = updateFile.exists()
|
||||
|
||||
var enable: Boolean = !disableFile.exists()
|
||||
set(enable) {
|
||||
field = if (enable) {
|
||||
disableFile.delete()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean = removeFile.exists()
|
||||
set(remove) {
|
||||
field = if (remove) {
|
||||
removeFile.createNewFile()
|
||||
} else {
|
||||
!removeFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@WorkerThread
|
||||
fun loadModules(): List<Module> {
|
||||
val moduleList = mutableListOf<Module>()
|
||||
val path = SuFile(Const.MAGISK_PATH)
|
||||
val modules =
|
||||
path.listFiles { _, name -> name != "lost+found" && name != ".core" }.orEmpty()
|
||||
for (file in modules) {
|
||||
if (file.isFile) continue
|
||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||
moduleList.add(module)
|
||||
}
|
||||
return moduleList.sortedBy { it.name }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.legalFilename
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repo(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String,
|
||||
override var author: String,
|
||||
override var version: String,
|
||||
override var versionCode: Int,
|
||||
override var description: String,
|
||||
var last_update: Long
|
||||
) : BaseModule(), Parcelable {
|
||||
|
||||
private val stringRepo: StringRepository get() = get()
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
|
||||
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
|
||||
|
||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
val readme get() = stringRepo.getReadme(this)
|
||||
|
||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||
|
||||
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update() {
|
||||
val props = runCatching {
|
||||
stringRepo.getMetadata(this).blockingGet()
|
||||
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
}.getOrElse {
|
||||
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
|
||||
}
|
||||
|
||||
props.runCatching {
|
||||
parseProps(this)
|
||||
}.onFailure {
|
||||
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update(lastUpdate: Date) {
|
||||
last_update = lastUpdate.time
|
||||
update()
|
||||
}
|
||||
|
||||
class IllegalRepoException(message: String) : Exception(message)
|
||||
|
||||
companion object {
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)!!
|
||||
}
|
||||
}
|
@@ -1,11 +1,26 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
class ConsoleRvItem(val item: String) : ComparableRvItem<ConsoleRvItem>() {
|
||||
class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_console
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
||||
val view = binding.root as TextView
|
||||
view.measure(0, 0)
|
||||
val desiredWidth = view.measuredWidth
|
||||
|
||||
view.updateLayoutParams { width = desiredWidth }
|
||||
|
||||
if (recyclerView.width < desiredWidth) {
|
||||
recyclerView.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
|
||||
}
|
@@ -1,17 +1,17 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.rxbus.RxBus
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
import com.topjohnwu.magisk.utils.RxBus
|
||||
|
||||
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
|
||||
ComparableRvItem<HideRvItem>() {
|
||||
|
@@ -0,0 +1,16 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* */
|
||||
abstract class LenientRvItem<in T> : ComparableRvItem<T>() {
|
||||
|
||||
open fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {}
|
||||
|
||||
}
|
@@ -1,14 +1,14 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.timeFormatMedium
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.timeFormatMedium
|
||||
import com.topjohnwu.magisk.utils.toTime
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_log
|
||||
|
@@ -2,48 +2,58 @@ package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.model.entity.OldModule
|
||||
import com.topjohnwu.magisk.model.entity.Repo
|
||||
import com.topjohnwu.magisk.model.entity.Repository
|
||||
import com.topjohnwu.magisk.utils.get
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.module.Module
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import com.topjohnwu.magisk.utils.KObservableField
|
||||
|
||||
class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
|
||||
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_module
|
||||
|
||||
val lastActionNotice = KObservableField("")
|
||||
val isChecked = KObservableField(item.isEnabled)
|
||||
val isDeletable = KObservableField(item.willBeRemoved())
|
||||
val isChecked = KObservableField(item.enable)
|
||||
val isDeletable = KObservableField(item.remove)
|
||||
|
||||
init {
|
||||
isChecked.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> item.removeDisableFile().notice(R.string.disable_file_removed)
|
||||
false -> item.createDisableFile().notice(R.string.disable_file_created)
|
||||
true -> {
|
||||
item.enable = true
|
||||
notice(R.string.disable_file_removed)
|
||||
}
|
||||
false -> {
|
||||
item.enable = false
|
||||
notice(R.string.disable_file_created)
|
||||
}
|
||||
}
|
||||
}
|
||||
isDeletable.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> item.createRemoveFile().notice(R.string.remove_file_created)
|
||||
false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted)
|
||||
true -> {
|
||||
item.remove = true
|
||||
notice(R.string.remove_file_created)
|
||||
}
|
||||
false -> {
|
||||
item.remove = false
|
||||
notice(R.string.remove_file_deleted)
|
||||
}
|
||||
}
|
||||
}
|
||||
when {
|
||||
item.isUpdated -> notice(R.string.update_file_created)
|
||||
item.willBeRemoved() -> notice(R.string.remove_file_created)
|
||||
item.updated -> notice(R.string.update_file_created)
|
||||
item.remove -> notice(R.string.remove_file_created)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() = isChecked.toggle()
|
||||
fun toggleDelete() = isDeletable.toggle()
|
||||
|
||||
@Suppress("unused")
|
||||
private fun Any.notice(@StringRes info: Int) {
|
||||
private fun notice(@StringRes info: Int) {
|
||||
lastActionNotice.value = get<Resources>().getString(info)
|
||||
}
|
||||
|
||||
@@ -57,15 +67,9 @@ class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
|
||||
|
||||
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
||||
|
||||
constructor(repo: Repository) : this(Repo(repo))
|
||||
|
||||
override val layoutRes: Int = R.layout.item_repo
|
||||
|
||||
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version
|
||||
&& item.lastUpdate == other.item.lastUpdate
|
||||
&& item.versionCode == other.item.versionCode
|
||||
&& item.description == other.item.description
|
||||
&& item.detailUrl == other.item.detailUrl
|
||||
override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item
|
||||
|
||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user