mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 10:27:29 +00:00
Compare commits
151 Commits
v19.4
...
manager-v7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7da97489cc | ||
![]() |
a9f11b28c8 | ||
![]() |
b31d986c8d | ||
![]() |
2dad751889 | ||
![]() |
c85b1c56af | ||
![]() |
6dd34aec47 | ||
![]() |
4cd154675f | ||
![]() |
d8d72f92b3 | ||
![]() |
a30f5b175f | ||
![]() |
8277896ca1 | ||
![]() |
493068c073 | ||
![]() |
f4299fbea8 | ||
![]() |
10ce11d671 | ||
![]() |
0f34457a10 | ||
![]() |
34c65e13bc | ||
![]() |
17a77e2577 | ||
![]() |
0f219e5ae6 | ||
![]() |
353c3c7d81 | ||
![]() |
0a89edf3b0 | ||
![]() |
e7155837d7 | ||
![]() |
31e003bda5 | ||
![]() |
490e4d3180 | ||
![]() |
dc9f69bab0 | ||
![]() |
fdf04f77f2 | ||
![]() |
5e87483f34 | ||
![]() |
f7aa451591 | ||
![]() |
321d11c2c6 | ||
![]() |
ee447bc4ce | ||
![]() |
31153e4366 | ||
![]() |
7693024c29 | ||
![]() |
9628700a2f | ||
![]() |
38576173cb | ||
![]() |
19a769c12e | ||
![]() |
3c1db7d2f7 | ||
![]() |
626507093a | ||
![]() |
588b3d14a3 | ||
![]() |
815efa7791 | ||
![]() |
97a691ce2f | ||
![]() |
9d948f2c2b | ||
![]() |
0b87108174 | ||
![]() |
7fc7809cfc | ||
![]() |
c30be20e49 | ||
![]() |
25c64db0a1 | ||
![]() |
676e9c6593 | ||
![]() |
d459859361 | ||
![]() |
2be0cef446 | ||
![]() |
294db93fde | ||
![]() |
7f971f7173 | ||
![]() |
5c7b59524d | ||
![]() |
5133e5910e | ||
![]() |
1512c350df | ||
![]() |
a5fc7891a6 | ||
![]() |
3eb9633231 | ||
![]() |
ac67b48247 | ||
![]() |
81b65ea646 | ||
![]() |
45c1f6bc27 | ||
![]() |
0d31e5c8b1 | ||
![]() |
6378abf454 | ||
![]() |
f8fcaadb5b | ||
![]() |
0b5fd3ee76 | ||
![]() |
d010cb7e42 | ||
![]() |
71136d7347 | ||
![]() |
a18c552ddf | ||
![]() |
9656878ef3 | ||
![]() |
7ded7de39a | ||
![]() |
0f74e89b44 | ||
![]() |
953c40b083 | ||
![]() |
271b0287d8 | ||
![]() |
96a8a2a8b8 | ||
![]() |
75306f658f | ||
![]() |
325d9a0b86 | ||
![]() |
a02493fbaa | ||
![]() |
9c27d691dd | ||
![]() |
935bd01f59 | ||
![]() |
eeb5d669f6 | ||
![]() |
78daa2eb62 | ||
![]() |
40eda05a30 | ||
![]() |
9f9de8c43b | ||
![]() |
a910c8ccd8 | ||
![]() |
43bda2d4a4 | ||
![]() |
c7033dd757 | ||
![]() |
5673a9bace | ||
![]() |
34ff764515 | ||
![]() |
1b3a009da7 | ||
![]() |
a49002bb2c | ||
![]() |
7342fc2307 | ||
![]() |
9867a3bd60 | ||
![]() |
5ffb9eaa5b | ||
![]() |
b05b688267 | ||
![]() |
f3d7f85063 | ||
![]() |
de969a9dab | ||
![]() |
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,8 +2,8 @@ out
|
|||||||
*.zip
|
*.zip
|
||||||
*.jks
|
*.jks
|
||||||
*.apk
|
*.apk
|
||||||
config.prop
|
/config.prop
|
||||||
update.sh
|
/update.sh
|
||||||
|
|
||||||
# Built binaries
|
# Built binaries
|
||||||
native/out
|
native/out
|
||||||
|
@@ -30,13 +30,12 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or
|
|||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Default string resources for Magisk Manager are scattered throughout
|
Default string resources for Magisk Manager and its stub APK are located here:
|
||||||
|
|
||||||
- `app/src/main/res/values/strings.xml`
|
- `app/src/main/res/values/strings.xml`
|
||||||
- `stub/src/main/res/values/strings.xml`
|
- `stub/src/main/res/values/strings.xml`
|
||||||
- `shared/src/main/res/values/strings.xml`
|
|
||||||
|
|
||||||
Translate each and place them in the respective locations (`<module>/src/main/res/values-<lang>/strings.xml`).
|
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
|
||||||
|
|
||||||
## Signature Verification
|
## Signature Verification
|
||||||
|
|
||||||
|
@@ -60,14 +60,26 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
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.ncapdevi:frag-nav:3.2.0'
|
||||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||||
|
|
||||||
def vMarkwon = '3.1.0'
|
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
|
||||||
implementation "ru.noties.markwon:core:${vMarkwon}"
|
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||||
implementation "ru.noties.markwon:html:${vMarkwon}"
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||||
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
|
|
||||||
|
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.2'
|
||||||
|
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'
|
def vLibsu = '2.5.1'
|
||||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||||
@@ -78,13 +90,13 @@ dependencies {
|
|||||||
implementation "org.koin:koin-android:${vKoin}"
|
implementation "org.koin:koin-android:${vKoin}"
|
||||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||||
|
|
||||||
def vRetrofit = '2.6.1'
|
def vRetrofit = '2.6.2'
|
||||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||||
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||||
|
|
||||||
def vOkHttp = '3.12.2'
|
def vOkHttp = '3.12.6'
|
||||||
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||||
|
|
||||||
@@ -100,20 +112,23 @@ dependencies {
|
|||||||
replacedBy('com.github.topjohnwu:room-runtime')
|
replacedBy('com.github.topjohnwu:room-runtime')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def vRoom = "2.1.0"
|
def vRoom = "2.2.1"
|
||||||
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
||||||
kapt "androidx.room:room-compiler:${vRoom}"
|
kapt "androidx.room:room-compiler:${vRoom}"
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVer}"
|
def vNav = "2.1.0"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}"
|
implementation "androidx.navigation:navigation-fragment-ktx:$vNav"
|
||||||
|
implementation "androidx.navigation:navigation-ui-ktx:$vNav"
|
||||||
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||||
implementation 'androidx.browser:browser:1.0.0'
|
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
|
||||||
implementation 'androidx.preference:preference:1.1.0'
|
implementation 'androidx.preference:preference:1.1.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04'
|
implementation 'androidx.recyclerview:recyclerview:1.1.0-rc01'
|
||||||
|
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc01'
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.work:work-runtime:2.2.0'
|
implementation 'androidx.work:work-runtime:2.2.0'
|
||||||
implementation 'androidx.transition:transition:1.2.0-rc01'
|
implementation 'androidx.transition:transition:1.3.0-rc01'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'com.google.android.material:material:1.1.0-alpha10'
|
implementation 'androidx.core:core-ktx:1.1.0'
|
||||||
|
implementation 'com.google.android.material:material:1.2.0-alpha01'
|
||||||
}
|
}
|
||||||
|
8
app/proguard-rules.pro
vendored
8
app/proguard-rules.pro
vendored
@@ -29,10 +29,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
# DelegateWorker
|
# DelegateWorker
|
||||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
|
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
|
||||||
|
|
||||||
# BootSigner
|
# BootSigner
|
||||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
-keep class a.a { *; }
|
||||||
|
|
||||||
|
# Workaround R8 bug
|
||||||
|
-keep,allowobfuscation class com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||||
|
-keepclassmembers class a.e { *; }
|
||||||
|
|
||||||
# Strip logging
|
# Strip logging
|
||||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||||
|
5
app/res-ids.txt
Normal file
5
app/res-ids.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
com.topjohnwu.magisk:color/xxxxxxxx = 0x7f010000
|
||||||
|
com.topjohnwu.magisk:drawable/xxxxxxxx = 0x7f020000
|
||||||
|
com.topjohnwu.magisk:string/xxxxxxxx = 0x7f030000
|
||||||
|
com.topjohnwu.magisk:style/xxxxxxxx = 0x7f040000
|
||||||
|
com.topjohnwu.magisk:xml/xxxxxxxx = 0x7f050000
|
@@ -1,4 +1,22 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
** Special Requirements **
|
||||||
|
|
||||||
|
This AndroidManifest.xml will be copied into the stub
|
||||||
|
APK to allow APK delegation. This is why these special
|
||||||
|
requirements exist.
|
||||||
|
|
||||||
|
* Class names *
|
||||||
|
Class names a.a, a.c, a.e should not be changed as they are used
|
||||||
|
externally. All other class names can be changed.
|
||||||
|
|
||||||
|
* Resource IDs *
|
||||||
|
All resource IDs referred in AndroidManifest.xml is required to be
|
||||||
|
included into the "shared" module to make the ID match with stub.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.topjohnwu.magisk">
|
package="com.topjohnwu.magisk">
|
||||||
@@ -11,41 +29,46 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="a.e"
|
android:name="a.e"
|
||||||
|
android:appComponentFactory="a.a"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:theme="@style/MagiskTheme"
|
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"
|
||||||
|
tools:replace="android:appComponentFactory">
|
||||||
|
|
||||||
<!-- Activities -->
|
<!-- Splash -->
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name="a.b"
|
|
||||||
android:configChanges="orientation|screenSize"
|
|
||||||
android:exported="true" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name="a.c"
|
android:name="a.c"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:exported="true"
|
|
||||||
android:theme="@style/SplashTheme">
|
android:theme="@style/SplashTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name="a.b"
|
||||||
|
android:configChanges="orientation|screenSize"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
|
<!-- Flashing -->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="a.f"
|
android:name="a.f"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:screenOrientation="nosensor"
|
android:screenOrientation="nosensor" />
|
||||||
android:theme="@style/MagiskTheme.Flashing" />
|
|
||||||
|
|
||||||
<!-- Superuser -->
|
<!-- Superuser -->
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="a.m"
|
android:name="a.m"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
android:directBootAware="true"
|
android:directBootAware="true"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:exported="false"
|
android:exported="false" />
|
||||||
android:theme="@style/MagiskTheme.SU" />
|
|
||||||
|
|
||||||
<!-- Receiver -->
|
<!-- Receiver -->
|
||||||
|
|
||||||
@@ -53,6 +76,7 @@
|
|||||||
android:name="a.h"
|
android:name="a.h"
|
||||||
android:directBootAware="true">
|
android:directBootAware="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.REBOOT" />
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -64,9 +88,10 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- Service -->
|
<!-- DownloadService -->
|
||||||
|
|
||||||
<service android:name="a.j"
|
<service
|
||||||
|
android:name="a.j"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<!-- Hardcode GMS version -->
|
<!-- Hardcode GMS version -->
|
||||||
@@ -74,6 +99,12 @@
|
|||||||
android:name="com.google.android.gms.version"
|
android:name="com.google.android.gms.version"
|
||||||
android:value="12451000" />
|
android:value="12451000" />
|
||||||
|
|
||||||
|
<!-- Initialize WorkManager on-demand -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||||
|
android:authorities="${applicationId}.workmanager-init"
|
||||||
|
tools:node="remove" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@@ -1,13 +1,22 @@
|
|||||||
package a;
|
package a;
|
||||||
|
|
||||||
|
import androidx.core.app.AppComponentFactory;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||||
import com.topjohnwu.signing.BootSigner;
|
import com.topjohnwu.signing.BootSigner;
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
public class a extends AppComponentFactory {
|
||||||
|
|
||||||
@Keep
|
@Deprecated
|
||||||
public class a extends BootSigner {
|
|
||||||
public static boolean patchAPK(String in, String out, String pkg) {
|
public static boolean patchAPK(String in, String out, String pkg) {
|
||||||
return PatchAPK.patch(in, out, pkg);
|
return PatchAPK.patch(in, out, pkg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean patchAPK(String in, String out, String pkg, String label) {
|
||||||
|
return PatchAPK.patch(in, out, pkg, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
BootSigner.main(args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,5 +3,11 @@ package a;
|
|||||||
import com.topjohnwu.magisk.App;
|
import com.topjohnwu.magisk.App;
|
||||||
|
|
||||||
public class e extends App {
|
public class e extends App {
|
||||||
/* stub */
|
public e() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public e(Object o) {
|
||||||
|
super(o);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.work.Worker;
|
import androidx.work.Worker;
|
||||||
import androidx.work.WorkerParameters;
|
import androidx.work.WorkerParameters;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.model.worker.DelegateWorker;
|
import com.topjohnwu.magisk.base.DelegateWorker;
|
||||||
|
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
|
||||||
|
@@ -6,6 +6,7 @@ import android.content.res.Configuration
|
|||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.multidex.MultiDex
|
import androidx.multidex.MultiDex
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
|
import androidx.work.WorkManager
|
||||||
import androidx.work.impl.WorkDatabase
|
import androidx.work.impl.WorkDatabase
|
||||||
import androidx.work.impl.WorkDatabase_Impl
|
import androidx.work.impl.WorkDatabase_Impl
|
||||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||||
@@ -13,21 +14,25 @@ import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
|||||||
import com.topjohnwu.magisk.di.ActivityTracker
|
import com.topjohnwu.magisk.di.ActivityTracker
|
||||||
import com.topjohnwu.magisk.di.koinModules
|
import com.topjohnwu.magisk.di.koinModules
|
||||||
import com.topjohnwu.magisk.extensions.get
|
import com.topjohnwu.magisk.extensions.get
|
||||||
import com.topjohnwu.magisk.net.Networking
|
import com.topjohnwu.magisk.extensions.unwrap
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager
|
import com.topjohnwu.magisk.utils.RootInit
|
||||||
import com.topjohnwu.magisk.utils.RootUtils
|
import com.topjohnwu.magisk.utils.updateConfig
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
open class App : Application() {
|
open class App() : Application() {
|
||||||
|
|
||||||
|
constructor(o: Any) : this() {
|
||||||
|
Info.stub = DynAPK.load(o)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||||
Shell.Config.addInitializers(RootUtils::class.java)
|
Shell.Config.addInitializers(RootInit::class.java)
|
||||||
Shell.Config.setTimeout(2)
|
Shell.Config.setTimeout(2)
|
||||||
Room.setFactory {
|
Room.setFactory {
|
||||||
when (it) {
|
when (it) {
|
||||||
@@ -39,24 +44,42 @@ open class App : Application() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context) {
|
override fun attachBaseContext(base: Context) {
|
||||||
super.attachBaseContext(base)
|
// Basic setup
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG)
|
||||||
MultiDex.install(base)
|
MultiDex.install(base)
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
|
|
||||||
|
// Some context magic
|
||||||
|
val app: Application
|
||||||
|
val impl: Context
|
||||||
|
if (base is Application) {
|
||||||
|
app = base
|
||||||
|
impl = base.baseContext
|
||||||
|
} else {
|
||||||
|
app = this
|
||||||
|
impl = base
|
||||||
|
}
|
||||||
|
val wrapped = impl.wrap()
|
||||||
|
super.attachBaseContext(wrapped)
|
||||||
|
|
||||||
|
// Normal startup
|
||||||
startKoin {
|
startKoin {
|
||||||
androidContext(this@App)
|
androidContext(wrapped)
|
||||||
modules(koinModules)
|
modules(koinModules)
|
||||||
}
|
}
|
||||||
|
ResourceMgr.init(impl)
|
||||||
|
app.registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||||
|
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
|
||||||
|
}
|
||||||
|
|
||||||
registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
// This is required as some platforms expect ContextImpl
|
||||||
|
override fun getBaseContext(): Context {
|
||||||
Networking.init(base)
|
return super.getBaseContext().unwrap()
|
||||||
LocaleManager.setLocale(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
resources.updateConfig(newConfig)
|
||||||
LocaleManager.setLocale(this)
|
if (!isRunningAsStub)
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
package com.topjohnwu.magisk
|
|
||||||
|
|
||||||
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
|
|
||||||
import com.topjohnwu.magisk.ui.SplashActivity
|
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
|
||||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
|
||||||
|
|
||||||
object ClassMap {
|
|
||||||
private val map = mapOf(
|
|
||||||
App::class.java to a.e::class.java,
|
|
||||||
MainActivity::class.java to a.b::class.java,
|
|
||||||
SplashActivity::class.java to a.c::class.java,
|
|
||||||
FlashActivity::class.java to a.f::class.java,
|
|
||||||
UpdateCheckService::class.java to a.g::class.java,
|
|
||||||
GeneralReceiver::class.java to a.h::class.java,
|
|
||||||
DownloadService::class.java to a.j::class.java,
|
|
||||||
SuRequestActivity::class.java to a.m::class.java
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun <T : Class<*>>get(c: Class<*>): T {
|
|
||||||
return map.getOrElse(c) { throw IllegalArgumentException() } as T
|
|
||||||
}
|
|
||||||
}
|
|
@@ -32,8 +32,9 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
const val ROOT_ACCESS = "root_access"
|
const val ROOT_ACCESS = "root_access"
|
||||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||||
const val SU_MNT_NS = "mnt_ns"
|
const val SU_MNT_NS = "mnt_ns"
|
||||||
const val SU_MANAGER = "requester"
|
|
||||||
const val SU_FINGERPRINT = "su_fingerprint"
|
const val SU_FINGERPRINT = "su_fingerprint"
|
||||||
|
const val SU_MANAGER = "requester"
|
||||||
|
const val KEYSTORE = "keystore"
|
||||||
|
|
||||||
// prefs
|
// prefs
|
||||||
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
||||||
@@ -97,7 +98,12 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val defaultChannel =
|
private val defaultChannel =
|
||||||
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
|
if (Utils.isCanary) {
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
Value.CANARY_DEBUG_CHANNEL
|
||||||
|
else
|
||||||
|
Value.CANARY_CHANNEL
|
||||||
|
}
|
||||||
else Value.DEFAULT_CHANNEL
|
else Value.DEFAULT_CHANNEL
|
||||||
|
|
||||||
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
@@ -123,6 +129,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||||
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
|
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
|
||||||
|
|
||||||
// Always return a path in external storage where we can write
|
// Always return a path in external storage where we can write
|
||||||
val downloadDirectory get() =
|
val downloadDirectory get() =
|
||||||
@@ -131,9 +138,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
fun initialize() = prefs.edit {
|
fun initialize() = prefs.edit {
|
||||||
parsePrefs(this)
|
parsePrefs(this)
|
||||||
|
|
||||||
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
|
||||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
|
||||||
|
|
||||||
// Get actual state
|
// Get actual state
|
||||||
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||||
|
|
||||||
@@ -142,6 +146,9 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||||
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||||
|
}.also {
|
||||||
|
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
||||||
|
prefs.edit().putString(Key.UPDATE_CHANNEL, defaultChannel.toString()).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||||
@@ -205,4 +212,4 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -13,8 +13,8 @@ object Const {
|
|||||||
|
|
||||||
// Versions
|
// Versions
|
||||||
const val SNET_EXT_VER = 13
|
const val SNET_EXT_VER = 13
|
||||||
const val SNET_REVISION = "5adbc435ce93ded953c30ebe587edfd50b5503bc"
|
const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||||
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
|
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||||
@@ -22,8 +22,9 @@ object Const {
|
|||||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||||
val USER_ID = Process.myUid() / 100000
|
val USER_ID = Process.myUid() / 100000
|
||||||
|
|
||||||
object MagiskVersion {
|
object Version {
|
||||||
const val MIN_SUPPORT = 18000
|
const val MIN_SUPPORT = 18000
|
||||||
|
const val CONNECT_MODE = 20002
|
||||||
}
|
}
|
||||||
|
|
||||||
object ID {
|
object ID {
|
||||||
|
199
app/src/main/java/com/topjohnwu/magisk/Hacks.kt
Normal file
199
app/src/main/java/com/topjohnwu/magisk/Hacks.kt
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
@file:Suppress("DEPRECATION")
|
||||||
|
|
||||||
|
package com.topjohnwu.magisk
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.job.JobInfo
|
||||||
|
import android.app.job.JobScheduler
|
||||||
|
import android.app.job.JobWorkItem
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.topjohnwu.magisk.extensions.langTagToLocale
|
||||||
|
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
|
||||||
|
import com.topjohnwu.magisk.ui.SplashActivity
|
||||||
|
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||||
|
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||||
|
import com.topjohnwu.magisk.utils.currentLocale
|
||||||
|
import com.topjohnwu.magisk.utils.defaultLocale
|
||||||
|
import com.topjohnwu.magisk.utils.refreshLocale
|
||||||
|
import com.topjohnwu.magisk.utils.updateConfig
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun AssetManager.addAssetPath(path: String) {
|
||||||
|
DynAPK.addAssetPath(this, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.wrap(global: Boolean = true): Context
|
||||||
|
= if (global) GlobalResContext(this) else ResContext(this)
|
||||||
|
|
||||||
|
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
|
||||||
|
|
||||||
|
override fun getApplicationContext(): Context {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun getSystemService(name: String): Any? {
|
||||||
|
return if (!isRunningAsStub) super.getSystemService(name) else
|
||||||
|
when (name) {
|
||||||
|
Context.JOB_SCHEDULER_SERVICE ->
|
||||||
|
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
|
||||||
|
else -> super.getSystemService(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Class<*>.cmp(pkg: String = BuildConfig.APPLICATION_ID): ComponentName {
|
||||||
|
val name = ClassMap[this].name
|
||||||
|
return ComponentName(pkg, Info.stub?.componentMap?.get(name) ?: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.intent(c: Class<*>): Intent {
|
||||||
|
val cls = ClassMap[c]
|
||||||
|
return Info.stub?.let {
|
||||||
|
val className = it.componentMap.getOrElse(cls.name) { cls.name }
|
||||||
|
Intent().setComponent(ComponentName(this, className))
|
||||||
|
} ?: Intent(this, cls)
|
||||||
|
}
|
||||||
|
|
||||||
|
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
|
||||||
|
open val mRes: Resources get() = ResourceMgr.resource
|
||||||
|
private val loader by lazy { javaClass.classLoader!! }
|
||||||
|
|
||||||
|
override fun getResources(): Resources {
|
||||||
|
return mRes
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getClassLoader(): ClassLoader {
|
||||||
|
return loader
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createConfigurationContext(config: Configuration): Context {
|
||||||
|
return ResContext(super.createConfigurationContext(config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ResContext(base: Context) : GlobalResContext(base) {
|
||||||
|
override val mRes by lazy { base.resources.patch() }
|
||||||
|
|
||||||
|
private fun Resources.patch(): Resources {
|
||||||
|
updateConfig()
|
||||||
|
if (isRunningAsStub)
|
||||||
|
assets.addAssetPath(ResourceMgr.resApk)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ResourceMgr {
|
||||||
|
|
||||||
|
lateinit var resource: Resources
|
||||||
|
lateinit var resApk: String
|
||||||
|
|
||||||
|
fun init(context: Context) {
|
||||||
|
resource = context.resources
|
||||||
|
refreshLocale()
|
||||||
|
if (isRunningAsStub) {
|
||||||
|
resApk = DynAPK.current(context).path
|
||||||
|
resource.assets.addAssetPath(resApk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = 28)
|
||||||
|
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
|
||||||
|
|
||||||
|
override fun schedule(job: JobInfo): Int {
|
||||||
|
return base.schedule(job.patch())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
|
||||||
|
return base.enqueue(job.patch(), work)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel(jobId: Int) {
|
||||||
|
base.cancel(jobId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancelAll() {
|
||||||
|
base.cancelAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllPendingJobs(): List<JobInfo> {
|
||||||
|
return base.allPendingJobs
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPendingJob(jobId: Int): JobInfo? {
|
||||||
|
return base.getPendingJob(jobId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JobInfo.patch(): JobInfo {
|
||||||
|
// We need to patch the component of JobInfo to access WorkManager SystemJobService
|
||||||
|
|
||||||
|
val name = service.className
|
||||||
|
val component = ComponentName(
|
||||||
|
service.packageName,
|
||||||
|
Info.stub!!.componentMap[name] ?: name)
|
||||||
|
|
||||||
|
// Clone the JobInfo except component
|
||||||
|
val builder = JobInfo.Builder(id, component)
|
||||||
|
.setExtras(extras)
|
||||||
|
.setTransientExtras(transientExtras)
|
||||||
|
.setClipData(clipData, clipGrantFlags)
|
||||||
|
.setRequiredNetwork(requiredNetwork)
|
||||||
|
.setEstimatedNetworkBytes(estimatedNetworkDownloadBytes, estimatedNetworkUploadBytes)
|
||||||
|
.setRequiresCharging(isRequireCharging)
|
||||||
|
.setRequiresDeviceIdle(isRequireDeviceIdle)
|
||||||
|
.setRequiresBatteryNotLow(isRequireBatteryNotLow)
|
||||||
|
.setRequiresStorageNotLow(isRequireStorageNotLow)
|
||||||
|
.also {
|
||||||
|
triggerContentUris?.let { uris ->
|
||||||
|
for (uri in uris)
|
||||||
|
it.addTriggerContentUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setTriggerContentUpdateDelay(triggerContentUpdateDelay)
|
||||||
|
.setTriggerContentMaxDelay(triggerContentMaxDelay)
|
||||||
|
.setImportantWhileForeground(isImportantWhileForeground)
|
||||||
|
.setPrefetch(isPrefetch)
|
||||||
|
.setPersisted(isPersisted)
|
||||||
|
|
||||||
|
if (isPeriodic) {
|
||||||
|
builder.setPeriodic(intervalMillis, flexMillis)
|
||||||
|
} else {
|
||||||
|
if (minLatencyMillis > 0)
|
||||||
|
builder.setMinimumLatency(minLatencyMillis)
|
||||||
|
if (maxExecutionDelayMillis > 0)
|
||||||
|
builder.setOverrideDeadline(maxExecutionDelayMillis)
|
||||||
|
}
|
||||||
|
if (!isRequireDeviceIdle)
|
||||||
|
builder.setBackoffCriteria(initialBackoffMillis, backoffPolicy)
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ClassMap {
|
||||||
|
|
||||||
|
private val map = mapOf(
|
||||||
|
App::class.java to a.e::class.java,
|
||||||
|
MainActivity::class.java to a.b::class.java,
|
||||||
|
SplashActivity::class.java to a.c::class.java,
|
||||||
|
FlashActivity::class.java to a.f::class.java,
|
||||||
|
UpdateCheckService::class.java to a.g::class.java,
|
||||||
|
GeneralReceiver::class.java to a.h::class.java,
|
||||||
|
DownloadService::class.java to a.j::class.java,
|
||||||
|
SuRequestActivity::class.java to a.m::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
operator fun get(c: Class<*>) = map.getOrElse(c) { throw IllegalArgumentException() }
|
||||||
|
}
|
@@ -1,26 +1,62 @@
|
|||||||
package com.topjohnwu.magisk
|
package com.topjohnwu.magisk
|
||||||
|
|
||||||
|
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
||||||
|
import com.topjohnwu.magisk.extensions.get
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||||
|
import com.topjohnwu.magisk.utils.CachedValue
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
|
|
||||||
|
val isRunningAsStub get() = Info.stub != null
|
||||||
|
|
||||||
object Info {
|
object Info {
|
||||||
|
|
||||||
var magiskVersionCode = -1
|
val envRef = CachedValue { loadState() }
|
||||||
|
|
||||||
var magiskVersionString = ""
|
val env by envRef // Local
|
||||||
|
var remote = UpdateInfo() // Remote
|
||||||
var remote = UpdateInfo()
|
var stub: DynAPK.Data? = null // Stub
|
||||||
|
|
||||||
var keepVerity = false
|
var keepVerity = false
|
||||||
var keepEnc = false
|
var keepEnc = false
|
||||||
var recovery = false
|
var recovery = false
|
||||||
|
|
||||||
fun loadMagiskInfo() {
|
val isConnected by lazy {
|
||||||
runCatching {
|
KObservableField(false).also { field ->
|
||||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
ReactiveNetwork.observeNetworkConnectivity(get())
|
||||||
magiskVersionCode = ShellUtils.fastCmd("magisk -V").toInt()
|
.subscribeK {
|
||||||
Config.magiskHide = Shell.su("magiskhide --status").exec().isSuccess
|
field.value = it.available()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadState() = runCatching {
|
||||||
|
val str = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
||||||
|
val code = ShellUtils.fastCmd("magisk -V").toInt()
|
||||||
|
val hide = Shell.su("magiskhide --status").exec().isSuccess
|
||||||
|
var mode = -1
|
||||||
|
if (code >= Const.Version.CONNECT_MODE) {
|
||||||
|
mode = Shell.su("magisk --connect-mode").exec().code
|
||||||
|
if (mode == 0) {
|
||||||
|
// Manually trigger broadcast test
|
||||||
|
Shell.su("magisk --broadcast-test").exec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Env(code, str, hide, mode)
|
||||||
|
}.getOrElse { Env() }
|
||||||
|
|
||||||
|
class Env(
|
||||||
|
val magiskVersionCode: Int = -1,
|
||||||
|
val magiskVersionString: String = "",
|
||||||
|
hide: Boolean = false,
|
||||||
|
var connectionMode: Int = -1
|
||||||
|
) {
|
||||||
|
val magiskHide get() = Config.magiskHide
|
||||||
|
|
||||||
|
init {
|
||||||
|
Config.magiskHide = hide
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
124
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
124
app/src/main/java/com/topjohnwu/magisk/base/BaseActivity.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
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.R
|
||||||
|
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.currentLocale
|
||||||
|
import com.topjohnwu.magisk.wrap
|
||||||
|
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 themeRes: Int = R.style.MagiskTheme
|
||||||
|
protected open val snackbarView get() = binding.root
|
||||||
|
|
||||||
|
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(base.wrap(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
setTheme(themeRes)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
app/src/main/java/com/topjohnwu/magisk/base/BaseReceiver.kt
Normal file
17
app/src/main/java/com/topjohnwu/magisk/base/BaseReceiver.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.topjohnwu.magisk.base
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
|
import android.content.Intent
|
||||||
|
import com.topjohnwu.magisk.wrap
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
|
||||||
|
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
|
||||||
|
|
||||||
|
final override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
onReceive(context.wrap() as ContextWrapper, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
|
||||||
|
}
|
12
app/src/main/java/com/topjohnwu/magisk/base/BaseService.kt
Normal file
12
app/src/main/java/com/topjohnwu/magisk/base/BaseService.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package com.topjohnwu.magisk.base
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import com.topjohnwu.magisk.wrap
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
|
||||||
|
abstract class BaseService : Service(), KoinComponent {
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base.wrap())
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package com.topjohnwu.magisk.model.worker
|
package com.topjohnwu.magisk.base
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Network
|
import android.net.Network
|
@@ -1,29 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.ui.base
|
package com.topjohnwu.magisk.base.viewmodel
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
|
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
|
||||||
import com.skoumal.teanity.extensions.doOnSubscribeUi
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.skoumal.teanity.viewmodel.LoadingViewModel
|
|
||||||
import com.topjohnwu.magisk.extensions.get
|
|
||||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.subjects.PublishSubject
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
import com.topjohnwu.magisk.Info.isConnected as gIsConnected
|
||||||
|
|
||||||
|
|
||||||
abstract class MagiskViewModel(
|
abstract class BaseViewModel(
|
||||||
initialState: State = State.LOADING
|
initialState: State = State.LOADING
|
||||||
) : LoadingViewModel(initialState) {
|
) : LoadingViewModel(initialState) {
|
||||||
|
|
||||||
val isConnected = KObservableField(true)
|
val isConnected = object : KObservableField<Boolean>(gIsConnected.value, gIsConnected) {
|
||||||
|
override fun get(): Boolean {
|
||||||
init {
|
return gIsConnected.value
|
||||||
ReactiveNetwork.observeNetworkConnectivity(get())
|
}
|
||||||
.subscribeK { isConnected.value = it.available() }
|
|
||||||
.add()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun withView(action: Activity.() -> Unit) {
|
fun withView(action: Activity.() -> Unit) {
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
@@ -19,10 +19,10 @@ interface GithubRawServices {
|
|||||||
@GET("$MAGISK_FILES/master/beta.json")
|
@GET("$MAGISK_FILES/master/beta.json")
|
||||||
fun fetchBetaUpdate(): Single<UpdateInfo>
|
fun fetchBetaUpdate(): Single<UpdateInfo>
|
||||||
|
|
||||||
@GET("$MAGISK_FILES/master/canary_builds/release.json")
|
@GET("$MAGISK_FILES/canary/release.json")
|
||||||
fun fetchCanaryUpdate(): Single<UpdateInfo>
|
fun fetchCanaryUpdate(): Single<UpdateInfo>
|
||||||
|
|
||||||
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
|
@GET("$MAGISK_FILES/canary/debug.json")
|
||||||
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
|
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@@ -29,7 +29,7 @@ class MagiskRepository(
|
|||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}.flatMap {
|
}.flatMap {
|
||||||
// If remote version is lower than current installed, try switching to beta
|
// If remote version is lower than current installed, try switching to beta
|
||||||
if (it.magisk.versionCode < Info.magiskVersionCode
|
if (it.magisk.versionCode < Info.env.magiskVersionCode
|
||||||
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
|
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
|
||||||
Config.updateChannel = Config.Value.BETA_CHANNEL
|
Config.updateChannel = Config.Value.BETA_CHANNEL
|
||||||
apiRaw.fetchBetaUpdate()
|
apiRaw.fetchBetaUpdate()
|
||||||
@@ -74,4 +74,4 @@ class MagiskRepository(
|
|||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import android.content.Context
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.skoumal.teanity.rxbus.RxBus
|
import com.topjohnwu.magisk.utils.RxBus
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
@@ -1,11 +1,18 @@
|
|||||||
package com.topjohnwu.magisk.di
|
package com.topjohnwu.magisk.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.squareup.moshi.JsonAdapter
|
import com.squareup.moshi.JsonAdapter
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
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.OkHttpClient
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
@@ -16,14 +23,15 @@ import retrofit2.converter.scalars.ScalarsConverterFactory
|
|||||||
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
||||||
|
|
||||||
val networkingModule = module {
|
val networkingModule = module {
|
||||||
single { createOkHttpClient() }
|
single { createOkHttpClient(get()) }
|
||||||
single { createMoshiConverterFactory() }
|
single { createRetrofit(get()) }
|
||||||
single { createRetrofit(get(), get()) }
|
|
||||||
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||||
|
single { createMarkwon(get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOkHttpClient(): OkHttpClient {
|
@Suppress("DEPRECATION")
|
||||||
|
fun createOkHttpClient(context: Context): OkHttpClient {
|
||||||
val builder = OkHttpClient.Builder()
|
val builder = OkHttpClient.Builder()
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
@@ -33,6 +41,10 @@ fun createOkHttpClient(): OkHttpClient {
|
|||||||
builder.addInterceptor(httpLoggingInterceptor)
|
builder.addInterceptor(httpLoggingInterceptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Networking.init(context)) {
|
||||||
|
builder.sslSocketFactory(NoSSLv3SocketFactory())
|
||||||
|
}
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,13 +55,10 @@ fun createMoshiConverterFactory(): MoshiConverterFactory {
|
|||||||
return MoshiConverterFactory.create(moshi)
|
return MoshiConverterFactory.create(moshi)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createRetrofit(
|
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
|
||||||
okHttpClient: OkHttpClient,
|
|
||||||
converterFactory: MoshiConverterFactory
|
|
||||||
): Retrofit.Builder {
|
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.addConverterFactory(ScalarsConverterFactory.create())
|
.addConverterFactory(ScalarsConverterFactory.create())
|
||||||
.addConverterFactory(converterFactory)
|
.addConverterFactory(createMoshiConverterFactory())
|
||||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
}
|
}
|
||||||
@@ -62,4 +71,13 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
|
|||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.build()
|
.build()
|
||||||
.create(T::class.java)
|
.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()
|
||||||
|
}
|
||||||
|
@@ -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)
|
||||||
|
}
|
@@ -1,6 +1,8 @@
|
|||||||
package com.topjohnwu.magisk.extensions
|
package com.topjohnwu.magisk.extensions
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.ComponentInfo
|
import android.content.pm.ComponentInfo
|
||||||
@@ -8,13 +10,32 @@ import android.content.pm.PackageInfo
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.*
|
import android.content.pm.PackageManager.*
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import com.topjohnwu.magisk.utils.FileProvider
|
import android.view.View
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.topjohnwu.magisk.Const
|
||||||
|
import com.topjohnwu.magisk.FileProvider
|
||||||
|
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||||
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.utils.currentLocale
|
import com.topjohnwu.magisk.utils.currentLocale
|
||||||
|
import com.topjohnwu.superuser.Shell
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
import java.lang.reflect.Array as JArray
|
||||||
|
|
||||||
val packageName: String get() = get<Context>().packageName
|
val packageName: String get() = get<Context>().packageName
|
||||||
|
|
||||||
@@ -83,8 +104,129 @@ fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
|||||||
fun Context.readUri(uri: Uri) =
|
fun Context.readUri(uri: Uri) =
|
||||||
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||||
|
|
||||||
|
fun Context.getBitmap(id: Int): Bitmap {
|
||||||
|
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||||
|
if (drawable is BitmapDrawable)
|
||||||
|
return drawable.bitmap
|
||||||
|
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
|
||||||
|
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
|
||||||
|
}
|
||||||
|
val bitmap = Bitmap.createBitmap(
|
||||||
|
drawable.intrinsicWidth, drawable.intrinsicHeight,
|
||||||
|
Bitmap.Config.ARGB_8888
|
||||||
|
)
|
||||||
|
val canvas = Canvas(bitmap)
|
||||||
|
drawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||||
|
|
||||||
|
fun Intent.startActivityWithRoot() {
|
||||||
|
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
|
||||||
|
val cmd = toCommand(args).joinToString(" ")
|
||||||
|
Shell.su(cmd).submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
|
||||||
|
action?.also {
|
||||||
|
args.add("-a")
|
||||||
|
args.add(it)
|
||||||
|
}
|
||||||
|
component?.also {
|
||||||
|
args.add("-n")
|
||||||
|
args.add(it.flattenToString())
|
||||||
|
}
|
||||||
|
data?.also {
|
||||||
|
args.add("-d")
|
||||||
|
args.add(it.toString())
|
||||||
|
}
|
||||||
|
categories?.also {
|
||||||
|
for (cat in it) {
|
||||||
|
args.add("-c")
|
||||||
|
args.add(cat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type?.also {
|
||||||
|
args.add("-t")
|
||||||
|
args.add(it)
|
||||||
|
}
|
||||||
|
extras?.also {
|
||||||
|
loop@ for (key in it.keySet()) {
|
||||||
|
val v = it[key] ?: continue
|
||||||
|
var value: Any = v
|
||||||
|
val arg: String
|
||||||
|
when {
|
||||||
|
v is String -> arg = "--es"
|
||||||
|
v is Boolean -> arg = "--ez"
|
||||||
|
v is Int -> arg = "--ei"
|
||||||
|
v is Long -> arg = "--el"
|
||||||
|
v is Float -> arg = "--ef"
|
||||||
|
v is Uri -> arg = "--eu"
|
||||||
|
v is ComponentName -> {
|
||||||
|
arg = "--ecn"
|
||||||
|
value = v.flattenToString()
|
||||||
|
}
|
||||||
|
v is List<*> -> {
|
||||||
|
if (v.isEmpty())
|
||||||
|
continue@loop
|
||||||
|
|
||||||
|
arg = if (v[0] is Int)
|
||||||
|
"--eial"
|
||||||
|
else if (v[0] is Long)
|
||||||
|
"--elal"
|
||||||
|
else if (v[0] is Float)
|
||||||
|
"--efal"
|
||||||
|
else if (v[0] is String)
|
||||||
|
"--esal"
|
||||||
|
else
|
||||||
|
continue@loop /* Unsupported */
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (o in v) {
|
||||||
|
sb.append(o.toString().replace(",", "\\,"))
|
||||||
|
sb.append(',')
|
||||||
|
}
|
||||||
|
// Remove trailing comma
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
value = sb
|
||||||
|
}
|
||||||
|
v.javaClass.isArray -> {
|
||||||
|
arg = if (v is IntArray)
|
||||||
|
"--eia"
|
||||||
|
else if (v is LongArray)
|
||||||
|
"--ela"
|
||||||
|
else if (v is FloatArray)
|
||||||
|
"--efa"
|
||||||
|
else if (v is Array<*> && v.isArrayOf<String>())
|
||||||
|
"--esa"
|
||||||
|
else
|
||||||
|
continue@loop /* Unsupported */
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
val len = JArray.getLength(v)
|
||||||
|
for (i in 0 until len) {
|
||||||
|
sb.append(JArray.get(v, i)!!.toString().replace(",", "\\,"))
|
||||||
|
sb.append(',')
|
||||||
|
}
|
||||||
|
// Remove trailing comma
|
||||||
|
sb.deleteCharAt(sb.length - 1)
|
||||||
|
value = sb
|
||||||
|
}
|
||||||
|
else -> continue@loop
|
||||||
|
} /* Unsupported */
|
||||||
|
|
||||||
|
args.add(arg)
|
||||||
|
args.add(key)
|
||||||
|
args.add(value.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.add("-f")
|
||||||
|
args.add(flags.toString())
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
fun File.provide(context: Context = get()): Uri {
|
fun File.provide(context: Context = get()): Uri {
|
||||||
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
|
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
|
||||||
}
|
}
|
||||||
@@ -119,3 +261,48 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
|||||||
|
|
||||||
return loadLabel(pm).toString()
|
return loadLabel(pm).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null
|
||||||
|
|
||||||
|
fun Context.colorCompat(@ColorRes id: Int) = try {
|
||||||
|
ContextCompat.getColor(this, id)
|
||||||
|
} catch (e: Resources.NotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.colorStateListCompat(@ColorRes id: Int) = try {
|
||||||
|
ContextCompat.getColorStateList(this, id)
|
||||||
|
} catch (e: Resources.NotFoundException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.drawableCompat(@DrawableRes id: Int) = 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())
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
inline fun <reified T> T.DynamicClassLoader(apk: File)
|
||||||
|
= DynamicClassLoader(apk, T::class.java.classLoader)
|
||||||
|
|
||||||
|
fun Context.unwrap() : Context {
|
||||||
|
var context = this
|
||||||
|
while (true) {
|
||||||
|
if (context is ContextWrapper)
|
||||||
|
context = context.baseContext
|
||||||
|
else
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.extensions
|
package com.topjohnwu.magisk.extensions
|
||||||
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
|
|
||||||
|
|
||||||
fun KObservableField<Boolean>.toggle() {
|
fun KObservableField<Boolean>.toggle() {
|
||||||
|
@@ -2,8 +2,7 @@ package com.topjohnwu.magisk.extensions
|
|||||||
|
|
||||||
import androidx.collection.SparseArrayCompat
|
import androidx.collection.SparseArrayCompat
|
||||||
import androidx.databinding.ObservableList
|
import androidx.databinding.ObservableList
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
import com.skoumal.teanity.util.DiffObservableList
|
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
|
|
||||||
fun <T> MutableList<T>.update(newList: List<T>) {
|
fun <T> MutableList<T>.update(newList: List<T>) {
|
||||||
@@ -26,8 +25,8 @@ fun List<String>.toShellCmd(): String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
|
||||||
target: DiffObservableList<T2>,
|
target: DiffObservableList<T2>,
|
||||||
mapper: (List<T1>) -> List<T2>
|
mapper: (List<T1>) -> List<T2>
|
||||||
) = addOnListChangedCallback(object :
|
) = addOnListChangedCallback(object :
|
||||||
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
ObservableList.OnListChangedCallback<ObservableList<T1>>() {
|
||||||
override fun onChanged(sender: ObservableList<T1>?) {
|
override fun onChanged(sender: ObservableList<T1>?) {
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.extensions
|
|
||||||
|
|
||||||
import io.reactivex.Single
|
|
||||||
import io.reactivex.functions.BiFunction
|
|
||||||
|
|
||||||
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) })
|
|
@@ -25,3 +25,5 @@ fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
|
|||||||
fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", "")
|
fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", "")
|
||||||
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
|
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
|
||||||
.replace("#", "").replace("@", "").replace("\\", "_")
|
.replace("#", "").replace("@", "").replace("\\", "_")
|
||||||
|
|
||||||
|
fun String.isEmptyInternal() = isNullOrBlank()
|
@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.binding
|
|||||||
|
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||||
|
|
||||||
|
@@ -1,22 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.model.download
|
package com.topjohnwu.magisk.model.download
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.topjohnwu.magisk.ClassMap
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.extensions.chooser
|
import com.topjohnwu.magisk.extensions.chooser
|
||||||
|
import com.topjohnwu.magisk.extensions.exists
|
||||||
import com.topjohnwu.magisk.extensions.provide
|
import com.topjohnwu.magisk.extensions.provide
|
||||||
|
import com.topjohnwu.magisk.intent
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
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.Configuration.Flash.Secondary
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||||
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.ui.flash.FlashActivity
|
||||||
import com.topjohnwu.magisk.utils.APKInstall
|
import com.topjohnwu.magisk.utils.APKInstall
|
||||||
|
import org.koin.core.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.random.Random.Default.nextInt
|
import kotlin.random.Random.Default.nextInt
|
||||||
|
|
||||||
@@ -61,50 +63,62 @@ open class DownloadService : RemoteFileService() {
|
|||||||
remove(id)
|
remove(id)
|
||||||
when (subject.configuration) {
|
when (subject.configuration) {
|
||||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||||
else -> Unit
|
is APK.Restore -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
override fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||||
= when (subject) {
|
= when (subject) {
|
||||||
is Magisk -> addActionsInternal(subject)
|
is Magisk -> addActionsInternal(subject)
|
||||||
is Module -> addActionsInternal(subject)
|
is Module -> addActionsInternal(subject)
|
||||||
is Manager -> addActionsInternal(subject)
|
is Manager -> addActionsInternal(subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
|
private fun Notification.Builder.addActionsInternal(subject: Magisk)
|
||||||
= when (val conf = subject.configuration) {
|
= when (val conf = subject.configuration) {
|
||||||
Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!))
|
Download -> this.apply {
|
||||||
.addAction(0, R.string.download_open_self, fileIntent(subject.file))
|
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))
|
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
|
||||||
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
|
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
|
||||||
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
|
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
|
||||||
else -> this
|
else -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
|
private fun Notification.Builder.addActionsInternal(subject: Module)
|
||||||
= when (subject.configuration) {
|
= when (subject.configuration) {
|
||||||
Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!))
|
Download -> this.apply {
|
||||||
.addAction(0, R.string.download_open_self, fileIntent(subject.file))
|
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))
|
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
|
||||||
else -> this
|
else -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
|
private fun Notification.Builder.addActionsInternal(subject: Manager)
|
||||||
= when (subject.configuration) {
|
= when (subject.configuration) {
|
||||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||||
else -> this
|
else -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ReplaceSingleLineLet")
|
@Suppress("ReplaceSingleLineLet")
|
||||||
private fun NotificationCompat.Builder.setContentIntent(intent: Intent) =
|
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
.let { setContentIntent(it) }
|
.let { setContentIntent(it) }
|
||||||
|
|
||||||
@Suppress("ReplaceSingleLineLet")
|
@Suppress("ReplaceSingleLineLet")
|
||||||
private fun NotificationCompat.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||||
.let { addAction(icon, getString(title), it) }
|
.let { addAction(icon, getString(title), it) }
|
||||||
|
|
||||||
@@ -115,7 +129,6 @@ open class DownloadService : RemoteFileService() {
|
|||||||
.setDataAndType(file.provide(this), file.type)
|
.setDataAndType(file.provide(this), file.type)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
.chooser()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Builder {
|
class Builder {
|
||||||
@@ -127,8 +140,7 @@ open class DownloadService : RemoteFileService() {
|
|||||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||||
val app = context.applicationContext
|
val app = context.applicationContext
|
||||||
val builder = Builder().apply(argBuilder)
|
val builder = Builder().apply(argBuilder)
|
||||||
val intent = Intent(app, ClassMap[DownloadService::class.java])
|
val intent = app.intent(DownloadService::class.java).putExtra(ARG_URL, builder.subject)
|
||||||
.putExtra(ARG_URL, builder.subject)
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
app.startForegroundService(intent)
|
app.startForegroundService(intent)
|
||||||
@@ -139,4 +151,4 @@ open class DownloadService : RemoteFileService() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,43 +1,47 @@
|
|||||||
package com.topjohnwu.magisk.model.download
|
package com.topjohnwu.magisk.model.download
|
||||||
|
|
||||||
import android.content.ComponentName
|
import com.topjohnwu.magisk.*
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.extensions.writeTo
|
||||||
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.Restore
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
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.PatchAPK
|
||||||
import com.topjohnwu.magisk.utils.RootUtils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private fun RemoteFileService.patchPackage(apk: File, id: Int) {
|
private fun RemoteFileService.patch(apk: File, id: Int) {
|
||||||
if (packageName != BuildConfig.APPLICATION_ID) {
|
if (packageName == BuildConfig.APPLICATION_ID)
|
||||||
update(id) { notification ->
|
return
|
||||||
notification.setProgress(0, 0, true)
|
|
||||||
.setProgress(0, 0, true)
|
update(id) { notification ->
|
||||||
.setContentTitle(getString(R.string.hide_manager_title))
|
notification.setProgress(0, 0, true)
|
||||||
.setContentText("")
|
.setProgress(0, 0, true)
|
||||||
}
|
.setContentTitle(getString(R.string.hide_manager_title))
|
||||||
val patched = File(apk.parent, "patched.apk")
|
.setContentText("")
|
||||||
try {
|
}
|
||||||
// Try using the new APK to patch itself
|
val patched = File(apk.parent, "patched.apk")
|
||||||
val loader = DynamicClassLoader(apk)
|
PatchAPK.patch(apk, patched, packageName, applicationInfo.nonLocalizedLabel.toString())
|
||||||
loader.loadClass("a.a")
|
apk.delete()
|
||||||
.getMethod("patchAPK", String::class.java, String::class.java, String::class.java)
|
patched.renameTo(apk)
|
||||||
.invoke(null, apk.path, patched.path, packageName)
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e)
|
private fun RemoteFileService.upgrade(apk: File, id: Int) {
|
||||||
// Fallback to use the current implementation
|
if (isRunningAsStub) {
|
||||||
PatchAPK.patch(apk.path, patched.path, packageName)
|
// Move to upgrade location
|
||||||
}
|
apk.copyTo(DynAPK.update(this), overwrite = true)
|
||||||
apk.delete()
|
apk.delete()
|
||||||
patched.renameTo(apk)
|
if (Info.stub!!.version < Info.remote.stub.versionCode) {
|
||||||
|
// We also want to upgrade stub
|
||||||
|
service.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
|
||||||
|
it.writeTo(apk)
|
||||||
|
}
|
||||||
|
patch(apk, id)
|
||||||
|
} else {
|
||||||
|
// Simply relaunch the app
|
||||||
|
ProcessPhoenix.triggerRebirth(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
patch(apk, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,15 +55,11 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
|
|||||||
Config.export()
|
Config.export()
|
||||||
// Make it world readable
|
// Make it world readable
|
||||||
apk.setReadable(true, false)
|
apk.setReadable(true, false)
|
||||||
if (Shell.su("pm install $apk").exec().isSuccess) {
|
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||||
val component = ComponentName(BuildConfig.APPLICATION_ID,
|
|
||||||
ClassMap.get<Class<*>>(SplashActivity::class.java).name)
|
|
||||||
RootUtils.rmAndLaunch(packageName, component)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
|
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
|
||||||
= when (subject.configuration) {
|
= when (subject.configuration) {
|
||||||
is Upgrade -> patchPackage(subject.file, subject.hashCode())
|
is Upgrade -> upgrade(subject.file, subject.hashCode())
|
||||||
is Restore -> restore(subject.file, subject.hashCode())
|
is Restore -> restore(subject.file, subject.hashCode())
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,22 @@
|
|||||||
package com.topjohnwu.magisk.model.download
|
package com.topjohnwu.magisk.model.download
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.Service
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationCompat
|
import com.topjohnwu.magisk.base.BaseService
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.random.Random.Default.nextInt
|
import kotlin.random.Random.Default.nextInt
|
||||||
|
|
||||||
abstract class NotificationService : Service() {
|
abstract class NotificationService : BaseService(), KoinComponent {
|
||||||
|
|
||||||
abstract val defaultNotification: NotificationCompat.Builder
|
abstract val defaultNotification: Notification.Builder
|
||||||
|
|
||||||
private val manager by lazy { NotificationManagerCompat.from(this) }
|
|
||||||
private val hasNotifications get() = notifications.isNotEmpty()
|
private val hasNotifications get() = notifications.isNotEmpty()
|
||||||
|
|
||||||
private val notifications =
|
private val notifications =
|
||||||
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
|
Collections.synchronizedMap(mutableMapOf<Int, Notification.Builder>())
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
@@ -29,7 +28,7 @@ abstract class NotificationService : Service() {
|
|||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
id: Int,
|
id: Int,
|
||||||
body: (NotificationCompat.Builder) -> Unit = {}
|
body: (Notification.Builder) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val notification = notifications.getOrPut(id) { defaultNotification }
|
val notification = notifications.getOrPut(id) { defaultNotification }
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@ abstract class NotificationService : Service() {
|
|||||||
|
|
||||||
protected fun finishNotify(
|
protected fun finishNotify(
|
||||||
id: Int,
|
id: Int,
|
||||||
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
|
editBody: (Notification.Builder) -> Notification.Builder? = { null }
|
||||||
) : Int {
|
) : Int {
|
||||||
val currentNotification = remove(id)?.run(editBody)
|
val currentNotification = remove(id)?.run(editBody)
|
||||||
|
|
||||||
@@ -61,11 +60,11 @@ abstract class NotificationService : Service() {
|
|||||||
// ---
|
// ---
|
||||||
|
|
||||||
private fun notify(id: Int, notification: Notification) {
|
private fun notify(id: Int, notification: Notification) {
|
||||||
manager.notify(id, notification)
|
Notifications.mgr.notify(id, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancel(id: Int) {
|
private fun cancel(id: Int) {
|
||||||
manager.cancel(id)
|
Notifications.mgr.cancel(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||||
@@ -83,4 +82,4 @@ abstract class NotificationService : Service() {
|
|||||||
// --
|
// --
|
||||||
|
|
||||||
override fun onBind(p0: Intent?): IBinder? = null
|
override fun onBind(p0: Intent?): IBinder? = null
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
package com.topjohnwu.magisk.model.download
|
package com.topjohnwu.magisk.model.download
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.Notification
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
import com.topjohnwu.magisk.di.NullActivity
|
import com.topjohnwu.magisk.di.NullActivity
|
||||||
import com.topjohnwu.magisk.extensions.get
|
import com.topjohnwu.magisk.extensions.get
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.extensions.writeTo
|
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.model.entity.internal.DownloadSubject.*
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||||
@@ -22,9 +22,9 @@ import java.io.InputStream
|
|||||||
|
|
||||||
abstract class RemoteFileService : NotificationService() {
|
abstract class RemoteFileService : NotificationService() {
|
||||||
|
|
||||||
private val service: GithubRawServices by inject()
|
val service: GithubRawServices by inject()
|
||||||
|
|
||||||
override val defaultNotification: NotificationCompat.Builder
|
override val defaultNotification
|
||||||
get() = Notifications.progress(this, "")
|
get() = Notifications.progress(this, "")
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
@@ -108,11 +108,11 @@ abstract class RemoteFileService : NotificationService() {
|
|||||||
@Throws(Throwable::class)
|
@Throws(Throwable::class)
|
||||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||||
|
|
||||||
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
|
||||||
: NotificationCompat.Builder
|
: Notification.Builder
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ARG_URL = "arg_url"
|
const val ARG_URL = "arg_url"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,8 @@ import se.ansman.kotshi.JsonSerializable
|
|||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val app: ManagerJson = ManagerJson(),
|
val app: ManagerJson = ManagerJson(),
|
||||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||||
val magisk: MagiskJson = MagiskJson()
|
val magisk: MagiskJson = MagiskJson(),
|
||||||
|
val stub: StubJson = StubJson()
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonSerializable
|
@JsonSerializable
|
||||||
@@ -33,3 +34,9 @@ data class ManagerJson(
|
|||||||
val link: String = "",
|
val link: String = "",
|
||||||
val note: String = ""
|
val note: String = ""
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
@JsonSerializable
|
||||||
|
data class StubJson(
|
||||||
|
val versionCode: Int = -1,
|
||||||
|
val link: String = ""
|
||||||
|
)
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.model.entity
|
|
||||||
|
|
||||||
data class Version(val version: String, val versionCode: Int)
|
|
@@ -48,7 +48,7 @@ class Module(path: String) : BaseModule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name.isEmpty()) {
|
if (name.isEmpty()) {
|
||||||
name = id;
|
name = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ class Module(path: String) : BaseModule() {
|
|||||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||||
moduleList.add(module)
|
moduleList.add(module)
|
||||||
}
|
}
|
||||||
return moduleList
|
return moduleList.sortedBy { it.name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,17 @@
|
|||||||
package com.topjohnwu.magisk.model.entity.recycler
|
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.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||||
import com.topjohnwu.magisk.extensions.inject
|
import com.topjohnwu.magisk.extensions.inject
|
||||||
import com.topjohnwu.magisk.extensions.toggle
|
import com.topjohnwu.magisk.extensions.toggle
|
||||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||||
|
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>) :
|
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
|
||||||
ComparableRvItem<HideRvItem>() {
|
ComparableRvItem<HideRvItem>() {
|
||||||
|
@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.entity.recycler
|
|||||||
|
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
package com.topjohnwu.magisk.model.entity.recycler
|
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.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
import com.topjohnwu.magisk.extensions.timeFormatMedium
|
import com.topjohnwu.magisk.extensions.timeFormatMedium
|
||||||
import com.topjohnwu.magisk.extensions.toTime
|
import com.topjohnwu.magisk.extensions.toTime
|
||||||
import com.topjohnwu.magisk.extensions.toggle
|
import com.topjohnwu.magisk.extensions.toggle
|
||||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||||
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
|
|
||||||
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||||
override val layoutRes: Int = R.layout.item_page_log
|
override val layoutRes: Int = R.layout.item_page_log
|
||||||
|
@@ -2,14 +2,14 @@ package com.topjohnwu.magisk.model.entity.recycler
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.annotation.StringRes
|
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.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||||
import com.topjohnwu.magisk.extensions.get
|
import com.topjohnwu.magisk.extensions.get
|
||||||
import com.topjohnwu.magisk.extensions.toggle
|
import com.topjohnwu.magisk.extensions.toggle
|
||||||
import com.topjohnwu.magisk.model.entity.module.Module
|
import com.topjohnwu.magisk.model.entity.module.Module
|
||||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
|
|
||||||
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||||
|
|
||||||
|
@@ -1,16 +1,16 @@
|
|||||||
package com.topjohnwu.magisk.model.entity.recycler
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
|
||||||
import com.skoumal.teanity.rxbus.RxBus
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.topjohnwu.magisk.R
|
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.inject
|
||||||
import com.topjohnwu.magisk.extensions.toggle
|
import com.topjohnwu.magisk.extensions.toggle
|
||||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||||
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
|
import com.topjohnwu.magisk.utils.RxBus
|
||||||
|
|
||||||
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.model.entity.recycler
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
|
||||||
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
|
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
|
||||||
override val layoutRes: Int = R.layout.item_section
|
override val layoutRes: Int = R.layout.item_section
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.model.entity.recycler
|
package com.topjohnwu.magisk.model.entity.recycler
|
||||||
|
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
|
||||||
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {
|
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {
|
||||||
|
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
|
internal interface EventHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called for all [ViewEvent]s published by associated viewModel.
|
||||||
|
* For [SimpleViewEvent]s, both this and [onSimpleEventDispatched]
|
||||||
|
* methods are called - you can choose the way how you handle them.
|
||||||
|
*/
|
||||||
|
fun onEventDispatched(event: ViewEvent) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called for all [SimpleViewEvent]s published by associated viewModel.
|
||||||
|
* Both this and [onEventDispatched] methods are called - you can choose
|
||||||
|
* the way how you handle them.
|
||||||
|
*/
|
||||||
|
fun onSimpleEventDispatched(event: Int) {}
|
||||||
|
|
||||||
|
val viewEventObserver get() = ViewEventObserver {
|
||||||
|
onEventDispatched(it)
|
||||||
|
if (it is SimpleViewEvent) {
|
||||||
|
onSimpleEventDispatched(it.event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,10 @@
|
|||||||
package com.topjohnwu.magisk.model.events
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
import com.skoumal.teanity.rxbus.RxBus
|
|
||||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||||
|
import com.topjohnwu.magisk.utils.RxBus
|
||||||
|
|
||||||
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
|
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
|
||||||
|
|
||||||
|
@@ -0,0 +1,5 @@
|
|||||||
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
|
class SimpleViewEvent(
|
||||||
|
val event: Int
|
||||||
|
) : ViewEvent()
|
@@ -0,0 +1,27 @@
|
|||||||
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
|
||||||
|
class SnackbarEvent private constructor(
|
||||||
|
@StringRes private val messageRes: Int,
|
||||||
|
private val messageString: String?,
|
||||||
|
val length: Int,
|
||||||
|
val f: Snackbar.() -> Unit
|
||||||
|
) : ViewEvent() {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@StringRes messageRes: Int,
|
||||||
|
length: Int = Snackbar.LENGTH_SHORT,
|
||||||
|
f: Snackbar.() -> Unit = {}
|
||||||
|
) : this(messageRes, null, length, f)
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: String,
|
||||||
|
length: Int = Snackbar.LENGTH_SHORT,
|
||||||
|
f: Snackbar.() -> Unit = {}
|
||||||
|
) : this(-1, message, length, f)
|
||||||
|
|
||||||
|
fun message(context: Context): String = messageString ?: context.getString(messageRes)
|
||||||
|
}
|
@@ -0,0 +1,17 @@
|
|||||||
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observer for [ViewEvent]s, which automatically checks if event was handled
|
||||||
|
*/
|
||||||
|
class ViewEventObserver(private val onEventUnhandled: (ViewEvent) -> Unit) : Observer<ViewEvent> {
|
||||||
|
override fun onChanged(event: ViewEvent?) {
|
||||||
|
event?.let {
|
||||||
|
if (!it.handled) {
|
||||||
|
it.handled = true
|
||||||
|
onEventUnhandled(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,19 @@
|
|||||||
package com.topjohnwu.magisk.model.events
|
package com.topjohnwu.magisk.model.events
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
import io.reactivex.subjects.PublishSubject
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for passing events from ViewModels to Activities/Fragments
|
||||||
|
* Variable [handled] used so each event is handled only once
|
||||||
|
* (see https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
|
||||||
|
* Use [ViewEventObserver] for observing these events
|
||||||
|
*/
|
||||||
|
abstract class ViewEvent {
|
||||||
|
|
||||||
|
var handled = false
|
||||||
|
}
|
||||||
|
|
||||||
data class OpenLinkEvent(val url: String) : ViewEvent()
|
data class OpenLinkEvent(val url: String) : ViewEvent()
|
||||||
|
|
||||||
@@ -35,4 +44,4 @@ class PermissionEvent(
|
|||||||
|
|
||||||
class BackPressEvent : ViewEvent()
|
class BackPressEvent : ViewEvent()
|
||||||
|
|
||||||
class DieEvent : ViewEvent()
|
class DieEvent : ViewEvent()
|
||||||
|
@@ -4,10 +4,12 @@ import android.os.Bundle
|
|||||||
import androidx.annotation.AnimRes
|
import androidx.annotation.AnimRes
|
||||||
import androidx.annotation.AnimatorRes
|
import androidx.annotation.AnimatorRes
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.skoumal.teanity.viewevents.NavigationDslMarker
|
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
@DslMarker
|
||||||
|
annotation class NavigationDslMarker
|
||||||
|
|
||||||
class MagiskNavigationEvent(
|
class MagiskNavigationEvent(
|
||||||
val navDirections: MagiskNavDirectionsBuilder,
|
val navDirections: MagiskNavDirectionsBuilder,
|
||||||
val navOptions: MagiskNavOptions,
|
val navOptions: MagiskNavOptions,
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
package com.topjohnwu.magisk.model.receiver
|
package com.topjohnwu.magisk.model.receiver
|
||||||
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.ContextWrapper
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.topjohnwu.magisk.ClassMap
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.*
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.base.BaseReceiver
|
||||||
import com.topjohnwu.magisk.Info
|
|
||||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||||
import com.topjohnwu.magisk.data.database.base.su
|
import com.topjohnwu.magisk.data.database.base.su
|
||||||
import com.topjohnwu.magisk.extensions.inject
|
|
||||||
import com.topjohnwu.magisk.extensions.reboot
|
import com.topjohnwu.magisk.extensions.reboot
|
||||||
|
import com.topjohnwu.magisk.extensions.startActivity
|
||||||
|
import com.topjohnwu.magisk.extensions.startActivityWithRoot
|
||||||
import com.topjohnwu.magisk.model.download.DownloadService
|
import com.topjohnwu.magisk.model.download.DownloadService
|
||||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||||
@@ -20,8 +19,10 @@ import com.topjohnwu.magisk.utils.SuLogger
|
|||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import org.koin.core.inject
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
open class GeneralReceiver : BroadcastReceiver() {
|
open class GeneralReceiver : BaseReceiver() {
|
||||||
|
|
||||||
private val policyDB: PolicyDao by inject()
|
private val policyDB: PolicyDao by inject()
|
||||||
|
|
||||||
@@ -36,8 +37,19 @@ open class GeneralReceiver : BroadcastReceiver() {
|
|||||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent?) {
|
override fun onReceive(context: ContextWrapper, intent: Intent?) {
|
||||||
intent ?: return
|
intent ?: return
|
||||||
|
|
||||||
|
// Debug messages
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Timber.d(intent.action)
|
||||||
|
intent.extras?.let { bundle ->
|
||||||
|
bundle.keySet().forEach {
|
||||||
|
Timber.d("[%s]=[%s]", it, bundle[it])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (intent.action ?: return) {
|
when (intent.action ?: return) {
|
||||||
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
|
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
|
||||||
val action = intent.getStringExtra("action")
|
val action = intent.getStringExtra("action")
|
||||||
@@ -51,16 +63,26 @@ open class GeneralReceiver : BroadcastReceiver() {
|
|||||||
}
|
}
|
||||||
when (action) {
|
when (action) {
|
||||||
REQUEST -> {
|
REQUEST -> {
|
||||||
val i = Intent(context, ClassMap[SuRequestActivity::class.java])
|
val i = context.intent(SuRequestActivity::class.java)
|
||||||
.setAction(action)
|
.setAction(action)
|
||||||
.putExtra("socket", intent.getStringExtra("socket"))
|
.putExtra("socket", intent.getStringExtra("socket"))
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||||
context.startActivity(i)
|
if (SDK_INT >= 29) {
|
||||||
|
// Android Q does not allow starting activity from background
|
||||||
|
i.startActivityWithRoot()
|
||||||
|
} else {
|
||||||
|
i.startActivity(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG -> SuLogger.handleLogs(context, intent)
|
||||||
|
NOTIFY -> SuLogger.handleNotify(context, intent)
|
||||||
|
TEST -> {
|
||||||
|
val mode = intent.getIntExtra("mode", 1 shl 1)
|
||||||
|
if (mode > Info.env.connectionMode)
|
||||||
|
Info.env.connectionMode = mode
|
||||||
|
Shell.su("magisk --connect-mode $mode").submit()
|
||||||
}
|
}
|
||||||
LOG -> SuLogger.handleLogs(intent)
|
|
||||||
NOTIFY -> SuLogger.handleNotify(intent)
|
|
||||||
TEST -> Shell.su("magisk --use-broadcast").submit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Intent.ACTION_PACKAGE_REPLACED ->
|
Intent.ACTION_PACKAGE_REPLACED ->
|
||||||
|
@@ -3,9 +3,9 @@ package com.topjohnwu.magisk.model.update
|
|||||||
import androidx.work.ListenableWorker
|
import androidx.work.ListenableWorker
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.Info
|
import com.topjohnwu.magisk.Info
|
||||||
|
import com.topjohnwu.magisk.base.DelegateWorker
|
||||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||||
import com.topjohnwu.magisk.extensions.inject
|
import com.topjohnwu.magisk.extensions.inject
|
||||||
import com.topjohnwu.magisk.model.worker.DelegateWorker
|
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class UpdateCheckService : DelegateWorker() {
|
|||||||
magiskRepo.fetchUpdate().blockingGet()
|
magiskRepo.fetchUpdate().blockingGet()
|
||||||
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
|
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
|
||||||
Notifications.managerUpdate(applicationContext)
|
Notifications.managerUpdate(applicationContext)
|
||||||
else if (Info.magiskVersionCode < Info.remote.magisk.versionCode)
|
else if (Info.env.magiskVersionCode < Info.remote.magisk.versionCode)
|
||||||
Notifications.magiskUpdate(applicationContext)
|
Notifications.magiskUpdate(applicationContext)
|
||||||
ListenableWorker.Result.success()
|
ListenableWorker.Result.success()
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
|
@@ -2,11 +2,11 @@ package com.topjohnwu.magisk.tasks
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.extensions.fileName
|
import com.topjohnwu.magisk.extensions.fileName
|
||||||
import com.topjohnwu.magisk.extensions.inject
|
import com.topjohnwu.magisk.extensions.inject
|
||||||
import com.topjohnwu.magisk.extensions.readUri
|
import com.topjohnwu.magisk.extensions.readUri
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.utils.unzip
|
import com.topjohnwu.magisk.utils.unzip
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
@@ -6,7 +6,6 @@ import android.os.Build
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.Info
|
import com.topjohnwu.magisk.Info
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
@@ -25,9 +24,11 @@ import org.kamranzafar.jtar.TarHeader
|
|||||||
import org.kamranzafar.jtar.TarInputStream
|
import org.kamranzafar.jtar.TarInputStream
|
||||||
import org.kamranzafar.jtar.TarOutputStream
|
import org.kamranzafar.jtar.TarOutputStream
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.*
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.*
|
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ abstract class MagiskInstaller {
|
|||||||
|
|
||||||
private val console: MutableList<String>
|
private val console: MutableList<String>
|
||||||
private val logs: MutableList<String>
|
private val logs: MutableList<String>
|
||||||
private var isTar = false
|
private var tarOut: TarOutputStream? = null
|
||||||
|
|
||||||
private val service: GithubRawServices by inject()
|
private val service: GithubRawServices by inject()
|
||||||
private val context: Context by inject()
|
private val context: Context by inject()
|
||||||
@@ -151,7 +152,9 @@ abstract class MagiskInstaller {
|
|||||||
private fun handleTar(input: InputStream) {
|
private fun handleTar(input: InputStream) {
|
||||||
console.add("- Processing tar file")
|
console.add("- Processing tar file")
|
||||||
var vbmeta = false
|
var vbmeta = false
|
||||||
withStreams(TarInputStream(input), TarOutputStream(destFile)) { tarIn, tarOut ->
|
val tarOut = TarOutputStream(destFile)
|
||||||
|
this.tarOut = tarOut
|
||||||
|
TarInputStream(input).use { tarIn ->
|
||||||
lateinit var entry: TarEntry
|
lateinit var entry: TarEntry
|
||||||
while (tarIn.nextEntry?.let { entry = it } != null) {
|
while (tarIn.nextEntry?.let { entry = it } != null) {
|
||||||
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
|
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
|
||||||
@@ -215,8 +218,7 @@ abstract class MagiskInstaller {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
it.reset()
|
it.reset()
|
||||||
if (Arrays.equals(magic, "ustar".toByteArray())) {
|
if (magic.contentEquals("ustar".toByteArray())) {
|
||||||
isTar = true
|
|
||||||
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
|
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
|
||||||
handleTar(it)
|
handleTar(it)
|
||||||
} else {
|
} else {
|
||||||
@@ -264,7 +266,7 @@ abstract class MagiskInstaller {
|
|||||||
|
|
||||||
val patched = File(installDir, "new-boot.img")
|
val patched = File(installDir, "new-boot.img")
|
||||||
if (isSigned) {
|
if (isSigned) {
|
||||||
console.add("- Signing boot image with test keys")
|
console.add("- Signing boot image with verity keys")
|
||||||
val signed = File(installDir, "signed.img")
|
val signed = File(installDir, "signed.img")
|
||||||
try {
|
try {
|
||||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
||||||
@@ -293,15 +295,13 @@ abstract class MagiskInstaller {
|
|||||||
protected fun storeBoot(): Boolean {
|
protected fun storeBoot(): Boolean {
|
||||||
val patched = SuFile.open(installDir, "new-boot.img")
|
val patched = SuFile.open(installDir, "new-boot.img")
|
||||||
try {
|
try {
|
||||||
val os: OutputStream
|
val os = tarOut?.let {
|
||||||
if (isTar) {
|
it.putNextEntry(newEntry(
|
||||||
os = TarOutputStream(destFile, true)
|
|
||||||
os.putNextEntry(newEntry(
|
|
||||||
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
|
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
|
||||||
patched.length()))
|
patched.length()))
|
||||||
} else {
|
tarOut = null
|
||||||
os = destFile.outputStream()
|
it
|
||||||
}
|
} ?: destFile.outputStream()
|
||||||
patched.suInputStream().use { it.copyTo(os); os.close() }
|
patched.suInputStream().use { it.copyTo(os); os.close() }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Failed to output to $destFile")
|
console.add("! Failed to output to $destFile")
|
||||||
|
@@ -4,15 +4,23 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
import androidx.fragment.app.FragmentTransaction
|
||||||
import com.topjohnwu.magisk.ClassMap
|
import com.ncapdevi.fragnav.FragNavController
|
||||||
import com.topjohnwu.magisk.Config
|
import com.ncapdevi.fragnav.FragNavTransactionOptions
|
||||||
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
|
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
|
||||||
import com.topjohnwu.magisk.Info
|
import com.topjohnwu.magisk.Info
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseActivity
|
||||||
|
import com.topjohnwu.magisk.base.BaseFragment
|
||||||
import com.topjohnwu.magisk.databinding.ActivityMainBinding
|
import com.topjohnwu.magisk.databinding.ActivityMainBinding
|
||||||
|
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.topjohnwu.magisk.extensions.snackbar
|
||||||
|
import com.topjohnwu.magisk.intent
|
||||||
|
import com.topjohnwu.magisk.model.events.*
|
||||||
|
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
|
||||||
|
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
|
||||||
import com.topjohnwu.magisk.model.navigation.Navigation
|
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
import com.topjohnwu.magisk.model.navigation.Navigator
|
||||||
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
|
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
|
||||||
import com.topjohnwu.magisk.ui.home.HomeFragment
|
import com.topjohnwu.magisk.ui.home.HomeFragment
|
||||||
import com.topjohnwu.magisk.ui.log.LogFragment
|
import com.topjohnwu.magisk.ui.log.LogFragment
|
||||||
@@ -23,15 +31,22 @@ import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
|
|||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
import timber.log.Timber
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Navigator,
|
||||||
open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
FragNavController.RootFragmentListener, FragNavController.TransactionListener {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.activity_main
|
override val layoutRes: Int = R.layout.activity_main
|
||||||
override val viewModel: MainViewModel by viewModel()
|
override val viewModel: MainViewModel by viewModel()
|
||||||
override val navHostId: Int = R.id.main_nav_host
|
private val navHostId: Int = R.id.main_nav_host
|
||||||
override val defaultPosition: Int = 0
|
private val defaultPosition: Int = 0
|
||||||
|
|
||||||
|
private val navigationController by lazy {
|
||||||
|
FragNavController(supportFragmentManager, navHostId)
|
||||||
|
}
|
||||||
|
private val isRootFragment get() =
|
||||||
|
navigationController.currentStackIndex != defaultPosition
|
||||||
|
|
||||||
override val baseFragments: List<KClass<out Fragment>> = listOf(
|
override val baseFragments: List<KClass<out Fragment>> = listOf(
|
||||||
HomeFragment::class,
|
HomeFragment::class,
|
||||||
@@ -43,17 +58,20 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
|||||||
SettingsFragment::class
|
SettingsFragment::class
|
||||||
)
|
)
|
||||||
|
|
||||||
/*override fun getDarkTheme(): Int {
|
|
||||||
return R.style.AppTheme_Dark
|
|
||||||
}*/
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
if (!SplashActivity.DONE) {
|
if (!SplashActivity.DONE) {
|
||||||
startActivity(Intent(this, ClassMap[SplashActivity::class.java]))
|
startActivity(intent(SplashActivity::class.java))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
navigationController.apply {
|
||||||
|
rootFragmentListener = this@MainActivity
|
||||||
|
transactionListener = this@MainActivity
|
||||||
|
initialize(defaultPosition, savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
checkHideSection()
|
checkHideSection()
|
||||||
setSupportActionBar(binding.mainInclude.mainToolbar)
|
setSupportActionBar(binding.mainInclude.mainToolbar)
|
||||||
|
|
||||||
@@ -68,6 +86,11 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
navigationController.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setTitle(title: CharSequence?) {
|
override fun setTitle(title: CharSequence?) {
|
||||||
supportActionBar?.title = title
|
supportActionBar?.title = title
|
||||||
}
|
}
|
||||||
@@ -76,25 +99,46 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
|||||||
supportActionBar?.setTitle(titleId)
|
supportActionBar?.setTitle(titleId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTabTransaction(fragment: Fragment?, index: Int) {
|
|
||||||
val fragmentId = when (fragment) {
|
|
||||||
is HomeFragment -> R.id.magiskFragment
|
|
||||||
is SuperuserFragment -> R.id.superuserFragment
|
|
||||||
is MagiskHideFragment -> R.id.magiskHideFragment
|
|
||||||
is ModulesFragment -> R.id.modulesFragment
|
|
||||||
is ReposFragment -> R.id.reposFragment
|
|
||||||
is LogFragment -> R.id.logFragment
|
|
||||||
is SettingsFragment -> R.id.settings
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
binding.navView.setCheckedItem(fragmentId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
|
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
|
||||||
binding.drawerLayout.closeDrawer(binding.navView)
|
binding.drawerLayout.closeDrawer(binding.navView)
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed()
|
val fragment = navigationController.currentFrag as? BaseFragment<*, *>
|
||||||
|
|
||||||
|
if (fragment?.onBackPressed() == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
navigationController.popFragment()
|
||||||
|
} catch (e: UnsupportedOperationException) {
|
||||||
|
when {
|
||||||
|
isRootFragment -> {
|
||||||
|
val options = FragNavTransactionOptions.newBuilder()
|
||||||
|
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
|
||||||
|
.build()
|
||||||
|
navigationController.switchTab(defaultPosition, options)
|
||||||
|
}
|
||||||
|
else -> super.onBackPressed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f)
|
||||||
|
is BackPressEvent -> onBackPressed()
|
||||||
|
is MagiskNavigationEvent -> navigateTo(event)
|
||||||
|
is ViewActionEvent -> event.action(this)
|
||||||
|
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
|
||||||
|
onSuccess { event.callback.onNext(true) }
|
||||||
|
onFailure {
|
||||||
|
event.callback.onNext(false)
|
||||||
|
event.callback.onError(SecurityException("User refused permissions"))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,14 +154,94 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
|||||||
private fun checkHideSection() {
|
private fun checkHideSection() {
|
||||||
val menu = binding.navView.menu
|
val menu = binding.navView.menu
|
||||||
menu.findItem(R.id.magiskHideFragment).isVisible =
|
menu.findItem(R.id.magiskHideFragment).isVisible =
|
||||||
Shell.rootAccess() && Config.magiskHide
|
Shell.rootAccess() && Info.env.magiskHide
|
||||||
menu.findItem(R.id.modulesFragment).isVisible =
|
menu.findItem(R.id.modulesFragment).isVisible =
|
||||||
Shell.rootAccess() && Info.magiskVersionCode >= 0
|
Shell.rootAccess() && Info.env.magiskVersionCode >= 0
|
||||||
menu.findItem(R.id.reposFragment).isVisible =
|
menu.findItem(R.id.reposFragment).isVisible =
|
||||||
(viewModel.isConnected.value && Shell.rootAccess() && Info.magiskVersionCode >= 0)
|
(viewModel.isConnected.value && Shell.rootAccess() && Info.env.magiskVersionCode >= 0)
|
||||||
menu.findItem(R.id.logFragment).isVisible =
|
menu.findItem(R.id.logFragment).isVisible =
|
||||||
Shell.rootAccess()
|
Shell.rootAccess()
|
||||||
menu.findItem(R.id.superuserFragment).isVisible =
|
menu.findItem(R.id.superuserFragment).isVisible =
|
||||||
Utils.showSuperUser()
|
Utils.showSuperUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
|
||||||
|
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
|
||||||
|
if (!options.anySet) {
|
||||||
|
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val numberOfRootFragments: Int get() = baseFragments.size
|
||||||
|
|
||||||
|
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
|
||||||
|
|
||||||
|
override fun onTabTransaction(fragment: Fragment?, index: Int) {
|
||||||
|
val fragmentId = when (fragment) {
|
||||||
|
is HomeFragment -> R.id.magiskFragment
|
||||||
|
is SuperuserFragment -> R.id.superuserFragment
|
||||||
|
is MagiskHideFragment -> R.id.magiskHideFragment
|
||||||
|
is ModulesFragment -> R.id.modulesFragment
|
||||||
|
is ReposFragment -> R.id.reposFragment
|
||||||
|
is LogFragment -> R.id.logFragment
|
||||||
|
is SettingsFragment -> R.id.settings
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
binding.navView.setCheckedItem(fragmentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun navigateTo(event: MagiskNavigationEvent) {
|
||||||
|
val directions = event.navDirections
|
||||||
|
|
||||||
|
navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
|
||||||
|
.customAnimations(event.animOptions)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
navigationController.currentStack
|
||||||
|
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
|
||||||
|
?.let { if (it == -1) null else it } // invalidate if class is not found
|
||||||
|
?.let { if (event.navOptions.inclusive) it + 1 else it }
|
||||||
|
?.let { navigationController.popFragments(it) }
|
||||||
|
|
||||||
|
when (directions.isActivity) {
|
||||||
|
true -> navigateToActivity(event)
|
||||||
|
else -> navigateToFragment(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToActivity(event: MagiskNavigationEvent) {
|
||||||
|
val destination = event.navDirections.destination?.java ?: let {
|
||||||
|
Timber.e("Cannot navigate to null destination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val options = event.navOptions
|
||||||
|
|
||||||
|
Intent(this, destination)
|
||||||
|
.putExtras(event.navDirections.args)
|
||||||
|
.apply {
|
||||||
|
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
}
|
||||||
|
.let { startActivity(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToFragment(event: MagiskNavigationEvent) {
|
||||||
|
val destination = event.navDirections.destination?.java ?: let {
|
||||||
|
Timber.e("Cannot navigate to null destination")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
|
||||||
|
-1 -> destination.newInstance()
|
||||||
|
.apply { arguments = event.navDirections.args }
|
||||||
|
.let { navigationController.pushFragment(it) }
|
||||||
|
// When it's desired that fragments of same class are put on top of one another edit this
|
||||||
|
else -> navigationController.switchTab(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFragmentTransaction(
|
||||||
|
fragment: Fragment?,
|
||||||
|
transactionType: FragNavController.TransactionType
|
||||||
|
) = Unit
|
||||||
}
|
}
|
||||||
|
@@ -2,11 +2,11 @@ package com.topjohnwu.magisk.ui
|
|||||||
|
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
import com.topjohnwu.magisk.model.navigation.Navigation
|
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
|
||||||
|
|
||||||
|
|
||||||
class MainViewModel : MagiskViewModel() {
|
class MainViewModel : BaseViewModel() {
|
||||||
|
|
||||||
fun navPressed() = Navigation.Main.OPEN_NAV.publish()
|
fun navPressed() = Navigation.Main.OPEN_NAV.publish()
|
||||||
|
|
||||||
|
@@ -1,27 +1,31 @@
|
|||||||
package com.topjohnwu.magisk.ui
|
package com.topjohnwu.magisk.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.topjohnwu.magisk.*
|
import com.topjohnwu.magisk.*
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.magisk.view.Shortcuts
|
import com.topjohnwu.magisk.view.Shortcuts
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
|
||||||
open class SplashActivity : AppCompatActivity() {
|
open class SplashActivity : Activity() {
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
super.attachBaseContext(base.wrap())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
Shell.getShell {
|
Shell.getShell {
|
||||||
if (Info.magiskVersionCode > 0 && Info.magiskVersionCode < Const.MagiskVersion.MIN_SUPPORT) {
|
if (Info.env.magiskVersionCode > 0 && Info.env.magiskVersionCode < Const.Version.MIN_SUPPORT) {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(R.string.unsupport_magisk_title)
|
.setTitle(R.string.unsupport_magisk_title)
|
||||||
.setMessage(R.string.unsupport_magisk_message)
|
.setMessage(R.string.unsupport_magisk_message)
|
||||||
.setNegativeButton(R.string.ok, null)
|
.setNegativeButton(android.R.string.ok, null)
|
||||||
.setOnDismissListener { finish() }
|
.setOnDismissListener { finish() }
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
@@ -56,7 +60,7 @@ open class SplashActivity : AppCompatActivity() {
|
|||||||
// Setup shortcuts
|
// Setup shortcuts
|
||||||
Shortcuts.setup(this)
|
Shortcuts.setup(this)
|
||||||
|
|
||||||
val intent = Intent(this, ClassMap[MainActivity::class.java])
|
val intent = intent(MainActivity::class.java)
|
||||||
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
|
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
|
||||||
DONE = true
|
DONE = true
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
@@ -1,62 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.base
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.preference.*
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
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 MagiskActivity<*, *>
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> {
|
|
||||||
return object : PreferenceGroupAdapter(preferenceScreen) {
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
|
|
||||||
super.onBindViewHolder(holder, position)
|
|
||||||
when (val preference = getItem(position)) {
|
|
||||||
is PreferenceCategory -> setZeroPaddingToLayoutChildren(holder.itemView)
|
|
||||||
else -> holder.itemView.findViewById<View>(R.id.icon_frame)?.isVisible =
|
|
||||||
preference.icon != null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setZeroPaddingToLayoutChildren(view: View) {
|
|
||||||
(view as? ViewGroup)?.children?.forEach {
|
|
||||||
setZeroPaddingToLayoutChildren(it)
|
|
||||||
} ?: return
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
|
|
||||||
view.setPaddingRelative(0, view.paddingTop, view.paddingEnd, view.paddingBottom)
|
|
||||||
else
|
|
||||||
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,242 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.base
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.collection.SparseArrayCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentTransaction
|
|
||||||
import com.karumi.dexter.Dexter
|
|
||||||
import com.karumi.dexter.MultiplePermissionsReport
|
|
||||||
import com.karumi.dexter.PermissionToken
|
|
||||||
import com.karumi.dexter.listener.PermissionRequest
|
|
||||||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
|
||||||
import com.ncapdevi.fragnav.FragNavController
|
|
||||||
import com.ncapdevi.fragnav.FragNavTransactionOptions
|
|
||||||
import com.skoumal.teanity.view.TeanityActivity
|
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.Config
|
|
||||||
import com.topjohnwu.magisk.extensions.set
|
|
||||||
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.model.navigation.MagiskAnimBuilder
|
|
||||||
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
|
|
||||||
import com.topjohnwu.magisk.model.navigation.Navigator
|
|
||||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
|
||||||
import com.topjohnwu.magisk.utils.LocaleManager
|
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.magisk.utils.currentLocale
|
|
||||||
import timber.log.Timber
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
typealias RequestCallback = MagiskActivity<*, *>.(Int, Intent?) -> Unit
|
|
||||||
|
|
||||||
abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
|
||||||
TeanityActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
|
|
||||||
Navigator, FragNavController.TransactionListener {
|
|
||||||
|
|
||||||
override val numberOfRootFragments: Int get() = baseFragments.size
|
|
||||||
override val baseFragments: List<KClass<out Fragment>> = listOf()
|
|
||||||
private val resultCallbacks = SparseArrayCompat<RequestCallback>()
|
|
||||||
|
|
||||||
|
|
||||||
protected open val defaultPosition: Int = 0
|
|
||||||
|
|
||||||
protected val navigationController get() = if (navHostId == 0) null else _navigationController
|
|
||||||
private val _navigationController by lazy {
|
|
||||||
if (navHostId == 0) throw IllegalStateException("Did you forget to override \"navHostId\"?")
|
|
||||||
FragNavController(supportFragmentManager, navHostId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val isRootFragment
|
|
||||||
get() = navigationController?.let { it.currentStackIndex != defaultPosition } ?: false
|
|
||||||
|
|
||||||
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)
|
|
||||||
navigationController?.apply {
|
|
||||||
rootFragmentListener = this@MagiskActivity
|
|
||||||
transactionListener = this@MagiskActivity
|
|
||||||
initialize(defaultPosition, savedInstanceState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
navigationController?.onSaveInstanceState(outState)
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onEventDispatched(event: ViewEvent) {
|
|
||||||
super.onEventDispatched(event)
|
|
||||||
when (event) {
|
|
||||||
is BackPressEvent -> onBackPressed()
|
|
||||||
is MagiskNavigationEvent -> navigateTo(event)
|
|
||||||
is ViewActionEvent -> event.action(this)
|
|
||||||
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
|
|
||||||
onSuccess { event.callback.onNext(true) }
|
|
||||||
onFailure {
|
|
||||||
event.callback.onNext(false)
|
|
||||||
event.callback.onError(SecurityException("User refused permissions"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
|
|
||||||
|
|
||||||
override fun navigateTo(event: MagiskNavigationEvent) {
|
|
||||||
val directions = event.navDirections
|
|
||||||
|
|
||||||
navigationController?.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
|
|
||||||
.customAnimations(event.animOptions)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
navigationController?.currentStack
|
|
||||||
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
|
|
||||||
?.let { if (it == -1) null else it } // invalidate if class is not found
|
|
||||||
?.let { if (event.navOptions.inclusive) it + 1 else it }
|
|
||||||
?.let { navigationController?.popFragments(it) }
|
|
||||||
|
|
||||||
when (directions.isActivity) {
|
|
||||||
true -> navigateToActivity(event)
|
|
||||||
else -> navigateToFragment(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToActivity(event: MagiskNavigationEvent) {
|
|
||||||
val destination = event.navDirections.destination?.java ?: let {
|
|
||||||
Timber.e("Cannot navigate to null destination")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val options = event.navOptions
|
|
||||||
|
|
||||||
Intent(this, destination)
|
|
||||||
.putExtras(event.navDirections.args)
|
|
||||||
.apply {
|
|
||||||
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
||||||
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
|
||||||
}
|
|
||||||
.let { startActivity(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun navigateToFragment(event: MagiskNavigationEvent) {
|
|
||||||
val destination = event.navDirections.destination?.java ?: let {
|
|
||||||
Timber.e("Cannot navigate to null destination")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
|
|
||||||
-1 -> destination.newInstance()
|
|
||||||
.apply { arguments = event.navDirections.args }
|
|
||||||
.let { navigationController?.pushFragment(it) }
|
|
||||||
// When it's desired that fragments of same class are put on top of one another edit this
|
|
||||||
else -> navigationController?.switchTab(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed() {
|
|
||||||
val fragment = navigationController?.currentFrag as? MagiskFragment<*, *>
|
|
||||||
|
|
||||||
if (fragment?.onBackPressed() == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
navigationController?.popFragment() ?: throw UnsupportedOperationException()
|
|
||||||
} catch (e: UnsupportedOperationException) {
|
|
||||||
when {
|
|
||||||
isRootFragment -> {
|
|
||||||
val options = FragNavTransactionOptions.newBuilder()
|
|
||||||
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
|
|
||||||
.build()
|
|
||||||
navigationController?.switchTab(defaultPosition, options)
|
|
||||||
}
|
|
||||||
else -> super.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentTransaction(
|
|
||||||
fragment: Fragment?,
|
|
||||||
transactionType: FragNavController.TransactionType
|
|
||||||
) = Unit
|
|
||||||
|
|
||||||
override fun onTabTransaction(fragment: Fragment?, index: Int) = Unit
|
|
||||||
|
|
||||||
fun openUrl(url: String) = Utils.openLink(this, url.toUri())
|
|
||||||
|
|
||||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
|
||||||
val request = PermissionRequestBuilder().apply(builder).build()
|
|
||||||
Dexter.withActivity(this)
|
|
||||||
.withPermissions(*permissions)
|
|
||||||
.withListener(object : MultiplePermissionsListener {
|
|
||||||
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
|
|
||||||
if (report.areAllPermissionsGranted()) {
|
|
||||||
request.onSuccess()
|
|
||||||
} else {
|
|
||||||
request.onFailure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPermissionRationaleShouldBeShown(
|
|
||||||
permissions: MutableList<PermissionRequest>,
|
|
||||||
token: PermissionToken
|
|
||||||
) = token.continuePermissionRequest()
|
|
||||||
}).check()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
|
||||||
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
|
|
||||||
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
|
|
||||||
if (!options.anySet) {
|
|
||||||
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
resultCallbacks[requestCode]?.apply {
|
|
||||||
resultCallbacks.remove(requestCode)
|
|
||||||
invoke(this@MagiskActivity, resultCode, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startActivityForResult(
|
|
||||||
intent: Intent,
|
|
||||||
requestCode: Int,
|
|
||||||
listener: RequestCallback
|
|
||||||
) {
|
|
||||||
resultCallbacks[requestCode] = listener
|
|
||||||
startActivityForResult(intent, requestCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,52 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.ui.base
|
|
||||||
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.skoumal.teanity.view.TeanityFragment
|
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
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.model.navigation.MagiskNavigationEvent
|
|
||||||
import com.topjohnwu.magisk.model.navigation.Navigator
|
|
||||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
|
|
||||||
abstract class MagiskFragment<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
|
||||||
TeanityFragment<ViewModel, Binding>(), Navigator {
|
|
||||||
|
|
||||||
protected val activity get() = requireActivity() as MagiskActivity<*, *>
|
|
||||||
|
|
||||||
// We don't need nested fragments
|
|
||||||
override val baseFragments: List<KClass<Fragment>> = listOf()
|
|
||||||
|
|
||||||
override fun navigateTo(event: MagiskNavigationEvent) = activity.navigateTo(event)
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onEventDispatched(event: ViewEvent) {
|
|
||||||
super.onEventDispatched(event)
|
|
||||||
when (event) {
|
|
||||||
is BackPressEvent -> activity.onBackPressed()
|
|
||||||
is MagiskNavigationEvent -> navigateTo(event)
|
|
||||||
is ViewActionEvent -> event.action(requireActivity())
|
|
||||||
is PermissionEvent -> activity.withPermissions(*event.permissions.toTypedArray()) {
|
|
||||||
onSuccess { event.callback.onNext(true) }
|
|
||||||
onFailure {
|
|
||||||
event.callback.onNext(false)
|
|
||||||
event.callback.onError(SecurityException("User refused permissions"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
|
|
||||||
activity.withPermissions(*permissions, builder = builder)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openLink(url: String) = activity.openUrl(url)
|
|
||||||
|
|
||||||
open fun onBackPressed(): Boolean = false
|
|
||||||
|
|
||||||
}
|
|
@@ -6,18 +6,24 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.topjohnwu.magisk.ClassMap
|
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
|
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
import com.topjohnwu.magisk.extensions.snackbar
|
||||||
|
import com.topjohnwu.magisk.intent
|
||||||
|
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>() {
|
open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>() {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.activity_flash
|
override val layoutRes: Int = R.layout.activity_flash
|
||||||
|
override val themeRes: Int = R.style.MagiskTheme_Flashing
|
||||||
override val viewModel: FlashViewModel by viewModel {
|
override val viewModel: FlashViewModel by viewModel {
|
||||||
val uri = intent.data ?: let { finish(); Uri.EMPTY }
|
val uri = intent.data ?: let { finish(); Uri.EMPTY }
|
||||||
val additionalUri = intent.getParcelableExtra(Const.Key.FLASH_DATA) ?: uri
|
val additionalUri = intent.getParcelableExtra(Const.Key.FLASH_DATA) ?: uri
|
||||||
@@ -37,9 +43,24 @@ open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>(
|
|||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
|
super.onEventDispatched(event)
|
||||||
|
when (event) {
|
||||||
|
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f)
|
||||||
|
is BackPressEvent -> onBackPressed()
|
||||||
|
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
|
||||||
|
onSuccess { event.callback.onNext(true) }
|
||||||
|
onFailure {
|
||||||
|
event.callback.onNext(false)
|
||||||
|
event.callback.onError(SecurityException("User refused permissions"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private fun intent(context: Context) = Intent(context, ClassMap[FlashActivity::class.java])
|
private fun intent(context: Context) = context.intent(FlashActivity::class.java)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
private fun intent(context: Context, file: File) = intent(context).setData(file.toUri())
|
private fun intent(context: Context, file: File) = intent(context).setData(file.toUri())
|
||||||
|
|
||||||
|
@@ -7,21 +7,20 @@ import android.net.Uri
|
|||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.databinding.ObservableArrayList
|
import androidx.databinding.ObservableArrayList
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.util.DiffObservableList
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.skoumal.teanity.viewevents.SnackbarEvent
|
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
import com.topjohnwu.magisk.extensions.*
|
import com.topjohnwu.magisk.extensions.*
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
|
||||||
|
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.model.flash.FlashResultListener
|
import com.topjohnwu.magisk.model.flash.FlashResultListener
|
||||||
import com.topjohnwu.magisk.model.flash.Flashing
|
import com.topjohnwu.magisk.model.flash.Flashing
|
||||||
import com.topjohnwu.magisk.model.flash.Patching
|
import com.topjohnwu.magisk.model.flash.Patching
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -32,7 +31,7 @@ class FlashViewModel(
|
|||||||
installer: Uri,
|
installer: Uri,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
private val resources: Resources
|
private val resources: Resources
|
||||||
) : MagiskViewModel(), FlashResultListener {
|
) : BaseViewModel(), FlashResultListener {
|
||||||
|
|
||||||
val canShowReboot = Shell.rootAccess()
|
val canShowReboot = Shell.rootAccess()
|
||||||
val showRestartTitle = KObservableField(false)
|
val showRestartTitle = KObservableField(false)
|
||||||
|
@@ -1,28 +1,28 @@
|
|||||||
package com.topjohnwu.magisk.ui.hide
|
package com.topjohnwu.magisk.ui.hide
|
||||||
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.rxbus.RxBus
|
|
||||||
import com.skoumal.teanity.util.DiffObservableList
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.extensions.toSingle
|
import com.topjohnwu.magisk.extensions.toSingle
|
||||||
import com.topjohnwu.magisk.extensions.update
|
import com.topjohnwu.magisk.extensions.update
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
|
import com.topjohnwu.magisk.utils.RxBus
|
||||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class HideViewModel(
|
class HideViewModel(
|
||||||
private val magiskRepo: MagiskRepository,
|
private val magiskRepo: MagiskRepository,
|
||||||
rxBus: RxBus
|
rxBus: RxBus
|
||||||
) : MagiskViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val query = KObservableField("")
|
val query = KObservableField("")
|
||||||
val isShowSystem = KObservableField(false)
|
val isShowSystem = KObservableField(false)
|
||||||
|
@@ -6,11 +6,11 @@ import android.view.MenuItem
|
|||||||
import android.widget.SearchView
|
import android.widget.SearchView
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
|
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBinding>(),
|
class MagiskHideFragment : BaseFragment<HideViewModel, FragmentMagiskHideBinding>(),
|
||||||
SearchView.OnQueryTextListener {
|
SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.fragment_magisk_hide
|
override val layoutRes: Int = R.layout.fragment_magisk_hide
|
||||||
|
@@ -1,31 +1,31 @@
|
|||||||
package com.topjohnwu.magisk.ui.home
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.Info
|
import com.topjohnwu.magisk.Info
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseActivity
|
||||||
|
import com.topjohnwu.magisk.base.BaseFragment
|
||||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||||
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
|
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
|
||||||
import com.topjohnwu.magisk.extensions.inject
|
import com.topjohnwu.magisk.extensions.DynamicClassLoader
|
||||||
|
import com.topjohnwu.magisk.extensions.openUrl
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.extensions.writeTo
|
import com.topjohnwu.magisk.extensions.writeTo
|
||||||
import com.topjohnwu.magisk.model.events.*
|
import com.topjohnwu.magisk.model.events.*
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
|
||||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
|
||||||
import com.topjohnwu.magisk.utils.SafetyNetHelper
|
import com.topjohnwu.magisk.utils.SafetyNetHelper
|
||||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||||
import com.topjohnwu.magisk.view.dialogs.*
|
import com.topjohnwu.magisk.view.dialogs.*
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import dalvik.system.DexFile
|
import dalvik.system.DexFile
|
||||||
import io.reactivex.Completable
|
import io.reactivex.Completable
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.InvocationHandler
|
import java.lang.reflect.InvocationHandler
|
||||||
|
|
||||||
class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
class HomeFragment : BaseFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||||
SafetyNetHelper.Callback {
|
SafetyNetHelper.Callback {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.fragment_magisk
|
override val layoutRes: Int = R.layout.fragment_magisk
|
||||||
@@ -33,13 +33,14 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
|||||||
|
|
||||||
private val magiskRepo: MagiskRepository by inject()
|
private val magiskRepo: MagiskRepository by inject()
|
||||||
private val EXT_APK by lazy { File("${activity.filesDir.parent}/snet", "snet.jar") }
|
private val EXT_APK by lazy { File("${activity.filesDir.parent}/snet", "snet.jar") }
|
||||||
|
private val EXT_DEX by lazy { File(EXT_APK.parent, "snet.dex") }
|
||||||
|
|
||||||
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
|
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
|
||||||
|
|
||||||
override fun onEventDispatched(event: ViewEvent) {
|
override fun onEventDispatched(event: ViewEvent) {
|
||||||
super.onEventDispatched(event)
|
super.onEventDispatched(event)
|
||||||
when (event) {
|
when (event) {
|
||||||
is OpenLinkEvent -> openLink(event.url)
|
is OpenLinkEvent -> activity.openUrl(event.url)
|
||||||
is ManagerInstallEvent -> installManager()
|
is ManagerInstallEvent -> installManager()
|
||||||
is MagiskInstallEvent -> installMagisk()
|
is MagiskInstallEvent -> installMagisk()
|
||||||
is UninstallEvent -> uninstall()
|
is UninstallEvent -> uninstall()
|
||||||
@@ -62,7 +63,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
MagiskInstallDialog(requireActivity() as MagiskActivity<*, *>).show()
|
MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
|
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
|
||||||
@@ -86,15 +87,15 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
|||||||
.setTitle(R.string.proprietary_title)
|
.setTitle(R.string.proprietary_title)
|
||||||
.setMessage(R.string.proprietary_notice)
|
.setMessage(R.string.proprietary_notice)
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.setPositiveButton(R.string.yes) { _, _ -> download() }
|
.setPositiveButton(android.R.string.yes) { _, _ -> download() }
|
||||||
.setNegativeButton(R.string.no_thanks) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
|
.setNegativeButton(android.R.string.no) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSafetyNet(dieOnError: Boolean) {
|
private fun updateSafetyNet(dieOnError: Boolean) {
|
||||||
Completable.fromAction {
|
Completable.fromAction {
|
||||||
val loader = DynamicClassLoader(EXT_APK)
|
val loader = DynamicClassLoader(EXT_APK)
|
||||||
val dex = DexFile.loadDex(EXT_APK.path, EXT_APK.parent, 0)
|
val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0)
|
||||||
|
|
||||||
// Scan through the dex and find our helper class
|
// Scan through the dex and find our helper class
|
||||||
var helperClass: Class<*>? = null
|
var helperClass: Class<*>? = null
|
||||||
|
@@ -1,28 +1,23 @@
|
|||||||
package com.topjohnwu.magisk.ui.home
|
package com.topjohnwu.magisk.ui.home
|
||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
|
||||||
import com.skoumal.teanity.extensions.doOnSubscribeUi
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.topjohnwu.magisk.*
|
import com.topjohnwu.magisk.*
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||||
import com.topjohnwu.magisk.extensions.get
|
import com.topjohnwu.magisk.extensions.*
|
||||||
import com.topjohnwu.magisk.extensions.packageName
|
|
||||||
import com.topjohnwu.magisk.extensions.res
|
|
||||||
import com.topjohnwu.magisk.extensions.toggle
|
|
||||||
import com.topjohnwu.magisk.model.events.*
|
import com.topjohnwu.magisk.model.events.*
|
||||||
import com.topjohnwu.magisk.model.observer.Observer
|
import com.topjohnwu.magisk.model.observer.Observer
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
import com.topjohnwu.magisk.utils.SafetyNetHelper
|
import com.topjohnwu.magisk.utils.SafetyNetHelper
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
|
import io.reactivex.Completable
|
||||||
|
|
||||||
enum class SafetyNetState {
|
enum class SafetyNetState {
|
||||||
LOADING, PASS, FAILED, IDLE
|
LOADING, PASS, FAILED, IDLE
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MagiskState {
|
enum class MagiskState {
|
||||||
NO_ROOT, NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
|
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MagiskItem {
|
enum class MagiskItem {
|
||||||
@@ -31,7 +26,7 @@ enum class MagiskItem {
|
|||||||
|
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
private val magiskRepo: MagiskRepository
|
private val magiskRepo: MagiskRepository
|
||||||
) : MagiskViewModel(State.LOADED) {
|
) : BaseViewModel(State.LOADED) {
|
||||||
|
|
||||||
val hasGMS = runCatching {
|
val hasGMS = runCatching {
|
||||||
get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true
|
get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true
|
||||||
@@ -43,13 +38,9 @@ class HomeViewModel(
|
|||||||
val isKeepVerity = KObservableField(Info.keepVerity)
|
val isKeepVerity = KObservableField(Info.keepVerity)
|
||||||
val isRecovery = KObservableField(Info.recovery)
|
val isRecovery = KObservableField(Info.recovery)
|
||||||
|
|
||||||
private val _magiskState = KObservableField(MagiskState.LOADING)
|
val magiskState = KObservableField(MagiskState.LOADING)
|
||||||
val magiskState = Observer(_magiskState, isConnected) {
|
|
||||||
if (isConnected.value) _magiskState.value else MagiskState.UP_TO_DATE
|
|
||||||
}
|
|
||||||
val magiskStateText = Observer(magiskState) {
|
val magiskStateText = Observer(magiskState) {
|
||||||
when (magiskState.value) {
|
when (magiskState.value) {
|
||||||
MagiskState.NO_ROOT -> TODO()
|
|
||||||
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res()
|
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res()
|
||||||
MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res()
|
MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res()
|
||||||
MagiskState.LOADING -> R.string.checking_for_updates.res()
|
MagiskState.LOADING -> R.string.checking_for_updates.res()
|
||||||
@@ -71,7 +62,6 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
val managerStateText = Observer(managerState) {
|
val managerStateText = Observer(managerState) {
|
||||||
when (managerState.value) {
|
when (managerState.value) {
|
||||||
MagiskState.NO_ROOT -> "wtf"
|
|
||||||
MagiskState.NOT_INSTALLED -> R.string.invalid_update_channel.res()
|
MagiskState.NOT_INSTALLED -> R.string.invalid_update_channel.res()
|
||||||
MagiskState.UP_TO_DATE -> R.string.manager_up_to_date.res()
|
MagiskState.UP_TO_DATE -> R.string.manager_up_to_date.res()
|
||||||
MagiskState.LOADING -> R.string.checking_for_updates.res()
|
MagiskState.LOADING -> R.string.checking_for_updates.res()
|
||||||
@@ -87,7 +77,7 @@ class HomeViewModel(
|
|||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text)
|
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text.res())
|
||||||
val ctsState = KObservableField(SafetyNetState.IDLE)
|
val ctsState = KObservableField(SafetyNetState.IDLE)
|
||||||
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
|
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
|
||||||
val safetyNetState = Observer(ctsState, basicIntegrityState) {
|
val safetyNetState = Observer(ctsState, basicIntegrityState) {
|
||||||
@@ -117,10 +107,10 @@ class HomeViewModel(
|
|||||||
Info.recovery = it ?: return@addOnPropertyChangedCallback
|
Info.recovery = it ?: return@addOnPropertyChangedCallback
|
||||||
}
|
}
|
||||||
isConnected.addOnPropertyChangedCallback {
|
isConnected.addOnPropertyChangedCallback {
|
||||||
if (it == true) refresh()
|
if (it == true) refresh(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh()
|
refresh(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
|
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
|
||||||
@@ -145,7 +135,7 @@ class HomeViewModel(
|
|||||||
fun safetyNetPressed() {
|
fun safetyNetPressed() {
|
||||||
ctsState.value = SafetyNetState.LOADING
|
ctsState.value = SafetyNetState.LOADING
|
||||||
basicIntegrityState.value = SafetyNetState.LOADING
|
basicIntegrityState.value = SafetyNetState.LOADING
|
||||||
safetyNetTitle.value = R.string.checking_safetyNet_status
|
safetyNetTitle.value = R.string.checking_safetyNet_status.res()
|
||||||
|
|
||||||
UpdateSafetyNetEvent().publish()
|
UpdateSafetyNetEvent().publish()
|
||||||
}
|
}
|
||||||
@@ -154,7 +144,7 @@ class HomeViewModel(
|
|||||||
response and 0x0F == 0 -> {
|
response and 0x0F == 0 -> {
|
||||||
val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0
|
val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0
|
||||||
val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0
|
val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0
|
||||||
safetyNetTitle.value = R.string.safetyNet_check_success
|
safetyNetTitle.value = R.string.safetyNet_check_success.res()
|
||||||
ctsState.value = if (hasCtsPassed) {
|
ctsState.value = if (hasCtsPassed) {
|
||||||
SafetyNetState.PASS
|
SafetyNetState.PASS
|
||||||
} else {
|
} else {
|
||||||
@@ -174,67 +164,83 @@ class HomeViewModel(
|
|||||||
ctsState.value = SafetyNetState.IDLE
|
ctsState.value = SafetyNetState.IDLE
|
||||||
basicIntegrityState.value = SafetyNetState.IDLE
|
basicIntegrityState.value = SafetyNetState.IDLE
|
||||||
safetyNetTitle.value = when (response) {
|
safetyNetTitle.value = when (response) {
|
||||||
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid
|
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid.res()
|
||||||
else -> R.string.safetyNet_api_error
|
else -> R.string.safetyNet_api_error.res()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
@JvmOverloads
|
||||||
refreshVersions()
|
fun refresh(invalidate: Boolean = true) {
|
||||||
|
if (invalidate)
|
||||||
magiskRepo.fetchUpdate()
|
Info.envRef.invalidate()
|
||||||
.applyViewModel(this)
|
|
||||||
.doOnSubscribeUi {
|
|
||||||
_magiskState.value = MagiskState.LOADING
|
|
||||||
_managerState.value = MagiskState.LOADING
|
|
||||||
ctsState.value = SafetyNetState.IDLE
|
|
||||||
basicIntegrityState.value = SafetyNetState.IDLE
|
|
||||||
safetyNetTitle.value = R.string.safetyNet_check_text
|
|
||||||
}
|
|
||||||
.subscribeK {
|
|
||||||
updateSelf()
|
|
||||||
ensureEnv()
|
|
||||||
refreshVersions()
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRoot.value = Shell.rootAccess()
|
hasRoot.value = Shell.rootAccess()
|
||||||
|
|
||||||
|
val fetchUpdate = if (isConnected.value)
|
||||||
|
magiskRepo.fetchUpdate().ignoreElement()
|
||||||
|
else
|
||||||
|
Completable.complete()
|
||||||
|
|
||||||
|
Completable.fromAction {
|
||||||
|
// Ensure value is ready
|
||||||
|
Info.env
|
||||||
|
}.andThen(fetchUpdate)
|
||||||
|
.applyViewModel(this)
|
||||||
|
.doOnSubscribeUi {
|
||||||
|
magiskState.value = MagiskState.LOADING
|
||||||
|
_managerState.value = MagiskState.LOADING
|
||||||
|
ctsState.value = SafetyNetState.IDLE
|
||||||
|
basicIntegrityState.value = SafetyNetState.IDLE
|
||||||
|
safetyNetTitle.value = R.string.safetyNet_check_text.res()
|
||||||
|
}.subscribeK {
|
||||||
|
updateSelf()
|
||||||
|
ensureEnv()
|
||||||
|
refreshVersions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshVersions() {
|
private fun refreshVersions() {
|
||||||
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
|
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
|
||||||
version.format(Info.magiskVersionString, Info.magiskVersionCode)
|
VERSION_FMT.format(Info.env.magiskVersionString, Info.env.magiskVersionCode)
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
managerCurrentVersion.value = version
|
managerCurrentVersion.value = if (isRunningAsStub) MGR_VER_FMT
|
||||||
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
|
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, Info.stub!!.version)
|
||||||
|
else
|
||||||
|
VERSION_FMT.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelf() {
|
private fun updateSelf() {
|
||||||
_magiskState.value = when (Info.magiskVersionCode) {
|
magiskState.value = when (Info.env.magiskVersionCode) {
|
||||||
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
|
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED
|
||||||
!in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
|
in 1 until Info.remote.magisk.versionCode -> MagiskState.OBSOLETE
|
||||||
else -> MagiskState.UP_TO_DATE
|
else -> MagiskState.UP_TO_DATE
|
||||||
}
|
}
|
||||||
|
|
||||||
magiskLatestVersion.value = version
|
magiskLatestVersion.value =
|
||||||
.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
|
VERSION_FMT.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
|
||||||
|
|
||||||
_managerState.value = when (Info.remote.app.versionCode) {
|
_managerState.value = when (Info.remote.app.versionCode) {
|
||||||
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
|
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED //wrong update channel
|
||||||
in (BuildConfig.VERSION_CODE + 1)..Int.MAX_VALUE -> MagiskState.OBSOLETE
|
in (BuildConfig.VERSION_CODE + 1) .. Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||||
else -> MagiskState.UP_TO_DATE
|
else -> {
|
||||||
|
if (Info.stub?.version ?: Int.MAX_VALUE < Info.remote.stub.versionCode)
|
||||||
|
MagiskState.OBSOLETE
|
||||||
|
else
|
||||||
|
MagiskState.UP_TO_DATE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
managerLatestVersion.value = version
|
managerLatestVersion.value = MGR_VER_FMT
|
||||||
.format(Info.remote.app.version, Info.remote.app.versionCode)
|
.format(Info.remote.app.version, Info.remote.app.versionCode, Info.remote.stub.versionCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ensureEnv() {
|
private fun ensureEnv() {
|
||||||
val invalidStates =
|
val invalidStates =
|
||||||
listOf(MagiskState.NOT_INSTALLED, MagiskState.NO_ROOT, MagiskState.LOADING)
|
listOf(MagiskState.NOT_INSTALLED, MagiskState.LOADING)
|
||||||
|
|
||||||
// Don't bother checking env when magisk is not installed, loading or already has been shown
|
// Don't bother checking env when magisk is not installed, loading or already has been shown
|
||||||
if (invalidStates.any { it == magiskState.value } || shownDialog) return
|
if (invalidStates.any { it == magiskState.value } || shownDialog) return
|
||||||
@@ -246,7 +252,8 @@ class HomeViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val version = "%s (%d)"
|
private const val VERSION_FMT = "%s (%d)"
|
||||||
|
private const val MGR_VER_FMT = "%s (%d) (%d)"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -6,14 +6,14 @@ import android.view.Menu
|
|||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentLogBinding
|
import com.topjohnwu.magisk.databinding.FragmentLogBinding
|
||||||
import com.topjohnwu.magisk.model.events.PageChangedEvent
|
import com.topjohnwu.magisk.model.events.PageChangedEvent
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class LogFragment : MagiskFragment<LogViewModel, FragmentLogBinding>() {
|
class LogFragment : BaseFragment<LogViewModel, FragmentLogBinding>() {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.fragment_log
|
override val layoutRes: Int = R.layout.fragment_log
|
||||||
override val viewModel: LogViewModel by viewModel()
|
override val viewModel: LogViewModel by viewModel()
|
||||||
|
@@ -1,25 +1,25 @@
|
|||||||
package com.topjohnwu.magisk.ui.log
|
package com.topjohnwu.magisk.ui.log
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
|
||||||
import com.skoumal.teanity.extensions.doOnSubscribeUi
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.util.DiffObservableList
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.skoumal.teanity.viewevents.SnackbarEvent
|
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||||
|
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.model.binding.BindingAdapter
|
import com.topjohnwu.magisk.model.binding.BindingAdapter
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
|
||||||
import com.topjohnwu.magisk.model.events.PageChangedEvent
|
import com.topjohnwu.magisk.model.events.PageChangedEvent
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
|
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
|
||||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||||
@@ -30,7 +30,7 @@ import java.util.*
|
|||||||
class LogViewModel(
|
class LogViewModel(
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
private val logRepo: LogRepository
|
private val logRepo: LogRepository
|
||||||
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
|
) : BaseViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
|
||||||
|
|
||||||
val itemsAdapter = BindingAdapter()
|
val itemsAdapter = BindingAdapter()
|
||||||
val items = DiffObservableList(ComparableRvItem.callback)
|
val items = DiffObservableList(ComparableRvItem.callback)
|
||||||
|
@@ -1,17 +1,12 @@
|
|||||||
package com.topjohnwu.magisk.ui.module
|
package com.topjohnwu.magisk.ui.module
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
|
||||||
import com.skoumal.teanity.extensions.doOnSuccessUi
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.util.DiffObservableList
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
import com.topjohnwu.magisk.data.database.RepoDao
|
import com.topjohnwu.magisk.data.database.RepoDao
|
||||||
import com.topjohnwu.magisk.extensions.toSingle
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
import com.topjohnwu.magisk.extensions.update
|
import com.topjohnwu.magisk.extensions.*
|
||||||
import com.topjohnwu.magisk.model.entity.module.Module
|
import com.topjohnwu.magisk.model.entity.module.Module
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
|
||||||
@@ -20,7 +15,8 @@ import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
|||||||
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
||||||
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
||||||
import com.topjohnwu.magisk.tasks.RepoUpdater
|
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||||
@@ -29,7 +25,7 @@ class ModuleViewModel(
|
|||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
private val repoUpdater: RepoUpdater,
|
private val repoUpdater: RepoUpdater,
|
||||||
private val repoDB: RepoDao
|
private val repoDB: RepoDao
|
||||||
) : MagiskViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val query = KObservableField("")
|
val query = KObservableField("")
|
||||||
|
|
||||||
|
@@ -8,19 +8,19 @@ import android.view.MenuInflater
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.ClassMap
|
|
||||||
import com.topjohnwu.magisk.Const
|
import com.topjohnwu.magisk.Const
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentModulesBinding
|
import com.topjohnwu.magisk.databinding.FragmentModulesBinding
|
||||||
import com.topjohnwu.magisk.extensions.reboot
|
import com.topjohnwu.magisk.extensions.reboot
|
||||||
|
import com.topjohnwu.magisk.intent
|
||||||
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>() {
|
class ModulesFragment : BaseFragment<ModuleViewModel, FragmentModulesBinding>() {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.fragment_modules
|
override val layoutRes: Int = R.layout.fragment_modules
|
||||||
override val viewModel: ModuleViewModel by sharedViewModel()
|
override val viewModel: ModuleViewModel by sharedViewModel()
|
||||||
@@ -28,7 +28,7 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
|
|||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
|
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
|
||||||
// Get the URI of the selected file
|
// Get the URI of the selected file
|
||||||
val intent = Intent(activity, ClassMap[FlashActivity::class.java])
|
val intent = activity.intent(FlashActivity::class.java)
|
||||||
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
|
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,9 @@ import android.view.Menu
|
|||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.SearchView
|
import android.widget.SearchView
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentReposBinding
|
import com.topjohnwu.magisk.databinding.FragmentReposBinding
|
||||||
import com.topjohnwu.magisk.model.download.DownloadService
|
import com.topjohnwu.magisk.model.download.DownloadService
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||||
@@ -16,12 +16,12 @@ import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
|||||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||||
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
import com.topjohnwu.magisk.model.events.InstallModuleEvent
|
||||||
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
|
|
||||||
class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
class ReposFragment : BaseFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||||
SearchView.OnQueryTextListener {
|
SearchView.OnQueryTextListener {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.fragment_repos
|
override val layoutRes: Int = R.layout.fragment_repos
|
||||||
|
@@ -13,21 +13,16 @@ import androidx.preference.ListPreference
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceCategory
|
import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
import com.topjohnwu.magisk.*
|
||||||
import com.skoumal.teanity.util.KObservableField
|
import com.topjohnwu.magisk.base.BasePreferenceFragment
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
|
||||||
import com.topjohnwu.magisk.Config
|
|
||||||
import com.topjohnwu.magisk.Const
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.data.database.RepoDao
|
import com.topjohnwu.magisk.data.database.RepoDao
|
||||||
import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding
|
import com.topjohnwu.magisk.databinding.CustomDownloadDialogBinding
|
||||||
import com.topjohnwu.magisk.extensions.toLangTag
|
import com.topjohnwu.magisk.databinding.DialogCustomNameBinding
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.model.download.DownloadService
|
import com.topjohnwu.magisk.model.download.DownloadService
|
||||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||||
import com.topjohnwu.magisk.model.observer.Observer
|
import com.topjohnwu.magisk.model.observer.Observer
|
||||||
import com.topjohnwu.magisk.net.Networking
|
|
||||||
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment
|
|
||||||
import com.topjohnwu.magisk.utils.*
|
import com.topjohnwu.magisk.utils.*
|
||||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@@ -57,6 +52,7 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
preferenceManager.setStorageDeviceProtected()
|
preferenceManager.setStorageDeviceProtected()
|
||||||
setPreferencesFromResource(R.xml.app_settings, rootKey)
|
setPreferencesFromResource(R.xml.app_settings, rootKey)
|
||||||
|
|
||||||
|
// Get preferences
|
||||||
updateChannel = findPreference(Config.Key.UPDATE_CHANNEL)!!
|
updateChannel = findPreference(Config.Key.UPDATE_CHANNEL)!!
|
||||||
rootConfig = findPreference(Config.Key.ROOT_ACCESS)!!
|
rootConfig = findPreference(Config.Key.ROOT_ACCESS)!!
|
||||||
autoRes = findPreference(Config.Key.SU_AUTO_RESPONSE)!!
|
autoRes = findPreference(Config.Key.SU_AUTO_RESPONSE)!!
|
||||||
@@ -70,17 +66,68 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
val magiskCategory = findPreference<PreferenceCategory>("magisk")!!
|
val magiskCategory = findPreference<PreferenceCategory>("magisk")!!
|
||||||
val suCategory = findPreference<PreferenceCategory>("superuser")!!
|
val suCategory = findPreference<PreferenceCategory>("superuser")!!
|
||||||
val hideManager = findPreference<Preference>("hide")!!
|
val hideManager = findPreference<Preference>("hide")!!
|
||||||
hideManager.setOnPreferenceClickListener {
|
val restoreManager = findPreference<Preference>("restore")!!
|
||||||
PatchAPK.hideManager(requireContext())
|
|
||||||
true
|
// Remove/Disable entries
|
||||||
|
|
||||||
|
// Only show canary channels if user is already on canary channel
|
||||||
|
// or the user have already chosen canary channel
|
||||||
|
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) {
|
||||||
|
// Remove the last 2 entries
|
||||||
|
val entries = updateChannel.entries
|
||||||
|
updateChannel.entries = entries.copyOf(entries.size - 2)
|
||||||
}
|
}
|
||||||
val restoreManager = findPreference<Preference>("restore")
|
|
||||||
restoreManager?.setOnPreferenceClickListener {
|
// Remove dangerous settings in secondary user
|
||||||
DownloadService(requireContext()) {
|
if (Const.USER_ID > 0) {
|
||||||
subject = DownloadSubject.Manager(Configuration.APK.Restore)
|
suCategory.removePreference(multiuserConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove re-authentication option on Android O, it will not work
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
suCategory.removePreference(reauth)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable fingerprint option if not possible
|
||||||
|
if (!FingerprintHelper.canUseFingerprint()) {
|
||||||
|
fingerprint.isEnabled = false
|
||||||
|
fingerprint.isChecked = false
|
||||||
|
fingerprint.setSummary(R.string.disable_fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Const.USER_ID == 0 && Info.isConnected.value && Shell.rootAccess()) {
|
||||||
|
if (activity.packageName == BuildConfig.APPLICATION_ID) {
|
||||||
|
generalCatagory.removePreference(restoreManager)
|
||||||
|
hideManager.setOnPreferenceClickListener {
|
||||||
|
showManagerNameDialog {
|
||||||
|
PatchAPK.hideManager(requireContext(), it)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generalCatagory.removePreference(hideManager)
|
||||||
|
restoreManager.setOnPreferenceClickListener {
|
||||||
|
DownloadService(requireContext()) {
|
||||||
|
subject = DownloadSubject.Manager(Configuration.APK.Restore)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
true
|
} else {
|
||||||
|
// Remove if not primary user, no connection, or no root
|
||||||
|
generalCatagory.removePreference(restoreManager)
|
||||||
|
generalCatagory.removePreference(hideManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Utils.showSuperUser()) {
|
||||||
|
preferenceScreen.removePreference(suCategory)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Shell.rootAccess()) {
|
||||||
|
preferenceScreen.removePreference(magiskCategory)
|
||||||
|
generalCatagory.removePreference(hideManager)
|
||||||
|
}
|
||||||
|
|
||||||
findPreference<Preference>("clear")?.setOnPreferenceClickListener {
|
findPreference<Preference>("clear")?.setOnPreferenceClickListener {
|
||||||
Completable.fromAction { repoDB.clear() }.subscribeK {
|
Completable.fromAction { repoDB.clear() }.subscribeK {
|
||||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
||||||
@@ -124,58 +171,7 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
|
|
||||||
setLocalePreference(findPreference(Config.Key.LOCALE)!!)
|
setLocalePreference(findPreference(Config.Key.LOCALE)!!)
|
||||||
|
|
||||||
/* We only show canary channels if user is already on canary channel
|
|
||||||
* or the user have already chosen canary channel */
|
|
||||||
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) {
|
|
||||||
// Remove the last 2 entries
|
|
||||||
val entries = updateChannel.entries
|
|
||||||
updateChannel.entries = entries.copyOf(entries.size - 2)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
setSummary()
|
setSummary()
|
||||||
|
|
||||||
// Disable dangerous settings in secondary user
|
|
||||||
if (Const.USER_ID > 0) {
|
|
||||||
suCategory.removePreference(multiuserConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable re-authentication option on Android O, it will not work
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
reauth.isEnabled = false
|
|
||||||
reauth.isChecked = false
|
|
||||||
reauth.setSummary(R.string.android_o_not_support)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable fingerprint option if not possible
|
|
||||||
if (!FingerprintHelper.canUseFingerprint()) {
|
|
||||||
fingerprint.isEnabled = false
|
|
||||||
fingerprint.isChecked = false
|
|
||||||
fingerprint.setSummary(R.string.disable_fingerprint)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Shell.rootAccess() && Const.USER_ID == 0) {
|
|
||||||
if (activity.packageName == BuildConfig.APPLICATION_ID) {
|
|
||||||
generalCatagory.removePreference(restoreManager)
|
|
||||||
} else {
|
|
||||||
if (!Networking.checkNetworkStatus(requireContext())) {
|
|
||||||
generalCatagory.removePreference(restoreManager)
|
|
||||||
}
|
|
||||||
generalCatagory.removePreference(hideManager)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
generalCatagory.removePreference(restoreManager)
|
|
||||||
generalCatagory.removePreference(hideManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Utils.showSuperUser()) {
|
|
||||||
preferenceScreen.removePreference(suCategory)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Shell.rootAccess()) {
|
|
||||||
preferenceScreen.removePreference(magiskCategory)
|
|
||||||
generalCatagory.removePreference(hideManager)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
|
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
|
||||||
@@ -202,7 +198,7 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
Shell.su("magiskhide --disable").submit()
|
Shell.su("magiskhide --disable").submit()
|
||||||
}
|
}
|
||||||
Config.Key.LOCALE -> {
|
Config.Key.LOCALE -> {
|
||||||
LocaleManager.setLocale(activity.application)
|
refreshLocale()
|
||||||
activity.recreate()
|
activity.recreate()
|
||||||
}
|
}
|
||||||
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity)
|
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck(activity)
|
||||||
@@ -226,27 +222,12 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
|
|
||||||
private fun setLocalePreference(lp: ListPreference) {
|
private fun setLocalePreference(lp: ListPreference) {
|
||||||
lp.isEnabled = false
|
lp.isEnabled = false
|
||||||
availableLocales.map {
|
availableLocales.subscribeK { (names, values) ->
|
||||||
val names = mutableListOf<String>()
|
lp.isEnabled = true
|
||||||
val values = mutableListOf<String>()
|
lp.entries = names
|
||||||
|
lp.entryValues = values
|
||||||
names.add(
|
lp.summary = currentLocale.getDisplayName(currentLocale)
|
||||||
LocaleManager.getString(defaultLocale, R.string.system_default)
|
}
|
||||||
)
|
|
||||||
values.add("")
|
|
||||||
|
|
||||||
it.forEach { locale ->
|
|
||||||
names.add(locale.getDisplayName(locale))
|
|
||||||
values.add(locale.toLangTag())
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair(names.toTypedArray(), values.toTypedArray())
|
|
||||||
}.subscribeK { (names, values) ->
|
|
||||||
lp.isEnabled = true
|
|
||||||
lp.entries = names
|
|
||||||
lp.entryValues = values
|
|
||||||
lp.summary = currentLocale.getDisplayName(currentLocale)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setSummary(key: String) {
|
private fun setSummary(key: String) {
|
||||||
@@ -298,8 +279,8 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
AlertDialog.Builder(requireActivity())
|
AlertDialog.Builder(requireActivity())
|
||||||
.setTitle(R.string.settings_update_custom)
|
.setTitle(R.string.settings_update_custom)
|
||||||
.setView(v)
|
.setView(v)
|
||||||
.setPositiveButton(R.string.ok) { _, _ -> onSuccess(url.text.toString()) }
|
.setPositiveButton(android.R.string.ok) { _, _ -> onSuccess(url.text.toString()) }
|
||||||
.setNegativeButton(R.string.close) { _, _ -> onCancel() }
|
.setNegativeButton(android.R.string.cancel) { _, _ -> onCancel() }
|
||||||
.setOnCancelListener { onCancel() }
|
.setOnCancelListener { onCancel() }
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@@ -323,11 +304,35 @@ class SettingsFragment : BasePreferenceFragment() {
|
|||||||
AlertDialog.Builder(requireActivity())
|
AlertDialog.Builder(requireActivity())
|
||||||
.setTitle(R.string.settings_download_path_title)
|
.setTitle(R.string.settings_download_path_title)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
Utils.ensureDownloadPath(data.text.value)?.let { onSuccess(data.text.value) }
|
Utils.ensureDownloadPath(data.text.value)?.let { onSuccess(data.text.value) }
|
||||||
?: Utils.toast(R.string.settings_download_path_error, Toast.LENGTH_SHORT)
|
?: Utils.toast(R.string.settings_download_path_error, Toast.LENGTH_SHORT)
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.close, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private inline fun showManagerNameDialog(
|
||||||
|
crossinline onSuccess: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val data = ManagerNameData()
|
||||||
|
val view = DialogCustomNameBinding
|
||||||
|
.inflate(LayoutInflater.from(requireContext()))
|
||||||
|
.also { it.data = data }
|
||||||
|
|
||||||
|
AlertDialog.Builder(requireActivity())
|
||||||
|
.setTitle(R.string.settings_app_name)
|
||||||
|
.setView(view.root)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
if (view.dialogNameInput.error.isNullOrBlank()) {
|
||||||
|
onSuccess(data.name.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ManagerNameData {
|
||||||
|
val name = KObservableField(resources.getString(R.string.re_app_name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
package com.topjohnwu.magisk.ui.superuser
|
package com.topjohnwu.magisk.ui.superuser
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseFragment
|
||||||
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding
|
import com.topjohnwu.magisk.databinding.FragmentSuperuserBinding
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class SuperuserFragment :
|
class SuperuserFragment :
|
||||||
MagiskFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
|
BaseFragment<SuperuserViewModel, FragmentSuperuserBinding>() {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.fragment_superuser
|
override val layoutRes: Int = R.layout.fragment_superuser
|
||||||
override val viewModel: SuperuserViewModel by viewModel()
|
override val viewModel: SuperuserViewModel by viewModel()
|
||||||
|
@@ -2,22 +2,22 @@ package com.topjohnwu.magisk.ui.superuser
|
|||||||
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.skoumal.teanity.extensions.applySchedulers
|
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
|
||||||
import com.skoumal.teanity.rxbus.RxBus
|
|
||||||
import com.skoumal.teanity.util.DiffObservableList
|
|
||||||
import com.skoumal.teanity.viewevents.SnackbarEvent
|
|
||||||
import com.topjohnwu.magisk.BR
|
import com.topjohnwu.magisk.BR
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.extensions.applySchedulers
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.extensions.toggle
|
import com.topjohnwu.magisk.extensions.toggle
|
||||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||||
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.model.events.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||||
|
import com.topjohnwu.magisk.utils.RxBus
|
||||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
||||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
@@ -29,7 +29,7 @@ class SuperuserViewModel(
|
|||||||
private val packageManager: PackageManager,
|
private val packageManager: PackageManager,
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
rxBus: RxBus
|
rxBus: RxBus
|
||||||
) : MagiskViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val items = DiffObservableList(ComparableRvItem.callback)
|
val items = DiffObservableList(ComparableRvItem.callback)
|
||||||
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
|
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||||
@@ -42,6 +42,11 @@ class SuperuserViewModel(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
rxBus.register<PolicyEnableEvent>()
|
rxBus.register<PolicyEnableEvent>()
|
||||||
|
.filter {
|
||||||
|
val isIgnored = it.item == ignoreNext
|
||||||
|
if (isIgnored) ignoreNext = null
|
||||||
|
!isIgnored
|
||||||
|
}
|
||||||
.subscribeK { togglePolicy(it.item, it.enable) }
|
.subscribeK { togglePolicy(it.item, it.enable) }
|
||||||
.add()
|
.add()
|
||||||
rxBus.register<PolicyUpdateEvent>()
|
rxBus.register<PolicyUpdateEvent>()
|
||||||
@@ -84,8 +89,8 @@ class SuperuserViewModel(
|
|||||||
CustomAlertDialog(this)
|
CustomAlertDialog(this)
|
||||||
.setTitle(R.string.su_revoke_title)
|
.setTitle(R.string.su_revoke_title)
|
||||||
.setMessage(getString(R.string.su_revoke_msg, item.item.appName))
|
.setMessage(getString(R.string.su_revoke_msg, item.item.appName))
|
||||||
.setPositiveButton(R.string.yes) { _, _ -> updateState() }
|
.setPositiveButton(android.R.string.yes) { _, _ -> updateState() }
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
.setNegativeButton(android.R.string.no, null)
|
||||||
.setCancelable(true)
|
.setCancelable(true)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
@@ -144,4 +149,4 @@ class SuperuserViewModel(
|
|||||||
private fun deletePolicy(policy: MagiskPolicy) =
|
private fun deletePolicy(policy: MagiskPolicy) =
|
||||||
policyDB.delete(policy.uid).andThen(Single.just(policy))
|
policyDB.delete(policy.uid).andThen(Single.just(policy))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,21 +3,21 @@ package com.topjohnwu.magisk.ui.surequest
|
|||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import com.skoumal.teanity.viewevents.ViewEvent
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.BaseActivity
|
||||||
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
|
import com.topjohnwu.magisk.databinding.ActivityRequestBinding
|
||||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||||
import com.topjohnwu.magisk.model.events.DieEvent
|
import com.topjohnwu.magisk.model.events.DieEvent
|
||||||
|
import com.topjohnwu.magisk.model.events.ViewEvent
|
||||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
|
||||||
import com.topjohnwu.magisk.utils.SuLogger
|
import com.topjohnwu.magisk.utils.SuLogger
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
open class SuRequestActivity : MagiskActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
open class SuRequestActivity : BaseActivity<SuRequestViewModel, ActivityRequestBinding>() {
|
||||||
|
|
||||||
override val layoutRes: Int = R.layout.activity_request
|
override val layoutRes: Int = R.layout.activity_request
|
||||||
|
override val themeRes: Int = R.style.MagiskTheme_SU
|
||||||
override val viewModel: SuRequestViewModel by viewModel()
|
override val viewModel: SuRequestViewModel by viewModel()
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
@@ -30,19 +30,17 @@ open class SuRequestActivity : MagiskActivity<SuRequestViewModel, ActivityReques
|
|||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val intent = intent
|
val intent = intent
|
||||||
val action = intent.action
|
|
||||||
|
|
||||||
if (TextUtils.equals(action, GeneralReceiver.REQUEST)) {
|
when (intent?.action) {
|
||||||
if (!viewModel.handleRequest(intent))
|
GeneralReceiver.REQUEST -> {
|
||||||
finish()
|
if (!viewModel.handleRequest(intent))
|
||||||
return
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GeneralReceiver.LOG -> SuLogger.handleLogs(this, intent)
|
||||||
|
GeneralReceiver.NOTIFY -> SuLogger.handleNotify(this, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.equals(action, GeneralReceiver.LOG))
|
|
||||||
SuLogger.handleLogs(intent)
|
|
||||||
else if (TextUtils.equals(action, GeneralReceiver.NOTIFY))
|
|
||||||
SuLogger.handleNotify(intent)
|
|
||||||
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -9,21 +9,21 @@ import android.graphics.drawable.Drawable
|
|||||||
import android.hardware.fingerprint.FingerprintManager
|
import android.hardware.fingerprint.FingerprintManager
|
||||||
import android.os.CountDownTimer
|
import android.os.CountDownTimer
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
|
||||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
|
||||||
import com.skoumal.teanity.util.DiffObservableList
|
|
||||||
import com.skoumal.teanity.util.KObservableField
|
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.Config
|
import com.topjohnwu.magisk.Config
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
|
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
|
||||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||||
|
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||||
|
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
|
||||||
import com.topjohnwu.magisk.extensions.now
|
import com.topjohnwu.magisk.extensions.now
|
||||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||||
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
|
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
|
||||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||||
import com.topjohnwu.magisk.model.events.DieEvent
|
import com.topjohnwu.magisk.model.events.DieEvent
|
||||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
import com.topjohnwu.magisk.utils.DiffObservableList
|
||||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||||
|
import com.topjohnwu.magisk.utils.KObservableField
|
||||||
import com.topjohnwu.magisk.utils.SuConnector
|
import com.topjohnwu.magisk.utils.SuConnector
|
||||||
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
|
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
|
||||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||||
@@ -36,7 +36,7 @@ class SuRequestViewModel(
|
|||||||
private val policyDB: PolicyDao,
|
private val policyDB: PolicyDao,
|
||||||
private val timeoutPrefs: SharedPreferences,
|
private val timeoutPrefs: SharedPreferences,
|
||||||
private val resources: Resources
|
private val resources: Resources
|
||||||
) : MagiskViewModel() {
|
) : BaseViewModel() {
|
||||||
|
|
||||||
val icon = KObservableField<Drawable?>(null)
|
val icon = KObservableField<Drawable?>(null)
|
||||||
val title = KObservableField("")
|
val title = KObservableField("")
|
||||||
|
22
app/src/main/java/com/topjohnwu/magisk/utils/CachedValue.kt
Normal file
22
app/src/main/java/com/topjohnwu/magisk/utils/CachedValue.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
class CachedValue<T>(private val factory: () -> T) : Lazy<T> {
|
||||||
|
|
||||||
|
private var _val : T? = null
|
||||||
|
|
||||||
|
override val value: T
|
||||||
|
get() {
|
||||||
|
val local = _val
|
||||||
|
return local ?: synchronized(this) {
|
||||||
|
_val ?: factory().also { _val = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isInitialized() = _val != null
|
||||||
|
|
||||||
|
fun invalidate() {
|
||||||
|
synchronized(this) {
|
||||||
|
_val = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -16,9 +16,10 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.viewpager.widget.ViewPager
|
import androidx.viewpager.widget.ViewPager
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import com.skoumal.teanity.extensions.subscribeK
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
|
import com.topjohnwu.magisk.extensions.replaceRandomWithSpecial
|
||||||
|
import com.topjohnwu.magisk.extensions.subscribeK
|
||||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
@@ -220,4 +221,11 @@ fun getScrollPosition(view: RecyclerView) = (view.layoutManager as? LinearLayout
|
|||||||
@BindingAdapter("isEnabled")
|
@BindingAdapter("isEnabled")
|
||||||
fun setEnabled(view: View, isEnabled: Boolean) {
|
fun setEnabled(view: View, isEnabled: Boolean) {
|
||||||
view.isEnabled = isEnabled
|
view.isEnabled = isEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
@BindingAdapter("error")
|
||||||
|
fun TextInputLayout.setErrorString(error: String) {
|
||||||
|
val newError = error.let { if (it.isEmpty()) null else it }
|
||||||
|
if (this.error == null && newError == null) return
|
||||||
|
this.error = newError
|
||||||
}
|
}
|
@@ -0,0 +1,234 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import androidx.annotation.MainThread
|
||||||
|
import androidx.databinding.ListChangeRegistry
|
||||||
|
import androidx.databinding.ObservableList
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param callback The callback that controls the behavior of the DiffObservableList.
|
||||||
|
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
|
||||||
|
*/
|
||||||
|
open class DiffObservableList<T>(
|
||||||
|
private val callback: Callback<T>,
|
||||||
|
private val detectMoves: Boolean = true
|
||||||
|
) : AbstractList<T>(), ObservableList<T> {
|
||||||
|
|
||||||
|
private val LIST_LOCK = Object()
|
||||||
|
private var list: MutableList<T> = ArrayList()
|
||||||
|
private val listeners = ListChangeRegistry()
|
||||||
|
private val listCallback = ObservableListUpdateCallback()
|
||||||
|
|
||||||
|
override val size: Int get() = list.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the list of update operations that can convert this list into the given one.
|
||||||
|
*
|
||||||
|
* @param newItems The items that this list will be set to.
|
||||||
|
* @return A DiffResult that contains the information about the edit sequence to covert this
|
||||||
|
* list into the given one.
|
||||||
|
*/
|
||||||
|
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
|
||||||
|
val frozenList = synchronized(LIST_LOCK) {
|
||||||
|
ArrayList(list)
|
||||||
|
}
|
||||||
|
return doCalculateDiff(frozenList, newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doCalculateDiff(oldItems: List<T>, newItems: List<T>?): DiffUtil.DiffResult {
|
||||||
|
return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize() = oldItems.size
|
||||||
|
|
||||||
|
override fun getNewListSize() = newItems?.size ?: 0
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldItems[oldItemPosition]
|
||||||
|
val newItem = newItems!![newItemPosition]
|
||||||
|
return callback.areItemsTheSame(oldItem, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
|
val oldItem = oldItems[oldItemPosition]
|
||||||
|
val newItem = newItems!![newItemPosition]
|
||||||
|
return callback.areContentsTheSame(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}, detectMoves)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the contents of this list to the given one using the DiffResults to dispatch change
|
||||||
|
* notifications.
|
||||||
|
*
|
||||||
|
* @param newItems The items to set this list to.
|
||||||
|
* @param diffResult The diff results to dispatch change notifications.
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
|
||||||
|
synchronized(LIST_LOCK) {
|
||||||
|
list = newItems.toMutableList()
|
||||||
|
}
|
||||||
|
diffResult.dispatchUpdatesTo(listCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets this list to the given items. This is a convenience method for calling [ ][.calculateDiff] followed by [.update].
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* **Warning!** If the lists are large this operation may be too slow for the main thread. In
|
||||||
|
* that case, you should call [.calculateDiff] on a background thread and then
|
||||||
|
* [.update] on the main thread.
|
||||||
|
*
|
||||||
|
* @param newItems The items to set this list to.
|
||||||
|
*/
|
||||||
|
@MainThread
|
||||||
|
fun update(newItems: List<T>) {
|
||||||
|
val diffResult = doCalculateDiff(list, newItems)
|
||||||
|
list = newItems.toMutableList()
|
||||||
|
diffResult.dispatchUpdatesTo(listCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||||
|
listeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeOnListChangedCallback(listener: ObservableList.OnListChangedCallback<out ObservableList<T>>) {
|
||||||
|
listeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun get(index: Int): T {
|
||||||
|
return list[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(element: T): Boolean {
|
||||||
|
list.add(element)
|
||||||
|
notifyAdd(size - 1, 1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(index: Int, element: T) {
|
||||||
|
list.add(index, element)
|
||||||
|
notifyAdd(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAll(elements: Collection<T>): Boolean {
|
||||||
|
val oldSize = size
|
||||||
|
val added = list.addAll(elements)
|
||||||
|
if (added) {
|
||||||
|
notifyAdd(oldSize, size - oldSize)
|
||||||
|
}
|
||||||
|
return added
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addAll(index: Int, elements: Collection<T>): Boolean {
|
||||||
|
val added = list.addAll(index, elements)
|
||||||
|
if (added) {
|
||||||
|
notifyAdd(index, elements.size)
|
||||||
|
}
|
||||||
|
return added
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear() {
|
||||||
|
val oldSize = size
|
||||||
|
list.clear()
|
||||||
|
if (oldSize != 0) {
|
||||||
|
notifyRemove(0, oldSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun remove(element: T): Boolean {
|
||||||
|
val index = indexOf(element)
|
||||||
|
return if (index >= 0) {
|
||||||
|
removeAt(index)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeAt(index: Int): T {
|
||||||
|
val element = list.removeAt(index)
|
||||||
|
notifyRemove(index, 1)
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLast(): T? {
|
||||||
|
if (size > 0) {
|
||||||
|
val index = size - 1
|
||||||
|
return removeAt(index)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(index: Int, element: T): T {
|
||||||
|
val old = list.set(index, element)
|
||||||
|
listeners.notifyChanged(this, index, 1)
|
||||||
|
return old
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyAdd(start: Int, count: Int) {
|
||||||
|
listeners.notifyInserted(this, start, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyRemove(start: Int, count: Int) {
|
||||||
|
listeners.notifyRemoved(this, start, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Callback class used by DiffUtil while calculating the diff between two lists.
|
||||||
|
*/
|
||||||
|
interface Callback<T> {
|
||||||
|
/**
|
||||||
|
* Called by the DiffUtil to decide whether two object represent the same Item.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* For example, if your items have unique ids, this method should check their id equality.
|
||||||
|
*
|
||||||
|
* @param oldItem The old item.
|
||||||
|
* @param newItem The new item.
|
||||||
|
* @return True if the two items represent the same object or false if they are different.
|
||||||
|
*/
|
||||||
|
fun areItemsTheSame(oldItem: T, newItem: T): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the DiffUtil when it wants to check whether two items have the same data.
|
||||||
|
* DiffUtil uses this information to detect if the contents of an item has changed.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* DiffUtil uses this method to check equality instead of [Object.equals] so
|
||||||
|
* that you can change its behavior depending on your UI.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* This method is called only if [.areItemsTheSame] returns `true` for
|
||||||
|
* these items.
|
||||||
|
*
|
||||||
|
* @param oldItem The old item.
|
||||||
|
* @param newItem The new item which replaces the old item.
|
||||||
|
* @return True if the contents of the items are the same or false if they are different.
|
||||||
|
*/
|
||||||
|
fun areContentsTheSame(oldItem: T, newItem: T): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ObservableListUpdateCallback : ListUpdateCallback {
|
||||||
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
listeners.notifyChanged(this@DiffObservableList, position, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
listeners.notifyMoved(this@DiffObservableList, fromPosition, toPosition, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInserted(position: Int, count: Int) {
|
||||||
|
modCount += 1
|
||||||
|
listeners.notifyInserted(this@DiffObservableList, position, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
|
modCount += 1
|
||||||
|
listeners.notifyRemoved(this@DiffObservableList, position, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -1,61 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils
|
|
||||||
|
|
||||||
import dalvik.system.DexClassLoader
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Suppress("FunctionName")
|
|
||||||
inline fun <reified T> T.DynamicClassLoader(apk: File) = DynamicClassLoader(apk, T::class.java.classLoader)
|
|
||||||
|
|
||||||
class DynamicClassLoader(apk: File, parent: ClassLoader?)
|
|
||||||
: DexClassLoader(apk.path, apk.parent, null, parent) {
|
|
||||||
|
|
||||||
private val base by lazy { Any::class.java.classLoader!! }
|
|
||||||
|
|
||||||
@Throws(ClassNotFoundException::class)
|
|
||||||
override fun loadClass(name: String, resolve: Boolean) : Class<*>
|
|
||||||
= findLoadedClass(name) ?: runCatching {
|
|
||||||
base.loadClass(name)
|
|
||||||
}.getOrElse {
|
|
||||||
runCatching {
|
|
||||||
findClass(name)
|
|
||||||
}.getOrElse { err ->
|
|
||||||
runCatching {
|
|
||||||
parent.loadClass(name)
|
|
||||||
}.getOrElse { throw err }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getResource(name: String) = base.getResource(name)
|
|
||||||
?: findResource(name)
|
|
||||||
?: parent?.getResource(name)
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
override fun getResources(name: String): Enumeration<URL> {
|
|
||||||
val resources = mutableListOf(
|
|
||||||
base.getResources(name),
|
|
||||||
findResources(name), parent.getResources(name))
|
|
||||||
return object : Enumeration<URL> {
|
|
||||||
override fun hasMoreElements(): Boolean {
|
|
||||||
while (true) {
|
|
||||||
if (resources.isEmpty())
|
|
||||||
return false
|
|
||||||
if (!resources[0].hasMoreElements()) {
|
|
||||||
resources.removeAt(0)
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun nextElement(): URL {
|
|
||||||
if (!hasMoreElements())
|
|
||||||
throw NoSuchElementException()
|
|
||||||
return resources[0].nextElement()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
117
app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt
Normal file
117
app/src/main/java/com/topjohnwu/magisk/utils/KItemDecoration.kt
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.view.get
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.topjohnwu.magisk.extensions.drawableCompat
|
||||||
|
|
||||||
|
class KItemDecoration(
|
||||||
|
private val context: Context,
|
||||||
|
@RecyclerView.Orientation private val orientation: Int
|
||||||
|
) :
|
||||||
|
RecyclerView.ItemDecoration() {
|
||||||
|
|
||||||
|
private val bounds = Rect()
|
||||||
|
private var divider: Drawable? = null
|
||||||
|
var showAfterLast = true
|
||||||
|
|
||||||
|
fun setDeco(@DrawableRes drawable: Int) = apply {
|
||||||
|
setDeco(context.drawableCompat(drawable))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDeco(drawable: Drawable?) = apply {
|
||||||
|
divider = drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
parent.layoutManager ?: return
|
||||||
|
|
||||||
|
divider?.let {
|
||||||
|
if (orientation == DividerItemDecoration.VERTICAL) {
|
||||||
|
drawVertical(canvas, parent, it)
|
||||||
|
} else {
|
||||||
|
drawHorizontal(canvas, parent, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawVertical(canvas: Canvas, parent: RecyclerView, drawable: Drawable) {
|
||||||
|
canvas.save()
|
||||||
|
val left: Int
|
||||||
|
val right: Int
|
||||||
|
if (parent.clipToPadding) {
|
||||||
|
left = parent.paddingLeft
|
||||||
|
right = parent.width - parent.paddingRight
|
||||||
|
canvas.clipRect(
|
||||||
|
left, parent.paddingTop, right,
|
||||||
|
parent.height - parent.paddingBottom
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
left = 0
|
||||||
|
right = parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
val to = if (showAfterLast) parent.childCount else parent.childCount - 1
|
||||||
|
|
||||||
|
(0 until to)
|
||||||
|
.map { parent[it] }
|
||||||
|
.forEach { child ->
|
||||||
|
parent.getDecoratedBoundsWithMargins(child, bounds)
|
||||||
|
val bottom = bounds.bottom + Math.round(child.translationY)
|
||||||
|
val top = bottom - drawable.intrinsicHeight
|
||||||
|
drawable.setBounds(left, top, right, bottom)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawHorizontal(canvas: Canvas, parent: RecyclerView, drawable: Drawable) {
|
||||||
|
canvas.save()
|
||||||
|
val top: Int
|
||||||
|
val bottom: Int
|
||||||
|
if (parent.clipToPadding) {
|
||||||
|
top = parent.paddingTop
|
||||||
|
bottom = parent.height - parent.paddingBottom
|
||||||
|
canvas.clipRect(
|
||||||
|
parent.paddingLeft, top,
|
||||||
|
parent.width - parent.paddingRight, bottom
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
top = 0
|
||||||
|
bottom = parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
val to = if (showAfterLast) parent.childCount else parent.childCount - 1
|
||||||
|
|
||||||
|
(0 until to)
|
||||||
|
.map { parent[it] }
|
||||||
|
.forEach { child ->
|
||||||
|
parent.layoutManager!!.getDecoratedBoundsWithMargins(child, bounds)
|
||||||
|
val right = bounds.right + Math.round(child.translationX)
|
||||||
|
val left = right - drawable.intrinsicWidth
|
||||||
|
drawable.setBounds(left, top, right, bottom)
|
||||||
|
drawable.draw(canvas)
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||||
|
if (parent.getChildAdapterPosition(view) == state.itemCount - 1) {
|
||||||
|
outRect.setEmpty()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orientation == RecyclerView.VERTICAL) {
|
||||||
|
outRect.set(0, 0, 0, divider?.intrinsicHeight ?: 0)
|
||||||
|
} else {
|
||||||
|
outRect.set(0, 0, divider?.intrinsicWidth ?: 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import androidx.databinding.Observable
|
||||||
|
import androidx.databinding.ObservableField
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kotlin version of [ObservableField].
|
||||||
|
* You can define if wrapped type is Nullable or not.
|
||||||
|
* You can use kotlin get/set syntax for value
|
||||||
|
*/
|
||||||
|
open class KObservableField<T> : ObservableField<T>, Serializable {
|
||||||
|
|
||||||
|
var value: T
|
||||||
|
get() = get()
|
||||||
|
set(value) { set(value) }
|
||||||
|
|
||||||
|
constructor(init: T) {
|
||||||
|
value = init
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(init: T, vararg dependencies: Observable) : super(*dependencies) {
|
||||||
|
value = init
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun get(): T {
|
||||||
|
return super.get() as T
|
||||||
|
}
|
||||||
|
}
|
143
app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt
Normal file
143
app/src/main/java/com/topjohnwu/magisk/utils/Keygen.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package com.topjohnwu.magisk.utils
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Base64OutputStream
|
||||||
|
import com.topjohnwu.magisk.Config
|
||||||
|
import com.topjohnwu.magisk.di.koinModules
|
||||||
|
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||||
|
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||||
|
import com.topjohnwu.superuser.internal.InternalUtils
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||||
|
import org.koin.core.context.GlobalContext
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.KeyStore
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.*
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
|
import java.util.zip.GZIPOutputStream
|
||||||
|
|
||||||
|
private interface CertKeyProvider {
|
||||||
|
val cert: X509Certificate
|
||||||
|
val key: PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
class Keygen: CertKeyProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ALIAS = "magisk"
|
||||||
|
private val PASSWORD get() = "magisk".toCharArray()
|
||||||
|
private const val TESTKEY_CERT = "61ed377e85d386a8dfee6b864bd85b0bfaa5af81"
|
||||||
|
private const val ALPHANUM = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
private const val BASE64_FLAG = Base64.NO_PADDING or Base64.NO_WRAP
|
||||||
|
}
|
||||||
|
|
||||||
|
private val start = Calendar.getInstance()
|
||||||
|
private val end = Calendar.getInstance().apply { add(Calendar.YEAR, 30) }
|
||||||
|
|
||||||
|
override val cert get() = provider.cert
|
||||||
|
override val key get() = provider.key
|
||||||
|
|
||||||
|
private val provider: CertKeyProvider
|
||||||
|
|
||||||
|
inner class KeyStoreProvider : CertKeyProvider {
|
||||||
|
private val ks by lazy { init() }
|
||||||
|
override val cert by lazy { ks.getCertificate(ALIAS) as X509Certificate }
|
||||||
|
override val key by lazy { ks.getKey(ALIAS, PASSWORD) as PrivateKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestProvider : CertKeyProvider {
|
||||||
|
override val cert by lazy {
|
||||||
|
readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem"))
|
||||||
|
}
|
||||||
|
override val key by lazy {
|
||||||
|
readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// This object could possibly be accessed from an external app
|
||||||
|
// Get context from reflection into Android's framework
|
||||||
|
val context = InternalUtils.getContext()
|
||||||
|
val pm = context.packageManager
|
||||||
|
val info = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
|
||||||
|
val sig = info.signatures[0]
|
||||||
|
val digest = MessageDigest.getInstance("SHA1")
|
||||||
|
val chksum = digest.digest(sig.toByteArray())
|
||||||
|
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (b in chksum) {
|
||||||
|
sb.append("%02x".format(0xFF and b.toInt()))
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = if (sb.toString() == TESTKEY_CERT) {
|
||||||
|
// The app was signed by the test key, continue to use it (legacy mode)
|
||||||
|
TestProvider()
|
||||||
|
} else {
|
||||||
|
KeyStoreProvider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun randomString(): String {
|
||||||
|
val rand = kotlin.random.Random.Default
|
||||||
|
val len = rand.nextInt(5, 10)
|
||||||
|
val sb = StringBuilder(len)
|
||||||
|
for (i in 0..len) {
|
||||||
|
val idx = rand.nextInt(ALPHANUM.length)
|
||||||
|
sb.append(ALPHANUM[idx])
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun init(): KeyStore {
|
||||||
|
GlobalContext.getOrNull() ?: {
|
||||||
|
// Invoked externally, do some basic initialization
|
||||||
|
startKoin {
|
||||||
|
modules(koinModules)
|
||||||
|
}
|
||||||
|
Timber.plant(Timber.DebugTree())
|
||||||
|
}()
|
||||||
|
|
||||||
|
val raw = Config.keyStoreRaw
|
||||||
|
val ks = KeyStore.getInstance("PKCS12")
|
||||||
|
if (raw.isEmpty()) {
|
||||||
|
ks.load(null)
|
||||||
|
} else {
|
||||||
|
GZIPInputStream(Base64.decode(raw, BASE64_FLAG).inputStream()).use {
|
||||||
|
ks.load(it, PASSWORD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys already exist
|
||||||
|
if (ks.containsAlias(ALIAS))
|
||||||
|
return ks
|
||||||
|
|
||||||
|
// Generate new private key and certificate
|
||||||
|
val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(4096) }.genKeyPair()
|
||||||
|
val dname = X500Name("CN=${randomString()}")
|
||||||
|
val builder = JcaX509v3CertificateBuilder(dname, BigInteger(160, Random()),
|
||||||
|
start.time, end.time, dname, kp.public)
|
||||||
|
val signer = JcaContentSignerBuilder("SHA256WithRSA").build(kp.private)
|
||||||
|
val cert = JcaX509CertificateConverter().getCertificate(builder.build(signer))
|
||||||
|
|
||||||
|
// Store them into keystore
|
||||||
|
ks.setKeyEntry(ALIAS, kp.private, PASSWORD, arrayOf(cert))
|
||||||
|
val bytes = ByteArrayOutputStream()
|
||||||
|
GZIPOutputStream(Base64OutputStream(bytes, BASE64_FLAG)).use {
|
||||||
|
ks.store(it, PASSWORD)
|
||||||
|
}
|
||||||
|
Config.keyStoreRaw = bytes.toString("UTF-8")
|
||||||
|
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
}
|
@@ -1,68 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.ContextWrapper
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.content.res.Resources
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import com.topjohnwu.magisk.Config
|
|
||||||
import com.topjohnwu.magisk.R
|
|
||||||
import com.topjohnwu.magisk.extensions.get
|
|
||||||
import com.topjohnwu.magisk.extensions.inject
|
|
||||||
import com.topjohnwu.magisk.extensions.langTagToLocale
|
|
||||||
import com.topjohnwu.superuser.internal.InternalUtils
|
|
||||||
import io.reactivex.Single
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
var currentLocale = Locale.getDefault()!!
|
|
||||||
private set
|
|
||||||
|
|
||||||
val defaultLocale = Locale.getDefault()!!
|
|
||||||
|
|
||||||
val availableLocales = Single.fromCallable {
|
|
||||||
val compareId = R.string.app_changelog
|
|
||||||
val res: Resources by inject()
|
|
||||||
mutableListOf<Locale>().apply {
|
|
||||||
// Add default locale
|
|
||||||
add(Locale.ENGLISH)
|
|
||||||
|
|
||||||
// Add some special locales
|
|
||||||
add(Locale.TAIWAN)
|
|
||||||
add(Locale("pt", "BR"))
|
|
||||||
|
|
||||||
// Other locales
|
|
||||||
val otherLocales = res.assets.locales
|
|
||||||
.map { it.langTagToLocale() }
|
|
||||||
.distinctBy { LocaleManager.getString(it, compareId) }
|
|
||||||
|
|
||||||
listOf("", "").toTypedArray()
|
|
||||||
|
|
||||||
addAll(otherLocales)
|
|
||||||
}.sortedWith(Comparator { a, b ->
|
|
||||||
a.getDisplayName(a).toLowerCase(a)
|
|
||||||
.compareTo(b.getDisplayName(b).toLowerCase(b))
|
|
||||||
})
|
|
||||||
}.cache()!!
|
|
||||||
|
|
||||||
object LocaleManager {
|
|
||||||
|
|
||||||
fun setLocale(wrapper: ContextWrapper) {
|
|
||||||
val localeConfig = Config.locale
|
|
||||||
currentLocale = when {
|
|
||||||
localeConfig.isEmpty() -> defaultLocale
|
|
||||||
else -> localeConfig.langTagToLocale()
|
|
||||||
}
|
|
||||||
Locale.setDefault(currentLocale)
|
|
||||||
InternalUtils.replaceBaseContext(wrapper, getLocaleContext(wrapper, currentLocale))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLocaleContext(context: Context, locale: Locale = currentLocale): Context {
|
|
||||||
val config = Configuration(context.resources.configuration)
|
|
||||||
config.setLocale(locale)
|
|
||||||
return context.createConfigurationContext(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getString(locale: Locale, @StringRes id: Int): String {
|
|
||||||
return getLocaleContext(get(), locale).getString(id)
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user