mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 10:27:29 +00:00
Compare commits
287 Commits
manager-v7
...
manager-v7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
120bd6cd68 | ||
![]() |
9aef06d1b8 | ||
![]() |
e6e9dd751c | ||
![]() |
5dd677756f | ||
![]() |
b77c590910 | ||
![]() |
7e5f2822ae | ||
![]() |
12bbc7fd6b | ||
![]() |
bf9ac8252b | ||
![]() |
4a3f5dc619 | ||
![]() |
ca156befbd | ||
![]() |
4db41e2ac4 | ||
![]() |
982a43fce1 | ||
![]() |
dd76a74e1c | ||
![]() |
70cb52b2c7 | ||
![]() |
5c7f69acaa | ||
![]() |
f1d9015e5f | ||
![]() |
e8d900c58e | ||
![]() |
a6241ae912 | ||
![]() |
4a697ca2ec | ||
![]() |
58bec7f2c9 | ||
![]() |
213f84985c | ||
![]() |
074b1f8c61 | ||
![]() |
326eee8c83 | ||
![]() |
00bff4912e | ||
![]() |
0ce1720516 | ||
![]() |
ee407472cf | ||
![]() |
f341f3b2dd | ||
![]() |
8513946e09 | ||
![]() |
8ebd9c8927 | ||
![]() |
1d54c5144e | ||
![]() |
e40d4318fa | ||
![]() |
7756e10779 | ||
![]() |
3e58d502d0 | ||
![]() |
1c8846dc57 | ||
![]() |
2f320c7239 | ||
![]() |
e799918ab6 | ||
![]() |
86c4928e0f | ||
![]() |
0293eb5c51 | ||
![]() |
1ee75b6aa6 | ||
![]() |
4b30b224b5 | ||
![]() |
16b232d2a3 | ||
![]() |
3f3b1f5b1d | ||
![]() |
cec017b7bf | ||
![]() |
3123cc1059 | ||
![]() |
caa9df86bc | ||
![]() |
f417389a7a | ||
![]() |
662a5c8ea6 | ||
![]() |
7edfbfb764 | ||
![]() |
c1602d2554 | ||
![]() |
9f8d4e1022 | ||
![]() |
d1dfda405f | ||
![]() |
28efded624 | ||
![]() |
06c86ee267 | ||
![]() |
5892780871 | ||
![]() |
4fcdcd9a8a | ||
![]() |
80d834fb55 | ||
![]() |
4122ebe18f | ||
![]() |
7d87777bf8 | ||
![]() |
4a73d634e0 | ||
![]() |
373dc10a40 | ||
![]() |
ed43ec8ea2 | ||
![]() |
7918fc3528 | ||
![]() |
bf58205b0a | ||
![]() |
c0d1ce96d1 | ||
![]() |
b31d3802eb | ||
![]() |
be1228c3b4 | ||
![]() |
15c94c6b34 | ||
![]() |
202d23426a | ||
![]() |
fc26de48b2 | ||
![]() |
76c88913f9 | ||
![]() |
a3a1aed723 | ||
![]() |
81aa56f60f | ||
![]() |
73bb850209 | ||
![]() |
8dfec12330 | ||
![]() |
ae24397793 | ||
![]() |
3b0f888407 | ||
![]() |
845d1e02b0 | ||
![]() |
5d357bc41f | ||
![]() |
6a54672b13 | ||
![]() |
3d9a15df44 | ||
![]() |
449c7fda2f | ||
![]() |
8b7b05da68 | ||
![]() |
92400ebcab | ||
![]() |
23d3e56967 | ||
![]() |
6785dc4967 | ||
![]() |
dad20f6a2d | ||
![]() |
bb15671046 | ||
![]() |
21984fac8b | ||
![]() |
f392afe87f | ||
![]() |
6a243ec7bc | ||
![]() |
8cd3b603df | ||
![]() |
6e1aefe6d8 | ||
![]() |
1c90b6eca3 | ||
![]() |
c33cf9f878 | ||
![]() |
27cb40eec9 | ||
![]() |
c06081b75d | ||
![]() |
a7eec2f0a0 | ||
![]() |
4fd0fe3194 | ||
![]() |
cc74593ddd | ||
![]() |
fdb7c5dba1 | ||
![]() |
77470c7cfa | ||
![]() |
f0a734fdab | ||
![]() |
75405b2b25 | ||
![]() |
90ed4b3c49 | ||
![]() |
290a17a764 | ||
![]() |
aaabd836e4 | ||
![]() |
076e5cea3b | ||
![]() |
8515971ccf | ||
![]() |
d86fb033ea | ||
![]() |
99d7d8ddbc | ||
![]() |
df78fd2d41 | ||
![]() |
dabe6267b9 | ||
![]() |
0119ebddbe | ||
![]() |
3216ef9f47 | ||
![]() |
b79d1bcded | ||
![]() |
17e234f9d5 | ||
![]() |
ea1f75f80e | ||
![]() |
8c40db5730 | ||
![]() |
6fe03d2795 | ||
![]() |
c595a87ccf | ||
![]() |
fac07c3913 | ||
![]() |
c63fdbbc6b | ||
![]() |
2ff5d9606b | ||
![]() |
ed43452c1a | ||
![]() |
8f28d4028f | ||
![]() |
b54543b18c | ||
![]() |
966d6593ca | ||
![]() |
ad95b1c9d1 | ||
![]() |
3bfa38c60a | ||
![]() |
0bdbcad8be | ||
![]() |
80855e89ec | ||
![]() |
0850401dc4 | ||
![]() |
337fda2023 | ||
![]() |
64f238191e | ||
![]() |
eb169cb133 | ||
![]() |
80cd85b061 | ||
![]() |
89275270f3 | ||
![]() |
e7339ba619 | ||
![]() |
d9ad7d522c | ||
![]() |
92789c3113 | ||
![]() |
c1c677e161 | ||
![]() |
2fe917ff82 | ||
![]() |
0e6c205732 | ||
![]() |
125ae0a173 | ||
![]() |
0245e13591 | ||
![]() |
d546733287 | ||
![]() |
c275326d59 | ||
![]() |
d4561507b8 | ||
![]() |
ef0e22cc41 | ||
![]() |
62db65bf18 | ||
![]() |
d5371f752c | ||
![]() |
a5f5e94115 | ||
![]() |
2624706c69 | ||
![]() |
d39d885ec2 | ||
![]() |
d83c744725 | ||
![]() |
843995cdb9 | ||
![]() |
9491ba77e0 | ||
![]() |
58a449d437 | ||
![]() |
7f55e0f05b | ||
![]() |
67c3f40adb | ||
![]() |
ff7a0ba599 | ||
![]() |
b152c63102 | ||
![]() |
415ff23be5 | ||
![]() |
b0d6de783e | ||
![]() |
ac28e6e5ca | ||
![]() |
4f9e8d2e8a | ||
![]() |
21be2f46f3 | ||
![]() |
a6e7680212 | ||
![]() |
e79e744e08 | ||
![]() |
7abdac72a4 | ||
![]() |
90d85eaf7d | ||
![]() |
e65f9740fb | ||
![]() |
7538f89b56 | ||
![]() |
7c755a3991 | ||
![]() |
10e903c9fc | ||
![]() |
b018124226 | ||
![]() |
a9350f50c9 | ||
![]() |
ed7babcbf1 | ||
![]() |
61ebc335c4 | ||
![]() |
0167bd76f1 | ||
![]() |
79d704008b | ||
![]() |
0a703585b0 | ||
![]() |
5d632d0d90 | ||
![]() |
4eecaea601 | ||
![]() |
63055818ec | ||
![]() |
0beb08b687 | ||
![]() |
b27801a27c | ||
![]() |
a0cfce7cbc | ||
![]() |
8b7144c986 | ||
![]() |
d3f5f5ee59 | ||
![]() |
a2a3c7f438 | ||
![]() |
4496f82d5b | ||
![]() |
09d531557d | ||
![]() |
7fee82f731 | ||
![]() |
475054c48a | ||
![]() |
a743d05751 | ||
![]() |
d1ed502e03 | ||
![]() |
37744c7ab6 | ||
![]() |
bbc9e60a12 | ||
![]() |
6c975ecc4c | ||
![]() |
23e8a4ce4b | ||
![]() |
50134a2f9b | ||
![]() |
628b37c4fa | ||
![]() |
1b4ae70a43 | ||
![]() |
b25c49725f | ||
![]() |
b245782c7e | ||
![]() |
a9f32baae0 | ||
![]() |
e7ef71865d | ||
![]() |
88c4f72b37 | ||
![]() |
abbcdf91a5 | ||
![]() |
b876df6e21 | ||
![]() |
4bb81f35d7 | ||
![]() |
ff20267b3f | ||
![]() |
2c9586d811 | ||
![]() |
2813d2031a | ||
![]() |
065051a360 | ||
![]() |
db218407b0 | ||
![]() |
d52210dd90 | ||
![]() |
f3cd9a096a | ||
![]() |
e426090a18 | ||
![]() |
cbe64fd559 | ||
![]() |
63ea7a70bd | ||
![]() |
fb0998f7a2 | ||
![]() |
a9b00dd537 | ||
![]() |
52eb059515 | ||
![]() |
7640246255 | ||
![]() |
52c83b2916 | ||
![]() |
d9cded0fc9 | ||
![]() |
750c42caf1 | ||
![]() |
bbf650c6cf | ||
![]() |
a25dace7e0 | ||
![]() |
14ff22fbcd | ||
![]() |
07eb7dda2d | ||
![]() |
d4058175b4 | ||
![]() |
2de984ae24 | ||
![]() |
761a8bf2a9 | ||
![]() |
6df7006b36 | ||
![]() |
aceb3ee863 | ||
![]() |
11d716a3c8 | ||
![]() |
7cc8c014eb | ||
![]() |
f21241d944 | ||
![]() |
a181fa0652 | ||
![]() |
3f748b4d2a | ||
![]() |
683450f9c6 | ||
![]() |
adbd47a36c | ||
![]() |
ce693aa5e9 | ||
![]() |
ad80804461 | ||
![]() |
2d55632430 | ||
![]() |
e81f00ef1a | ||
![]() |
93fb0e3d74 | ||
![]() |
71ce0de606 | ||
![]() |
0407062c1d | ||
![]() |
cda14af208 | ||
![]() |
258f170cd7 | ||
![]() |
f76015d714 | ||
![]() |
7e5e14163c | ||
![]() |
bcd1064e94 | ||
![]() |
8a8441c875 | ||
![]() |
15aa813416 | ||
![]() |
605faccffd | ||
![]() |
79f2d08c81 | ||
![]() |
0568ae5391 | ||
![]() |
5330dda9f8 | ||
![]() |
ebab126579 | ||
![]() |
0e5417a13e | ||
![]() |
9a968e0584 | ||
![]() |
ffec64d209 | ||
![]() |
f332746188 | ||
![]() |
b2fa5b551e | ||
![]() |
36e83edddc | ||
![]() |
6b045eadef | ||
![]() |
147264822c | ||
![]() |
36e4ccd800 | ||
![]() |
796c16237d | ||
![]() |
861ad9881c | ||
![]() |
3101c538e9 | ||
![]() |
42adc7382f | ||
![]() |
9bb4dfad13 | ||
![]() |
bd00ae8ede | ||
![]() |
f309522268 | ||
![]() |
0efaddff23 | ||
![]() |
94ba7cb0c5 | ||
![]() |
2d58c725e0 | ||
![]() |
e035523eb8 | ||
![]() |
bea5308ab7 | ||
![]() |
f006a85fec | ||
![]() |
ea93013ebc |
104
app/build.gradle
104
app/build.gradle
@@ -1,6 +1,6 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
kapt {
|
||||
@@ -16,20 +16,41 @@ android {
|
||||
defaultConfig {
|
||||
applicationId 'com.topjohnwu.magisk'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled true
|
||||
versionName configProps['appVersion']
|
||||
versionCode configProps['appVersionCode'] as Integer
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
argument('butterknife.debuggable', 'false')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
'proguard-rules.pro', 'proguard-kotlin.pro'
|
||||
}
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude '/META-INF/*.version'
|
||||
exclude '/META-INF/*.kotlin_module'
|
||||
exclude '/META-INF/rxkotlin.properties'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -37,30 +58,57 @@ dependencies {
|
||||
implementation project(':net')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
|
||||
implementation 'com.github.topjohnwu:jtar:1.0.0'
|
||||
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0'
|
||||
implementation 'com.github.sevar83:indeterminate-checkbox:1.0.5'
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.github.skoumalcz:teanity:0.3.3'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
|
||||
def markwonVersion = '3.0.0'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
implementation "ru.noties.markwon:html:${markwonVersion}"
|
||||
implementation "ru.noties.markwon:image-svg:${markwonVersion}"
|
||||
def vMarkwon = '3.0.1'
|
||||
implementation "ru.noties.markwon:core:${vMarkwon}"
|
||||
implementation "ru.noties.markwon:html:${vMarkwon}"
|
||||
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
|
||||
|
||||
def vLibsu = '2.5.0'
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
def vKoin = "2.0.1"
|
||||
implementation "org.koin:koin-core:${vKoin}"
|
||||
implementation "org.koin:koin-android:${vKoin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||
|
||||
def vRetrofit = "2.6.0"
|
||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||
|
||||
def vOkHttp = "3.12.3"
|
||||
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
def vMoshi = "1.8.0"
|
||||
implementation "com.squareup.moshi:moshi:${vMoshi}"
|
||||
|
||||
def vKotshi = "2.0.1"
|
||||
implementation "se.ansman.kotshi:api:${vKotshi}"
|
||||
kapt "se.ansman.kotshi:compiler:${vKotshi}"
|
||||
|
||||
modules {
|
||||
module('androidx.room:room-runtime') {
|
||||
replacedBy('com.github.topjohnwu:room-runtime')
|
||||
}
|
||||
}
|
||||
def vRoom = "2.1.0"
|
||||
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
|
||||
|
||||
def androidXVersion = "1.0.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation "androidx.preference:preference:${androidXVersion}"
|
||||
implementation "androidx.recyclerview:recyclerview:${androidXVersion}"
|
||||
implementation "androidx.cardview:cardview:${androidXVersion}"
|
||||
implementation "com.google.android.material:material:${androidXVersion}"
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.0.1'
|
||||
implementation 'androidx.transition:transition:1.1.0-beta01'
|
||||
|
||||
def libsuVersion = '2.5.0'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
|
||||
def butterKnifeVersion = '10.1.0'
|
||||
implementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
|
||||
kapt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"
|
||||
implementation 'androidx.transition:transition:1.2.0-alpha01'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha07'
|
||||
}
|
||||
|
20
app/proguard-kotlin.pro
Normal file
20
app/proguard-kotlin.pro
Normal file
@@ -0,0 +1,20 @@
|
||||
## So every class is case insensitive to avoid some bizare problems
|
||||
-dontusemixedcaseclassnames
|
||||
|
||||
## If reflection issues come up uncomment this, that should temporarily fix it
|
||||
#-keep class kotlin.** { *; }
|
||||
#-keep class kotlin.Metadata { *; }
|
||||
#-keepclassmembers class kotlin.Metadata {
|
||||
# public <methods>;
|
||||
#}
|
||||
|
||||
## Never warn about Kotlin, it should work as-is
|
||||
-dontwarn kotlin.**
|
||||
|
||||
## Removes runtime null checks - doesn't really matter if it crashes on kotlin or java NPE
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||
}
|
||||
|
||||
## Useless option for dex
|
||||
-dontpreverify
|
18
app/proguard-rules.pro
vendored
18
app/proguard-rules.pro
vendored
@@ -16,12 +16,6 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# BouncyCastle
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.rsa.**SHA1** { *; }
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.asymmetric.RSA** { *; }
|
||||
-keep,allowoptimization class org.bouncycastle.jcajce.provider.digest.SHA1** { *; }
|
||||
-dontwarn javax.naming.**
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
|
||||
@@ -29,19 +23,19 @@
|
||||
void onResponse(int);
|
||||
}
|
||||
|
||||
# Keep all fragment constructors
|
||||
-keepclassmembers class * extends androidx.fragment.app.Fragment {
|
||||
public <init>(...);
|
||||
}
|
||||
|
||||
# DelegateWorker
|
||||
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||
|
||||
# BootSigner
|
||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
||||
|
||||
# SVG
|
||||
-dontwarn com.caverock.androidsvg.SVGAndroidRenderer
|
||||
|
||||
# RetroStreams
|
||||
-dontwarn java9.**
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
||||
public *** debug(...);
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name="a.e"
|
||||
android:theme="@style/AppTheme"
|
||||
android:theme="@style/MagiskTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
android:name="a.f"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:screenOrientation="nosensor"
|
||||
android:theme="@style/AppTheme.NoDrawer" />
|
||||
android:theme="@style/MagiskTheme.Flashing" />
|
||||
|
||||
<!-- Superuser -->
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
android:exported="false"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@style/SuRequest" />
|
||||
android:theme="@style/MagiskTheme.SU" />
|
||||
|
||||
<!-- Receiver -->
|
||||
|
||||
@@ -74,4 +74,4 @@
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
@@ -1,101 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB;
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.RootUtils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
public class App extends Application implements Application.ActivityLifecycleCallbacks {
|
||||
|
||||
public static App self;
|
||||
public static Context deContext;
|
||||
public static ThreadPoolExecutor THREAD_POOL;
|
||||
|
||||
// Global resources
|
||||
public SharedPreferences prefs;
|
||||
public MagiskDB mDB;
|
||||
public RepoDatabaseHelper repoDB;
|
||||
private volatile BaseActivity foreground;
|
||||
|
||||
static {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER | Shell.FLAG_USE_MAGISK_BUSYBOX);
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG);
|
||||
Shell.Config.addInitializers(RootUtils.class);
|
||||
Shell.Config.setTimeout(2);
|
||||
THREAD_POOL = (ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
self = this;
|
||||
deContext = base;
|
||||
registerActivityLifecycleCallbacks(this);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
deContext = base.createDeviceProtectedStorageContext();
|
||||
deContext.moveSharedPreferencesFrom(base,
|
||||
PreferenceManager.getDefaultSharedPreferencesName(base));
|
||||
}
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(deContext);
|
||||
mDB = new MagiskDB(base);
|
||||
|
||||
Networking.init(base);
|
||||
LocaleManager.setLocale(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
LocaleManager.setLocale(this);
|
||||
}
|
||||
|
||||
public static BaseActivity foreground() {
|
||||
return self.foreground;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle bundle) {}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(@NonNull Activity activity) {}
|
||||
|
||||
@Override
|
||||
public synchronized void onActivityResumed(@NonNull Activity activity) {
|
||||
foreground = (BaseActivity) activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onActivityPaused(@NonNull Activity activity) {
|
||||
foreground = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(@NonNull Activity activity) {}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle bundle) {}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(@NonNull Activity activity) {}
|
||||
}
|
128
app/src/main/java/com/topjohnwu/magisk/App.kt
Normal file
128
app/src/main/java/com/topjohnwu/magisk/App.kt
Normal file
@@ -0,0 +1,128 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.room.Room
|
||||
import androidx.work.impl.WorkDatabase
|
||||
import androidx.work.impl.WorkDatabase_Impl
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
|
||||
open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
|
||||
lateinit var protectedContext: Context
|
||||
|
||||
@Volatile
|
||||
private var foreground: Activity? = null
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
if (BuildConfig.DEBUG)
|
||||
MultiDex.install(base)
|
||||
Timber.plant(Timber.DebugTree())
|
||||
|
||||
startKoin {
|
||||
androidContext(this@App)
|
||||
modules(koinModules)
|
||||
}
|
||||
|
||||
protectedContext = baseContext
|
||||
self = this
|
||||
deContext = base
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
protectedContext = base.createDeviceProtectedStorageContext()
|
||||
deContext = protectedContext
|
||||
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
|
||||
}
|
||||
|
||||
registerActivityLifecycleCallbacks(this)
|
||||
|
||||
Networking.init(base)
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
//region ActivityLifecycleCallbacks
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
//endregion
|
||||
|
||||
private val Context.defaultPrefsName get() = "${packageName}_preferences"
|
||||
|
||||
companion object {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection")
|
||||
@JvmStatic
|
||||
lateinit var self: App
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection; replace with protectedContext")
|
||||
@JvmStatic
|
||||
lateinit var deContext: Context
|
||||
|
||||
@Deprecated("Use Rx or similar")
|
||||
@JvmField
|
||||
var THREAD_POOL: ThreadPoolExecutor
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootUtils::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
@JvmStatic
|
||||
fun foreground(): Activity? {
|
||||
val app: App by inject()
|
||||
return app.foreground
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,31 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
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 java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ClassMap {
|
||||
private static Map<Class, Class> classMap = new HashMap<>();
|
||||
|
||||
static {
|
||||
classMap.put(App.class, a.e.class);
|
||||
classMap.put(MainActivity.class, a.b.class);
|
||||
classMap.put(SplashActivity.class, a.c.class);
|
||||
classMap.put(FlashActivity.class, a.f.class);
|
||||
classMap.put(UpdateCheckService.class, a.g.class);
|
||||
classMap.put(GeneralReceiver.class, a.h.class);
|
||||
classMap.put(DownloadModuleService.class, a.j.class);
|
||||
classMap.put(SuRequestActivity.class, a.m.class);
|
||||
}
|
||||
|
||||
public static <T> Class<T> get(Class c) {
|
||||
return classMap.get(c);
|
||||
}
|
||||
}
|
27
app/src/main/java/com/topjohnwu/magisk/ClassMap.kt
Normal file
27
app/src/main/java/com/topjohnwu/magisk/ClassMap.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService
|
||||
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,
|
||||
DownloadModuleService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
operator fun <T : Class<*>>get(c: Class<*>): T {
|
||||
return map.getOrElse(c) { throw IllegalArgumentException() } as T
|
||||
}
|
||||
}
|
@@ -1,396 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Xml;
|
||||
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Config {
|
||||
|
||||
// Current status
|
||||
public static String magiskVersionString;
|
||||
public static int magiskVersionCode = -1;
|
||||
private static boolean magiskHide;
|
||||
|
||||
// Update Info
|
||||
public static String remoteMagiskVersionString;
|
||||
public static int remoteMagiskVersionCode = -1;
|
||||
public static String magiskLink;
|
||||
public static String magiskNoteLink;
|
||||
public static String magiskMD5;
|
||||
public static String remoteManagerVersionString;
|
||||
public static int remoteManagerVersionCode = -1;
|
||||
public static String managerLink;
|
||||
public static String managerNoteLink;
|
||||
public static String uninstallerLink;
|
||||
|
||||
// Install flags
|
||||
public static boolean keepVerity = false;
|
||||
public static boolean keepEnc = false;
|
||||
public static boolean recovery = false;
|
||||
|
||||
public static int suLogTimeout = 14;
|
||||
|
||||
public static class Key {
|
||||
// su configs
|
||||
public static final String ROOT_ACCESS = "root_access";
|
||||
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
|
||||
public static final String SU_MNT_NS = "mnt_ns";
|
||||
public static final String SU_MANAGER = "requester";
|
||||
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
|
||||
public static final String SU_AUTO_RESPONSE = "su_auto_response";
|
||||
public static final String SU_NOTIFICATION = "su_notification";
|
||||
public static final String SU_REAUTH = "su_reauth";
|
||||
public static final String SU_FINGERPRINT = "su_fingerprint";
|
||||
|
||||
// prefs
|
||||
public static final String CHECK_UPDATES = "check_update";
|
||||
public static final String UPDATE_CHANNEL = "update_channel";
|
||||
public static final String CUSTOM_CHANNEL = "custom_channel";
|
||||
public static final String LOCALE = "locale";
|
||||
public static final String DARK_THEME = "dark_theme";
|
||||
public static final String ETAG_KEY = "ETag";
|
||||
public static final String REPO_ORDER = "repo_order";
|
||||
public static final String SHOW_SYSTEM_APP = "show_system";
|
||||
|
||||
// system state
|
||||
public static final String UPDATE_SERVICE_VER = "update_service_version";
|
||||
public static final String MAGISKHIDE = "magiskhide";
|
||||
public static final String COREONLY = "disable";
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public static final int DEFAULT_CHANNEL = -1;
|
||||
public static final int STABLE_CHANNEL = 0;
|
||||
public static final int BETA_CHANNEL = 1;
|
||||
public static final int CUSTOM_CHANNEL = 2;
|
||||
public static final int CANARY_CHANNEL = 3;
|
||||
public static final int CANARY_DEBUG_CHANNEL = 4;
|
||||
public static final int ROOT_ACCESS_DISABLED = 0;
|
||||
public static final int ROOT_ACCESS_APPS_ONLY = 1;
|
||||
public static final int ROOT_ACCESS_ADB_ONLY = 2;
|
||||
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
|
||||
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
|
||||
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
|
||||
public static final int MULTIUSER_MODE_USER = 2;
|
||||
public static final int NAMESPACE_MODE_GLOBAL = 0;
|
||||
public static final int NAMESPACE_MODE_REQUESTER = 1;
|
||||
public static final int NAMESPACE_MODE_ISOLATE = 2;
|
||||
public static final int NO_NOTIFICATION = 0;
|
||||
public static final int NOTIFICATION_TOAST = 1;
|
||||
public static final int SU_PROMPT = 0;
|
||||
public static final int SU_AUTO_DENY = 1;
|
||||
public static final int SU_AUTO_ALLOW = 2;
|
||||
public static final int[] TIMEOUT_LIST = {0, -1, 10, 20, 30, 60};
|
||||
public static final int ORDER_NAME = 0;
|
||||
public static final int ORDER_DATE = 1;
|
||||
}
|
||||
|
||||
public static void loadMagiskInfo() {
|
||||
try {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
||||
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
public static void export() {
|
||||
// Flush prefs to disk
|
||||
App app = App.self;
|
||||
app.prefs.edit().commit();
|
||||
File xml = new File(App.deContext.getFilesDir().getParent() + "/shared_prefs",
|
||||
app.getPackageName() + "_preferences.xml");
|
||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
SharedPreferences pref = App.self.prefs;
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
||||
if (config.exists()) {
|
||||
try {
|
||||
SuFileInputStream is = new SuFileInputStream(config);
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(is, "UTF-8");
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG)
|
||||
continue;
|
||||
String key = parser.getAttributeValue(null, "name");
|
||||
String value = parser.getAttributeValue(null, "value");
|
||||
switch (parser.getName()) {
|
||||
case "string":
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
editor.putString(key, parser.nextText());
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
break;
|
||||
case "boolean":
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean");
|
||||
editor.putBoolean(key, Boolean.parseBoolean(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean");
|
||||
break;
|
||||
case "int":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putInt(key, Integer.parseInt(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
case "long":
|
||||
parser.require(XmlPullParser.START_TAG, null, "long");
|
||||
editor.putLong(key, Long.parseLong(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "long");
|
||||
break;
|
||||
case "float":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putFloat(key, Float.parseFloat(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
default:
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
editor.remove(Key.ETAG_KEY);
|
||||
editor.apply();
|
||||
editor = pref.edit();
|
||||
config.delete();
|
||||
}
|
||||
|
||||
// Set defaults if not set
|
||||
setDefs(pref, editor);
|
||||
|
||||
// These settings are from actual device state
|
||||
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
|
||||
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static final int PREF_INT = 0;
|
||||
private static final int PREF_STR_INT = 1;
|
||||
private static final int PREF_BOOL = 2;
|
||||
private static final int PREF_STR = 3;
|
||||
private static final int DB_INT = 4;
|
||||
private static final int DB_BOOL = 5;
|
||||
private static final int DB_STR = 6;
|
||||
|
||||
private static int getConfigType(String key) {
|
||||
switch (key) {
|
||||
case Key.REPO_ORDER:
|
||||
return PREF_INT;
|
||||
|
||||
case Key.SU_REQUEST_TIMEOUT:
|
||||
case Key.SU_AUTO_RESPONSE:
|
||||
case Key.SU_NOTIFICATION:
|
||||
case Key.UPDATE_CHANNEL:
|
||||
return PREF_STR_INT;
|
||||
|
||||
case Key.DARK_THEME:
|
||||
case Key.SU_REAUTH:
|
||||
case Key.CHECK_UPDATES:
|
||||
case Key.MAGISKHIDE:
|
||||
case Key.COREONLY:
|
||||
case Key.SHOW_SYSTEM_APP:
|
||||
return PREF_BOOL;
|
||||
|
||||
case Key.CUSTOM_CHANNEL:
|
||||
case Key.LOCALE:
|
||||
case Key.ETAG_KEY:
|
||||
return PREF_STR;
|
||||
|
||||
case Key.ROOT_ACCESS:
|
||||
case Key.SU_MNT_NS:
|
||||
case Key.SU_MULTIUSER_MODE:
|
||||
return DB_INT;
|
||||
|
||||
case Key.SU_FINGERPRINT:
|
||||
return DB_BOOL;
|
||||
|
||||
case Key.SU_MANAGER:
|
||||
return DB_STR;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T get(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
return (T) (Integer) app.prefs.getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(app.prefs, key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) app.prefs.getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) app.prefs.getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) app.mDB.getSettings(key, getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (app.mDB.getSettings(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) app.mDB.getStrings(key, getDef(key));
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void set(String key, Object val) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
app.prefs.edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
app.prefs.edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
app.prefs.edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
app.prefs.edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
app.mDB.setSettings(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
app.mDB.setSettings(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.mDB.setStrings(key, (String) val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(String key) {
|
||||
App app = App.self;
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case PREF_STR_INT:
|
||||
case PREF_BOOL:
|
||||
case PREF_STR:
|
||||
app.prefs.edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
app.mDB.rmSettings(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.mDB.setStrings(key, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
|
||||
static {
|
||||
/* Set default configurations */
|
||||
|
||||
// prefs int
|
||||
defs.put(Key.REPO_ORDER, Value.ORDER_DATE);
|
||||
|
||||
// prefs string int
|
||||
defs.put(Key.SU_REQUEST_TIMEOUT, 10);
|
||||
defs.put(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
|
||||
defs.put(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
|
||||
defs.put(Key.UPDATE_CHANNEL, Utils.isCanary() ?
|
||||
Value.CANARY_DEBUG_CHANNEL : Value.DEFAULT_CHANNEL);
|
||||
|
||||
// prefs bool
|
||||
defs.put(Key.CHECK_UPDATES, true);
|
||||
defs.put(Key.DARK_THEME, true);
|
||||
//defs.put(Key.SU_REAUTH, false);
|
||||
//defs.put(Key.SHOW_SYSTEM_APP, false);
|
||||
|
||||
// prefs string
|
||||
defs.put(Key.CUSTOM_CHANNEL, "");
|
||||
defs.put(Key.LOCALE, "");
|
||||
//defs.put(Key.ETAG_KEY, null);
|
||||
|
||||
// db int
|
||||
defs.put(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
|
||||
defs.put(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
|
||||
defs.put(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
|
||||
|
||||
// db bool
|
||||
//defs.put(Key.SU_FINGERPRINT, false);
|
||||
|
||||
// db strings
|
||||
//defs.put(Key.SU_MANAGER, null);
|
||||
}
|
||||
|
||||
private static <T> T getDef(String key) {
|
||||
Object val = defs.get(key);
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case DB_INT:
|
||||
case PREF_STR_INT:
|
||||
return val != null ? (T) val : (T) (Integer) 0;
|
||||
case DB_BOOL:
|
||||
case PREF_BOOL:
|
||||
return val != null ? (T) val : (T) (Boolean) false;
|
||||
case DB_STR:
|
||||
case PREF_STR:
|
||||
return (T) val;
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
|
||||
App app = App.self;
|
||||
for (String key : defs.keySet()) {
|
||||
int type = getConfigType(key);
|
||||
switch (type) {
|
||||
case DB_INT:
|
||||
editor.putString(key, String.valueOf(
|
||||
app.mDB.getSettings(key, (Integer) defs.get(key))));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, app.mDB.getStrings(key, (String) defs.get(key)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = app.mDB.getSettings(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
|
||||
continue;
|
||||
}
|
||||
if (pref.contains(key))
|
||||
continue;
|
||||
switch (type) {
|
||||
case PREF_INT:
|
||||
editor.putInt(key, (Integer) defs.get(key));
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
editor.putString(key, String.valueOf(defs.get(key)));
|
||||
break;
|
||||
case PREF_STR:
|
||||
editor.putString(key, (String) defs.get(key));
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
editor.putBoolean(key, (Boolean) defs.get(key));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
196
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
196
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
@@ -0,0 +1,196 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Xml
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.*
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
|
||||
override val stringDao: StringDao by inject()
|
||||
override val settingsDao: SettingsDao by inject()
|
||||
override val context: Context by inject(Protected)
|
||||
|
||||
object Key {
|
||||
// db configs
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
const val SU_MULTIUSER_MODE = "multiuser_mode"
|
||||
const val SU_MNT_NS = "mnt_ns"
|
||||
const val SU_MANAGER = "requester"
|
||||
const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
// prefs
|
||||
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
|
||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val CHECK_UPDATES = "check_update"
|
||||
const val UPDATE_CHANNEL = "update_channel"
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
const val LOCALE = "locale"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
const val ETAG_KEY = "ETag"
|
||||
const val REPO_ORDER = "repo_order"
|
||||
const val SHOW_SYSTEM_APP = "show_system"
|
||||
|
||||
// system state
|
||||
const val MAGISKHIDE = "magiskhide"
|
||||
const val COREONLY = "disable"
|
||||
}
|
||||
|
||||
object Value {
|
||||
// Update channels
|
||||
const val DEFAULT_CHANNEL = -1
|
||||
const val STABLE_CHANNEL = 0
|
||||
const val BETA_CHANNEL = 1
|
||||
const val CUSTOM_CHANNEL = 2
|
||||
const val CANARY_CHANNEL = 3
|
||||
const val CANARY_DEBUG_CHANNEL = 4
|
||||
|
||||
// root access mode
|
||||
const val ROOT_ACCESS_DISABLED = 0
|
||||
const val ROOT_ACCESS_APPS_ONLY = 1
|
||||
const val ROOT_ACCESS_ADB_ONLY = 2
|
||||
const val ROOT_ACCESS_APPS_AND_ADB = 3
|
||||
|
||||
// su multiuser
|
||||
const val MULTIUSER_MODE_OWNER_ONLY = 0
|
||||
const val MULTIUSER_MODE_OWNER_MANAGED = 1
|
||||
const val MULTIUSER_MODE_USER = 2
|
||||
|
||||
// su mnt ns
|
||||
const val NAMESPACE_MODE_GLOBAL = 0
|
||||
const val NAMESPACE_MODE_REQUESTER = 1
|
||||
const val NAMESPACE_MODE_ISOLATE = 2
|
||||
|
||||
// su notification
|
||||
const val NO_NOTIFICATION = 0
|
||||
const val NOTIFICATION_TOAST = 1
|
||||
|
||||
// su auto response
|
||||
const val SU_PROMPT = 0
|
||||
const val SU_AUTO_DENY = 1
|
||||
const val SU_AUTO_ALLOW = 2
|
||||
|
||||
// su timeout
|
||||
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
|
||||
|
||||
// repo order
|
||||
const val ORDER_NAME = 0
|
||||
const val ORDER_DATE = 1
|
||||
}
|
||||
|
||||
private val defaultChannel =
|
||||
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
|
||||
else Value.DEFAULT_CHANNEL
|
||||
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
|
||||
var darkTheme by preference(Key.DARK_THEME, true)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
@JvmStatic
|
||||
var magiskHide by preference(Key.MAGISKHIDE, true)
|
||||
var coreOnly by preference(Key.COREONLY, false)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
|
||||
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
|
||||
var locale by preference(Key.LOCALE, "")
|
||||
@JvmStatic
|
||||
var etagKey by preference(Key.ETAG_KEY, "")
|
||||
|
||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||
@JvmStatic
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "")
|
||||
|
||||
fun initialize() = prefs.edit {
|
||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config).buffered()
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
val value: String = parser.getAttributeValue(null, "value")
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value.toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value.toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value.toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value.toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
config.delete()
|
||||
}
|
||||
remove(Key.ETAG_KEY)
|
||||
if (!prefs.contains(Key.UPDATE_CHANNEL))
|
||||
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
|
||||
|
||||
// Get actual state
|
||||
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
|
||||
// Write database configs
|
||||
putString(Key.ROOT_ACCESS, rootMode.toString())
|
||||
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
|
||||
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
|
||||
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().apply()
|
||||
val xml = File("${get<Context>(Protected).filesDir.parent}/shared_prefs",
|
||||
"${packageName}_preferences.xml")
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Const {
|
||||
|
||||
public static final String DEBUG_TAG = "MagiskManager";
|
||||
|
||||
// APK content
|
||||
public static final String ANDROID_MANIFEST = "AndroidManifest.xml";
|
||||
|
||||
public static final String SU_KEYSTORE_KEY = "su_key";
|
||||
|
||||
// Paths
|
||||
public static final String MAGISK_PATH = "/sbin/.magisk/img";
|
||||
public static final File EXTERNAL_PATH;
|
||||
public static File MAGISK_DISABLE_FILE;
|
||||
|
||||
static {
|
||||
MAGISK_DISABLE_FILE = new File("xxx");
|
||||
EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
|
||||
EXTERNAL_PATH.mkdirs();
|
||||
}
|
||||
|
||||
public static final String TMP_FOLDER_PATH = "/dev/tmp";
|
||||
public static final String MAGISK_LOG = "/cache/magisk.log";
|
||||
public static final String MANAGER_CONFIGS = ".tmp.magisk.config";
|
||||
|
||||
// Versions
|
||||
public static final int UPDATE_SERVICE_VER = 1;
|
||||
public static final int SNET_EXT_VER = 12;
|
||||
|
||||
public static final int USER_ID = Process.myUid() / 100000;
|
||||
|
||||
public static final class MAGISK_VER {
|
||||
public static final int MIN_SUPPORT = 18000;
|
||||
}
|
||||
|
||||
public static class ID {
|
||||
public static final int FETCH_ZIP = 2;
|
||||
public static final int SELECT_BOOT = 3;
|
||||
|
||||
// notifications
|
||||
public static final int MAGISK_UPDATE_NOTIFICATION_ID = 4;
|
||||
public static final int APK_UPDATE_NOTIFICATION_ID = 5;
|
||||
public static final int DTBO_NOTIFICATION_ID = 7;
|
||||
public static final int HIDE_MANAGER_NOTIFICATION_ID = 8;
|
||||
public static final String UPDATE_NOTIFICATION_CHANNEL = "update";
|
||||
public static final String PROGRESS_NOTIFICATION_CHANNEL = "progress";
|
||||
public static final String CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update";
|
||||
}
|
||||
|
||||
public static class Url {
|
||||
private static String getRaw(String where, String name) {
|
||||
return String.format("https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s", where, name);
|
||||
}
|
||||
public static final String STABLE_URL = getRaw("master", "stable.json");
|
||||
public static final String BETA_URL = getRaw("master", "beta.json");
|
||||
public static final String CANARY_URL = getRaw("master", "canary_builds/release.json");
|
||||
public static final String CANARY_DEBUG_URL = getRaw("master", "canary_builds/canary.json");
|
||||
public static final String REPO_URL = "https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d";
|
||||
public static final String FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s";
|
||||
public static final String ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip";
|
||||
public static final String MODULE_INSTALLER = "https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh";
|
||||
public static final String PAYPAL_URL = "https://www.paypal.me/topjohnwu";
|
||||
public static final String PATREON_URL = "https://www.patreon.com/topjohnwu";
|
||||
public static final String TWITTER_URL = "https://twitter.com/topjohnwu";
|
||||
public static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
|
||||
public static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk";
|
||||
public static final String SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk");
|
||||
public static final String BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl");
|
||||
}
|
||||
|
||||
public static class Key {
|
||||
// others
|
||||
public static final String LINK_KEY = "Link";
|
||||
public static final String IF_NONE_MATCH = "If-None-Match";
|
||||
// intents
|
||||
public static final String OPEN_SECTION = "section";
|
||||
public static final String INTENT_SET_NAME = "filename";
|
||||
public static final String INTENT_SET_LINK = "link";
|
||||
public static final String FLASH_ACTION = "action";
|
||||
public static final String BROADCAST_MANAGER_UPDATE = "manager_update";
|
||||
public static final String BROADCAST_REBOOT = "reboot";
|
||||
}
|
||||
|
||||
public static class Value {
|
||||
public static final String FLASH_ZIP = "flash";
|
||||
public static final String PATCH_FILE = "patch";
|
||||
public static final String FLASH_MAGISK = "magisk";
|
||||
public static final String FLASH_INACTIVE_SLOT = "slot";
|
||||
public static final String UNINSTALL = "uninstall";
|
||||
}
|
||||
|
||||
|
||||
}
|
98
app/src/main/java/com/topjohnwu/magisk/Const.kt
Normal file
98
app/src/main/java/com/topjohnwu/magisk/Const.kt
Normal file
@@ -0,0 +1,98 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Environment
|
||||
import android.os.Process
|
||||
|
||||
import java.io.File
|
||||
|
||||
object Const {
|
||||
|
||||
const val DEBUG_TAG = "MagiskManager"
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
@JvmField
|
||||
val EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)!!
|
||||
@JvmField
|
||||
var MAGISK_DISABLE_FILE = File("xxx")
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
// Versions
|
||||
const val SNET_EXT_VER = 12
|
||||
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
|
||||
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
|
||||
|
||||
// Misc
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
@JvmField
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
init {
|
||||
EXTERNAL_PATH.mkdirs()
|
||||
}
|
||||
|
||||
object MagiskVersion {
|
||||
const val MIN_SUPPORT = 18000
|
||||
}
|
||||
|
||||
object ID {
|
||||
const val FETCH_ZIP = 2
|
||||
const val SELECT_BOOT = 3
|
||||
|
||||
// notifications
|
||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||
const val DTBO_NOTIFICATION_ID = 7
|
||||
const val HIDE_MANAGER_NOTIFICATION_ID = 8
|
||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
||||
}
|
||||
|
||||
object Url {
|
||||
@Deprecated("This shouldn't be used. There's literally no need for it")
|
||||
const val REPO_URL =
|
||||
"https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d"
|
||||
const val FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s"
|
||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||
const val MODULE_INSTALLER =
|
||||
"https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh"
|
||||
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
||||
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
@JvmField
|
||||
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
|
||||
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
|
||||
|
||||
private fun getRaw(where: String, name: String) =
|
||||
"${GITHUB_RAW_API_URL}topjohnwu/magisk_files/$where/$name"
|
||||
}
|
||||
|
||||
object Key {
|
||||
// others
|
||||
const val LINK_KEY = "Link"
|
||||
const val IF_NONE_MATCH = "If-None-Match"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val INTENT_SET_NAME = "filename"
|
||||
const val INTENT_SET_LINK = "link"
|
||||
const val FLASH_ACTION = "action"
|
||||
const val BROADCAST_MANAGER_UPDATE = "manager_update"
|
||||
const val BROADCAST_REBOOT = "reboot"
|
||||
}
|
||||
|
||||
object Value {
|
||||
const val FLASH_ZIP = "flash"
|
||||
const val PATCH_FILE = "patch"
|
||||
const val FLASH_MAGISK = "magisk"
|
||||
const val FLASH_INACTIVE_SLOT = "slot"
|
||||
const val UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
|
||||
}
|
30
app/src/main/java/com/topjohnwu/magisk/Info.java
Normal file
30
app/src/main/java/com/topjohnwu/magisk/Info.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
|
||||
public final class Info {
|
||||
|
||||
public static int magiskVersionCode = -1;
|
||||
|
||||
@NonNull
|
||||
public static String magiskVersionString = "";
|
||||
|
||||
public static UpdateInfo remote = new UpdateInfo();
|
||||
|
||||
public static boolean keepVerity = false;
|
||||
public static boolean keepEnc = false;
|
||||
public static boolean recovery = false;
|
||||
|
||||
public static void loadMagiskInfo() {
|
||||
try {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
||||
Config.setMagiskHide(Shell.su("magiskhide --status").exec().isSuccess());
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class LogDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.LOG
|
||||
|
||||
fun deleteOutdated(
|
||||
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
lessThan("time", suTimeout.toString())
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun deleteAll() = query<Delete> {}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
orderBy("time", Order.DESC)
|
||||
}.flattenAsFlowable { it }
|
||||
.map { it.toLog() }
|
||||
.toList()
|
||||
|
||||
fun put(log: MagiskLog) = query<Insert> {
|
||||
values(log.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
}
|
@@ -1,190 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.model.entity.Policy;
|
||||
import com.topjohnwu.magisk.model.entity.SuLogEntry;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MagiskDB {
|
||||
|
||||
private static final String POLICY_TABLE = "policies";
|
||||
private static final String LOG_TABLE = "logs";
|
||||
private static final String SETTINGS_TABLE = "settings";
|
||||
private static final String STRINGS_TABLE = "strings";
|
||||
|
||||
private PackageManager pm;
|
||||
|
||||
public MagiskDB(Context context) {
|
||||
pm = context.getPackageManager();
|
||||
}
|
||||
|
||||
public void deletePolicy(Policy policy) {
|
||||
deletePolicy(policy.uid);
|
||||
}
|
||||
|
||||
private List<String> rawSQL(String fmt, Object... args) {
|
||||
return Shell.su("magisk --sqlite '" + Utils.fmt(fmt, args) + "'").exec().getOut();
|
||||
}
|
||||
|
||||
private List<ContentValues> SQL(String fmt, Object... args) {
|
||||
List<ContentValues> list = new ArrayList<>();
|
||||
for (String raw : rawSQL(fmt, args)) {
|
||||
ContentValues values = new ContentValues();
|
||||
String[] cols = raw.split("\\|");
|
||||
for (String col : cols) {
|
||||
String[] pair = col.split("=", 2);
|
||||
if (pair.length != 2)
|
||||
continue;
|
||||
values.put(pair[0], pair[1]);
|
||||
}
|
||||
list.add(values);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String toSQL(ContentValues values) {
|
||||
StringBuilder keys = new StringBuilder(), vals = new StringBuilder();
|
||||
keys.append('(');
|
||||
vals.append("VALUES(");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, Object> entry : values.valueSet()) {
|
||||
if (!first) {
|
||||
keys.append(',');
|
||||
vals.append(',');
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
keys.append(entry.getKey());
|
||||
vals.append('"');
|
||||
vals.append(entry.getValue());
|
||||
vals.append('"');
|
||||
}
|
||||
keys.append(')');
|
||||
vals.append(')');
|
||||
keys.append(vals);
|
||||
return keys.toString();
|
||||
}
|
||||
|
||||
public void clearOutdated() {
|
||||
rawSQL(
|
||||
"DELETE FROM %s WHERE until > 0 AND until < %d;" +
|
||||
"DELETE FROM %s WHERE time < %d",
|
||||
POLICY_TABLE, System.currentTimeMillis() / 1000,
|
||||
LOG_TABLE, System.currentTimeMillis() - Config.suLogTimeout * 86400000
|
||||
);
|
||||
}
|
||||
|
||||
public void deletePolicy(String pkg) {
|
||||
rawSQL("DELETE FROM %s WHERE package_name=\"%s\"", POLICY_TABLE, pkg);
|
||||
}
|
||||
|
||||
public void deletePolicy(int uid) {
|
||||
rawSQL("DELETE FROM %s WHERE uid=%d", POLICY_TABLE, uid);
|
||||
}
|
||||
|
||||
public Policy getPolicy(int uid) {
|
||||
List<ContentValues> res =
|
||||
SQL("SELECT * FROM %s WHERE uid=%d", POLICY_TABLE, uid);
|
||||
if (!res.isEmpty()) {
|
||||
try {
|
||||
return new Policy(res.get(0), pm);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
deletePolicy(uid);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void updatePolicy(Policy policy) {
|
||||
rawSQL("REPLACE INTO %s %s", POLICY_TABLE, toSQL(policy.getContentValues()));
|
||||
}
|
||||
|
||||
public List<Policy> getPolicyList() {
|
||||
List<Policy> list = new ArrayList<>();
|
||||
for (ContentValues values : SQL("SELECT * FROM %s WHERE uid/100000=%d", POLICY_TABLE, Const.USER_ID)) {
|
||||
try {
|
||||
list.add(new Policy(values, pm));
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
deletePolicy(values.getAsInteger("uid"));
|
||||
}
|
||||
}
|
||||
Collections.sort(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<List<SuLogEntry>> getLogs() {
|
||||
List<List<SuLogEntry>> ret = new ArrayList<>();
|
||||
List<SuLogEntry> list = null;
|
||||
String dateString = null, newString;
|
||||
for (ContentValues values : SQL("SELECT * FROM %s ORDER BY time DESC", LOG_TABLE)) {
|
||||
Date date = new Date(values.getAsLong("time"));
|
||||
newString = DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
|
||||
if (!TextUtils.equals(dateString, newString)) {
|
||||
dateString = newString;
|
||||
list = new ArrayList<>();
|
||||
ret.add(list);
|
||||
}
|
||||
list.add(new SuLogEntry(values));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void addLog(SuLogEntry log) {
|
||||
rawSQL("INSERT INTO %s %s", LOG_TABLE, toSQL(log.getContentValues()));
|
||||
}
|
||||
|
||||
public void clearLogs() {
|
||||
rawSQL("DELETE FROM %s", LOG_TABLE);
|
||||
}
|
||||
|
||||
public void rmSettings(String key) {
|
||||
rawSQL("DELETE FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
|
||||
}
|
||||
|
||||
public void setSettings(String key, int value) {
|
||||
ContentValues data = new ContentValues();
|
||||
data.put("key", key);
|
||||
data.put("value", value);
|
||||
rawSQL("REPLACE INTO %s %s", SETTINGS_TABLE, toSQL(data));
|
||||
}
|
||||
|
||||
public int getSettings(String key, int defaultValue) {
|
||||
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
|
||||
if (res.isEmpty())
|
||||
return defaultValue;
|
||||
return res.get(0).getAsInteger("value");
|
||||
}
|
||||
|
||||
public void setStrings(String key, String value) {
|
||||
if (value == null) {
|
||||
rawSQL("DELETE FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
|
||||
return;
|
||||
}
|
||||
ContentValues data = new ContentValues();
|
||||
data.put("key", key);
|
||||
data.put("value", value);
|
||||
rawSQL("REPLACE INTO %s %s", STRINGS_TABLE, toSQL(data));
|
||||
}
|
||||
|
||||
public String getStrings(String key, String defaultValue) {
|
||||
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
|
||||
if (res.isEmpty())
|
||||
return defaultValue;
|
||||
return res.get(0).getAsString("value");
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.utils.now
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class PolicyDao(
|
||||
private val context: Context
|
||||
) : BaseDao() {
|
||||
|
||||
override val table: String = DatabaseDefinition.Table.POLICY
|
||||
|
||||
fun deleteOutdated(
|
||||
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
greaterThan("until", "0")
|
||||
and {
|
||||
lessThan("until", nowSeconds.toString())
|
||||
}
|
||||
or {
|
||||
lessThan("until", "0")
|
||||
}
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun delete(packageName: String) = query<Delete> {
|
||||
condition {
|
||||
equals("package_name", packageName)
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun delete(uid: Int) = query<Delete> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(uid: Int) = query<Select> {
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.map { it.first().toPolicySafe() }
|
||||
|
||||
fun update(policy: MagiskPolicy) = query<Replace> {
|
||||
values(policy.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.map { it.mapNotNull { it.toPolicySafe() } }
|
||||
|
||||
|
||||
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
|
||||
val taskResult = runCatching { toPolicy(context.packageManager) }
|
||||
val result = taskResult.getOrNull()
|
||||
val exception = taskResult.exceptionOrNull()
|
||||
|
||||
Timber.e(exception)
|
||||
|
||||
when (exception) {
|
||||
is PackageManager.NameNotFoundException -> {
|
||||
val uid = getOrElse("uid") { null } ?: return null
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
@@ -1,107 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final int DATABASE_VER = 5;
|
||||
private static final String TABLE_NAME = "repos";
|
||||
|
||||
private SQLiteDatabase mDb;
|
||||
|
||||
public RepoDatabaseHelper(Context context) {
|
||||
super(context, "repo.db", null, DATABASE_VER);
|
||||
mDb = getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
if (oldVersion != newVersion) {
|
||||
// Nuke old DB and create new table
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
|
||||
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
|
||||
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))");
|
||||
Config.remove(Config.Key.ETAG_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
public void clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
|
||||
public void removeRepo(String id) {
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
}
|
||||
|
||||
public void removeRepo(Repo repo) {
|
||||
removeRepo(repo.getId());
|
||||
}
|
||||
|
||||
public void removeRepo(Iterable<String> list) {
|
||||
for (String id : list) {
|
||||
if (id == null) continue;
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
}
|
||||
}
|
||||
|
||||
public void addRepo(Repo repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.getContentValues());
|
||||
}
|
||||
|
||||
public Repo getRepo(String id) {
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) {
|
||||
if (c.moveToNext()) {
|
||||
return new Repo(c);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Cursor getRawCursor() {
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public Cursor getRepoCursor() {
|
||||
String orderBy = null;
|
||||
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
|
||||
case Config.Value.ORDER_NAME:
|
||||
orderBy = "name COLLATE NOCASE";
|
||||
break;
|
||||
case Config.Value.ORDER_DATE:
|
||||
orderBy = "last_update DESC";
|
||||
}
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
|
||||
}
|
||||
|
||||
public Set<String> getRepoIDSet() {
|
||||
HashSet<String> set = new HashSet<>(300);
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
while (c.moveToNext()) {
|
||||
set.add(c.getString(c.getColumnIndex("id")));
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
}
|
@@ -0,0 +1,109 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.entity.Repo
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("")
|
||||
class RepoDatabaseHelper
|
||||
constructor(context: Context) : SQLiteOpenHelper(context, "repo.db", null, DATABASE_VER) {
|
||||
|
||||
private val mDb: SQLiteDatabase = writableDatabase
|
||||
|
||||
val rawCursor: Cursor
|
||||
@Deprecated("")
|
||||
get() = mDb.query(TABLE_NAME, null, null, null, null, null, null)
|
||||
|
||||
val repoCursor: Cursor
|
||||
@Deprecated("")
|
||||
get() {
|
||||
var orderBy: String? = null
|
||||
when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> orderBy = "name COLLATE NOCASE"
|
||||
Config.Value.ORDER_DATE -> orderBy = "last_update DESC"
|
||||
}
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy)
|
||||
}
|
||||
|
||||
val repoIDSet: Set<String>
|
||||
@Deprecated("")
|
||||
get() {
|
||||
val set = HashSet<String>(300)
|
||||
mDb.query(TABLE_NAME, null, null, null, null, null, null).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
set.add(c.getString(c.getColumnIndex("id")))
|
||||
}
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
onUpgrade(db, 0, DATABASE_VER)
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
if (oldVersion != newVersion) {
|
||||
// Nuke old DB and create new table
|
||||
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
|
||||
db.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
|
||||
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
|
||||
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))")
|
||||
Config.prefs.edit {
|
||||
remove(Config.Key.ETAG_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
onUpgrade(db, 0, DATABASE_VER)
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
fun clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null)
|
||||
}
|
||||
|
||||
|
||||
@Deprecated("")
|
||||
fun removeRepo(id: String) {
|
||||
mDb.delete(TABLE_NAME, "id=?", arrayOf(id))
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
fun removeRepo(repo: Repo) {
|
||||
removeRepo(repo.id)
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
fun removeRepo(list: Iterable<String>) {
|
||||
list.forEach {
|
||||
mDb.delete(TABLE_NAME, "id=?", arrayOf(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
fun addRepo(repo: Repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.contentValues)
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
fun getRepo(id: String): Repo? {
|
||||
mDb.query(TABLE_NAME, null, "id=?", arrayOf(id), null, null, null).use { c ->
|
||||
if (c.moveToNext()) {
|
||||
return Repo(c)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATABASE_VER = 5
|
||||
private val TABLE_NAME = "repos"
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
|
||||
class SettingsDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.SETTINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: Int) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: Int = -1) = query<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
|
||||
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
|
||||
class StringDao : BaseDao() {
|
||||
|
||||
override val table = DatabaseDefinition.Table.STRINGS
|
||||
|
||||
fun delete(key: String) = query<Delete> {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: String) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: String = "") = query<Select> {
|
||||
fields("value")
|
||||
condition { equals("key", key) }
|
||||
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
abstract class BaseDao {
|
||||
|
||||
abstract val table: String
|
||||
|
||||
inline fun <reified Builder : MagiskQueryBuilder> query(builder: Builder.() -> Unit) =
|
||||
Builder::class.java.newInstance()
|
||||
.apply { table = this@BaseDao.table }
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let { MagiskQuery(it) }
|
||||
.query()
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
import androidx.annotation.AnyThread
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
object DatabaseDefinition {
|
||||
|
||||
object Table {
|
||||
const val POLICY = "policies"
|
||||
const val LOG = "logs"
|
||||
const val SETTINGS = "settings"
|
||||
const val STRINGS = "strings"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
fun MagiskQuery.query() = query.su()
|
||||
|
||||
fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
|
||||
fun String.su() = suRaw().map { it.toMap() }
|
||||
|
||||
fun List<String>.toMap() = map { it.split(Regex("\\|")) }
|
||||
.map { it.toMapInternal() }
|
||||
|
||||
private fun List<String>.toMapInternal() = map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { Pair(it[0], it[1]) }
|
||||
.toMap()
|
@@ -0,0 +1,5 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
inline class MagiskQuery(private val _query: String) {
|
||||
val query get() = "magisk --sqlite '$_query'"
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
package com.topjohnwu.magisk.data.database.base
|
||||
|
||||
import androidx.annotation.StringDef
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.ASC
|
||||
import com.topjohnwu.magisk.data.database.base.Order.Companion.DESC
|
||||
|
||||
interface MagiskQueryBuilder {
|
||||
|
||||
val requestType: String
|
||||
var table: String
|
||||
|
||||
companion object {
|
||||
inline operator fun <reified Builder : MagiskQueryBuilder> invoke(builder: Builder.() -> Unit): MagiskQuery =
|
||||
Builder::class.java.newInstance()
|
||||
.apply(builder)
|
||||
.toString()
|
||||
.let {
|
||||
MagiskQuery(it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Delete : MagiskQueryBuilder {
|
||||
override val requestType: String = "DELETE FROM"
|
||||
override var table = ""
|
||||
|
||||
private var condition = ""
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Select : MagiskQueryBuilder {
|
||||
override val requestType: String get() = "SELECT $fields FROM"
|
||||
override lateinit var table: String
|
||||
|
||||
private var fields = "*"
|
||||
private var condition = ""
|
||||
private var orderField = ""
|
||||
|
||||
fun fields(vararg newFields: String) {
|
||||
if (newFields.isEmpty()) {
|
||||
fields = "*"
|
||||
return
|
||||
}
|
||||
fields = newFields.joinToString(", ")
|
||||
}
|
||||
|
||||
fun condition(builder: Condition.() -> Unit) {
|
||||
condition = Condition().apply(builder).toString()
|
||||
}
|
||||
|
||||
fun orderBy(field: String, @OrderStrict order: String) {
|
||||
orderField = "ORDER BY $field $order"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Replace : Insert() {
|
||||
override val requestType: String = "REPLACE INTO"
|
||||
}
|
||||
|
||||
open class Insert : MagiskQueryBuilder {
|
||||
override val requestType: String = "INSERT INTO"
|
||||
override lateinit var table: String
|
||||
|
||||
private val keys get() = _values.keys.joinToString(",")
|
||||
private val values get() = _values.values.joinToString(",") {
|
||||
when (it) {
|
||||
is Boolean -> if (it) "1" else "0"
|
||||
is Number -> it.toString()
|
||||
else -> "\"$it\""
|
||||
}
|
||||
}
|
||||
private var _values: Map<String, Any> = mapOf()
|
||||
|
||||
fun values(vararg pairs: Pair<String, Any>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, Any>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
class Condition {
|
||||
|
||||
private val conditionWord = "WHERE %s"
|
||||
private var condition: String = ""
|
||||
|
||||
fun equals(field: String, value: Any) {
|
||||
condition = when (value) {
|
||||
is String -> "$field=\"$value\""
|
||||
else -> "$field=$value"
|
||||
}
|
||||
}
|
||||
|
||||
fun greaterThan(field: String, value: String) {
|
||||
condition = "$field > $value"
|
||||
}
|
||||
|
||||
fun lessThan(field: String, value: String) {
|
||||
condition = "$field < $value"
|
||||
}
|
||||
|
||||
fun greaterOrEqualTo(field: String, value: String) {
|
||||
condition = "$field >= $value"
|
||||
}
|
||||
|
||||
fun lessOrEqualTo(field: String, value: String) {
|
||||
condition = "$field <= $value"
|
||||
}
|
||||
|
||||
fun and(builder: Condition.() -> Unit) {
|
||||
condition = "($condition AND ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
fun or(builder: Condition.() -> Unit) {
|
||||
condition = "($condition OR ${Condition().apply(builder).condition})"
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return conditionWord.format(condition)
|
||||
}
|
||||
}
|
||||
|
||||
class Order {
|
||||
|
||||
@set:OrderStrict
|
||||
var order = DESC
|
||||
var field = ""
|
||||
|
||||
companion object {
|
||||
const val ASC = "ASC"
|
||||
const val DESC = "DESC"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@StringDef(ASC, DESC)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class OrderStrict
|
@@ -0,0 +1,62 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Streaming
|
||||
import retrofit2.http.Url
|
||||
|
||||
|
||||
interface GithubRawApiServices {
|
||||
|
||||
//region topjohnwu/magisk_files
|
||||
|
||||
@GET("$MAGISK_FILES/master/stable.json")
|
||||
fun fetchStableUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/release.json")
|
||||
fun fetchCanaryUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
|
||||
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
|
||||
|
||||
@GET
|
||||
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
|
||||
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* This method shall be used exclusively for fetching files from urls from previous requests.
|
||||
* Him, who uses it in a wrong way, shall die in an eternal flame.
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||
|
||||
|
||||
companion object {
|
||||
private const val REVISION = "revision"
|
||||
private const val MODULE = "module"
|
||||
private const val FILE = "file"
|
||||
|
||||
|
||||
private const val MAGISK_FILES = "topjohnwu/magisk_files"
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
|
||||
class AppRepository(private val policyDao: PolicyDao) {
|
||||
|
||||
fun deleteOutdated() = policyDao.deleteOutdated()
|
||||
fun delete(packageName: String) = policyDao.delete(packageName)
|
||||
fun delete(uid: Int) = policyDao.delete(uid)
|
||||
fun fetch(uid: Int) = policyDao.fetch(uid)
|
||||
fun fetchAll() = policyDao.fetchAll()
|
||||
fun update(policy: MagiskPolicy) = policyDao.update(policy)
|
||||
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface DBConfig {
|
||||
val settingsDao: SettingsDao
|
||||
val stringDao: StringDao
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Int
|
||||
) = DBSettingsValue(name, default)
|
||||
|
||||
fun dbSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) = DBBoolSettings(name, default)
|
||||
|
||||
fun dbStrings(
|
||||
name: String,
|
||||
default: String
|
||||
) = DBStringsValue(name, default)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
||||
private var value: Int? = null
|
||||
|
||||
private fun getKey(property: KProperty<*>) = name.trimEmptyToNull() ?: property.name
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
||||
if (value == null)
|
||||
value = thisRef.settingsDao.fetch(getKey(property), default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
thisRef.settingsDao.put(getKey(property), value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
class DBBoolSettings(
|
||||
name: String,
|
||||
default: Boolean
|
||||
) : ReadWriteProperty<DBConfig, Boolean> {
|
||||
|
||||
val base = DBSettingsValue(name, if (default) 1 else 0)
|
||||
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean
|
||||
= base.getValue(thisRef, property) != 0
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
|
||||
base.setValue(thisRef, property, if (value) 1 else 0)
|
||||
}
|
||||
|
||||
class DBStringsValue(
|
||||
private val name: String,
|
||||
private val default: String
|
||||
) : ReadWriteProperty<DBConfig, String> {
|
||||
|
||||
private var value: String? = null
|
||||
|
||||
private fun getKey(property: KProperty<*>) = name.trimEmptyToNull() ?: property.name
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
||||
if (value == null)
|
||||
value = thisRef.stringDao.fetch(getKey(property), default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
thisRef.stringDao.put(getKey(property), value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.LogDao
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class LogRepository(
|
||||
private val logDao: LogDao
|
||||
) {
|
||||
|
||||
fun fetchLogs() = logDao.fetchAll()
|
||||
.map { it.sortByDescending { it.date.time }; it }
|
||||
.map { it.wrap() }
|
||||
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
fun clearLogs() = logDao.deleteAll()
|
||||
fun clearOutdated() = logDao.deleteOutdated()
|
||||
|
||||
fun clearMagiskLogs() = Shell.su("echo -n > " + Const.MAGISK_LOG)
|
||||
.toSingle()
|
||||
.map { it.exec() }
|
||||
|
||||
fun put(log: MagiskLog) = logDao.put(log)
|
||||
|
||||
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
|
||||
val day = TimeUnit.DAYS.toMillis(1)
|
||||
return groupBy { it.date.time / day }
|
||||
.map { WrappedMagiskLog(it.key * day, it.value) }
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.magisk.utils.writeToFile
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
|
||||
class MagiskRepository(
|
||||
private val context: Context,
|
||||
private val apiRaw: GithubRawApiServices,
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun fetchMagisk() = fetchUpdate()
|
||||
.flatMap { apiRaw.fetchFile(it.magisk.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
|
||||
|
||||
fun fetchManager() = fetchUpdate()
|
||||
.flatMap { apiRaw.fetchFile(it.app.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_APK) }
|
||||
|
||||
fun fetchUninstaller() = fetchUpdate()
|
||||
.flatMap { apiRaw.fetchFile(it.uninstaller.link) }
|
||||
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) }
|
||||
|
||||
fun fetchSafetynet() = apiRaw.fetchSafetynet()
|
||||
|
||||
fun fetchBootctl() = apiRaw
|
||||
.fetchBootctl()
|
||||
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
|
||||
|
||||
|
||||
fun fetchUpdate() = when (Config.updateChannel) {
|
||||
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
|
||||
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
|
||||
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
|
||||
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
|
||||
else -> throw IllegalArgumentException()
|
||||
}.flatMap {
|
||||
// If remote version is lower than current installed, try switching to beta
|
||||
if (it.magisk.versionCode < Info.magiskVersionCode
|
||||
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
|
||||
Config.updateChannel = Config.Value.BETA_CHANNEL
|
||||
apiRaw.fetchBetaUpdate()
|
||||
} else {
|
||||
Single.just(it)
|
||||
}
|
||||
}.map { Info.remote = it; it }
|
||||
|
||||
fun fetchApps() =
|
||||
Single.fromCallable { packageManager.getInstalledApplications(0) }
|
||||
.flattenAsFlowable { it }
|
||||
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
||||
.map {
|
||||
val label = Utils.getAppLabel(it, packageManager)
|
||||
val icon = it.loadIcon(packageManager)
|
||||
HideAppInfo(it, label, icon)
|
||||
}
|
||||
.filter { it.processes.isNotEmpty() }
|
||||
.toList()
|
||||
|
||||
fun fetchHideTargets() = Shell.su("magiskhide --ls").toSingle()
|
||||
.map { it.exec().out }
|
||||
.flattenAsFlowable { it }
|
||||
.map { HideTarget(it) }
|
||||
.toList()
|
||||
|
||||
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
|
||||
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
|
||||
|
||||
private val Boolean.state get() = if (this) "add" else "rm"
|
||||
|
||||
companion object {
|
||||
const val FILE_MAGISK_ZIP = "magisk.zip"
|
||||
const val FILE_MAGISK_APK = "magisk.apk"
|
||||
const val FILE_UNINSTALLER_ZIP = "uninstaller.zip"
|
||||
const val FILE_SAFETY_NET_APK = "safetynet.apk"
|
||||
const val FILE_BOOTCTL_SH = "bootctl"
|
||||
|
||||
private val blacklist = listOf(
|
||||
let { val app: App by inject(); app }.packageName,
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.skoumal.teanity.rxbus.RxBus
|
||||
import com.topjohnwu.magisk.App
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val applicationModule = module {
|
||||
single { RxBus() }
|
||||
factory { get<Context>().resources }
|
||||
factory { get<Context>() as App }
|
||||
factory { get<Context>().packageManager }
|
||||
factory(Protected) { get<App>().protectedContext }
|
||||
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
||||
}
|
15
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
15
app/src/main/java/com/topjohnwu/magisk/di/DatabaseModule.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.data.database.*
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val databaseModule = module {
|
||||
single { LogDao() }
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
single { RepoDatabaseHelper(get()) }
|
||||
single { UpdateRepos(get()) }
|
||||
}
|
10
app/src/main/java/com/topjohnwu/magisk/di/MiscModule.kt
Normal file
10
app/src/main/java/com/topjohnwu/magisk/di/MiscModule.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val miscModule = module {
|
||||
|
||||
// define miscs here
|
||||
|
||||
}
|
10
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
10
app/src/main/java/com/topjohnwu/magisk/di/Modules.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
val koinModules = listOf(
|
||||
applicationModule,
|
||||
networkingModule,
|
||||
databaseModule,
|
||||
repositoryModule,
|
||||
viewModelModules,
|
||||
miscModule
|
||||
)
|
@@ -0,0 +1,6 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
@@ -0,0 +1,73 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
||||
|
||||
val networkingModule = module {
|
||||
single { createOkHttpClient() }
|
||||
single { createConverterFactory() }
|
||||
single { createCallAdapterFactory() }
|
||||
single { createRetrofit(get(), get(), get()) }
|
||||
single { createApiService<GithubRawApiServices>(get(), Const.Url.GITHUB_RAW_API_URL) }
|
||||
}
|
||||
|
||||
fun createOkHttpClient(): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun createConverterFactory(): Converter.Factory {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(JsonAdapterFactory.INSTANCE)
|
||||
.build()
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
fun createCallAdapterFactory(): CallAdapter.Factory {
|
||||
return RxJava2CallAdapterFactory.create()
|
||||
}
|
||||
|
||||
fun createRetrofit(
|
||||
okHttpClient: OkHttpClient,
|
||||
converterFactory: Converter.Factory,
|
||||
callAdapterFactory: CallAdapter.Factory
|
||||
): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(converterFactory)
|
||||
.addCallAdapterFactory(callAdapterFactory)
|
||||
.client(okHttpClient)
|
||||
}
|
||||
|
||||
@KotshiJsonAdapterFactory
|
||||
abstract class JsonAdapterFactory : JsonAdapter.Factory {
|
||||
companion object {
|
||||
val INSTANCE: JsonAdapterFactory = KotshiJsonAdapterFactory
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
|
||||
return retrofitBuilder
|
||||
.baseUrl(baseUrl)
|
||||
.build()
|
||||
.create(T::class.java)
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.data.repository.AppRepository
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val repositoryModule = module {
|
||||
single { MagiskRepository(get(), get(), get()) }
|
||||
single { LogRepository(get()) }
|
||||
single { AppRepository(get()) }
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.ui.MainViewModel
|
||||
import com.topjohnwu.magisk.ui.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.ui.hide.HideViewModel
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val viewModelModules = module {
|
||||
viewModel { MainViewModel() }
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
|
||||
viewModel { HideViewModel(get(), get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { LogViewModel(get(), get()) }
|
||||
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) }
|
||||
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||
}
|
@@ -1,427 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.collection.ArraySet;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.buildware.widget.indeterm.IndeterminateCheckBox;
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.magisk.view.ArrowExpandable;
|
||||
import com.topjohnwu.magisk.view.Expandable;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import butterknife.BindView;
|
||||
import java9.util.Comparators;
|
||||
import java9.util.Lists;
|
||||
import java9.util.Objects;
|
||||
import java9.util.Sets;
|
||||
import java9.util.stream.Collectors;
|
||||
import java9.util.stream.Stream;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
public class ApplicationAdapter extends SectionedAdapter
|
||||
<ApplicationAdapter.AppViewHolder, ApplicationAdapter.ProcessViewHolder> {
|
||||
|
||||
private static final String SAFETYNET_PROCESS = "com.google.android.gms.unstable";
|
||||
private static final String GMS_PACKAGE = "com.google.android.gms";
|
||||
private static boolean old_hide = false;
|
||||
|
||||
/* A list of apps that should not be shown as hide-able */
|
||||
private static final List<String> HIDE_BLACKLIST = Lists.of(
|
||||
App.self.getPackageName(),
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
);
|
||||
private static final List<String> DEFAULT_HIDELIST = Lists.of(
|
||||
SAFETYNET_PROCESS
|
||||
);
|
||||
|
||||
private static int BOTTOM_MARGIN = -1;
|
||||
|
||||
private List<HideAppInfo> fullList, showList;
|
||||
private List<HideTarget> hideList;
|
||||
private PackageManager pm;
|
||||
private boolean showSystem;
|
||||
|
||||
public ApplicationAdapter(Context context) {
|
||||
fullList = showList = Collections.emptyList();
|
||||
hideList = Collections.emptyList();
|
||||
pm = context.getPackageManager();
|
||||
showSystem = Config.get(Config.Key.SHOW_SYSTEM_APP);
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
|
||||
}
|
||||
|
||||
private static ViewGroup.MarginLayoutParams getMargins(RecyclerView.ViewHolder vh) {
|
||||
return (ViewGroup.MarginLayoutParams) vh.itemView.getLayoutParams();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return showList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(int section) {
|
||||
HideAppInfo app = showList.get(section);
|
||||
return app.expanded ? app.processList.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppViewHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_app, parent, false);
|
||||
AppViewHolder vh = new AppViewHolder(v);
|
||||
if (BOTTOM_MARGIN < 0)
|
||||
BOTTOM_MARGIN = getMargins(vh).bottomMargin;
|
||||
return vh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProcessViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_hide_process, parent, false);
|
||||
return new ProcessViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindSectionViewHolder(AppViewHolder holder, int section) {
|
||||
HideAppInfo app = showList.get(section);
|
||||
holder.app_name.setText(app.name);
|
||||
holder.app_icon.setImageDrawable(app.info.loadIcon(pm));
|
||||
holder.package_name.setText(app.info.packageName);
|
||||
holder.checkBox.setOnStateChangedListener(null);
|
||||
holder.checkBox.setState(app.getState());
|
||||
holder.ex.setExpanded(app.expanded);
|
||||
|
||||
int index = getItemPosition(section, 0);
|
||||
holder.checkBox.setOnStateChangedListener((IndeterminateCheckBox box, @Nullable Boolean status) -> {
|
||||
if (status != null) {
|
||||
setHide(status, app);
|
||||
if (app.expanded)
|
||||
notifyItemRangeChanged(index, app.processList.size());
|
||||
}
|
||||
});
|
||||
|
||||
if (app.processList.size() > 1) {
|
||||
holder.arrow.setVisibility(View.VISIBLE);
|
||||
|
||||
holder.trigger.setOnClickListener((v) -> {
|
||||
if (app.expanded) {
|
||||
app.expanded = false;
|
||||
notifyItemRangeRemoved(index, app.processList.size());
|
||||
holder.ex.collapse();
|
||||
} else {
|
||||
app.expanded = true;
|
||||
notifyItemRangeInserted(index, app.processList.size());
|
||||
holder.ex.expand();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
holder.arrow.setVisibility(View.GONE);
|
||||
holder.trigger.setOnClickListener(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ProcessViewHolder holder, int section, int position) {
|
||||
HideAppInfo app = showList.get(section);
|
||||
HideProcessInfo target = app.processList.get(position);
|
||||
holder.process.setText(target.name);
|
||||
holder.checkbox.setOnCheckedChangeListener(null);
|
||||
holder.checkbox.setChecked(target.hidden);
|
||||
holder.checkbox.setOnCheckedChangeListener((v, checked) -> {
|
||||
setHide(checked, app, target);
|
||||
notifyItemChanged(getSectionPosition(section));
|
||||
});
|
||||
getMargins(holder).bottomMargin =
|
||||
position == app.processList.size() - 1 ? BOTTOM_MARGIN : 0;
|
||||
}
|
||||
|
||||
public void filter(String constraint) {
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(() -> {
|
||||
Stream<HideAppInfo> s = StreamSupport.stream(fullList)
|
||||
.filter(this::systemFilter)
|
||||
.filter(t -> nameFilter(t, constraint));
|
||||
UiThreadHandler.run(() -> {
|
||||
showList = s.collect(Collectors.toList());
|
||||
notifyDataSetChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void setShowSystem(boolean b) {
|
||||
showSystem = b;
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(this::loadApps);
|
||||
}
|
||||
|
||||
private void setHide(boolean add, HideAppInfo app) {
|
||||
if (add) {
|
||||
StreamSupport.stream(app.processList).forEach(p -> setHide(true, app, p));
|
||||
} else {
|
||||
if (StreamSupport.stream(app.processList)
|
||||
.anyMatch(p -> p.name.equals(SAFETYNET_PROCESS))) {
|
||||
StreamSupport.stream(app.processList).forEach(p -> setHide(false, app, p));
|
||||
} else {
|
||||
// Quick removal
|
||||
Shell.su("magiskhide --rm " + app.info.packageName).submit();
|
||||
StreamSupport.stream(app.processList).forEach(p -> p.hidden = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setHide(boolean add, HideAppInfo app, HideProcessInfo process) {
|
||||
// Don't remove SafetyNet
|
||||
if (!add && DEFAULT_HIDELIST.contains(process.name))
|
||||
return;
|
||||
Shell.su(Utils.fmt("magiskhide --%s %s %s", add ? "add" : "rm",
|
||||
app.info.packageName, process.name)).submit();
|
||||
process.hidden = add;
|
||||
}
|
||||
|
||||
private void addProcesses(Set<String> set, ComponentInfo[] infos) {
|
||||
if (infos != null)
|
||||
for (ComponentInfo info : infos)
|
||||
set.add(info.processName);
|
||||
}
|
||||
|
||||
private PackageInfo getPackageInfo(String pkg) {
|
||||
// Try super hard to get as much info as possible
|
||||
try {
|
||||
return pm.getPackageInfo(pkg,
|
||||
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES |
|
||||
PackageManager.GET_RECEIVERS | PackageManager.GET_PROVIDERS);
|
||||
} catch (Exception e1) {
|
||||
try {
|
||||
PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_ACTIVITIES);
|
||||
info.services = pm.getPackageInfo(pkg, PackageManager.GET_SERVICES).services;
|
||||
info.receivers = pm.getPackageInfo(pkg, PackageManager.GET_RECEIVERS).receivers;
|
||||
info.providers = pm.getPackageInfo(pkg, PackageManager.GET_PROVIDERS).providers;
|
||||
return info;
|
||||
} catch (Exception e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void loadApps() {
|
||||
hideList = StreamSupport.stream(Shell.su("magiskhide --ls").exec().getOut())
|
||||
.map(HideTarget::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
fullList = StreamSupport.stream(pm.getInstalledApplications(0))
|
||||
.filter(info -> !HIDE_BLACKLIST.contains(info.packageName) && info.enabled)
|
||||
.map(info -> {
|
||||
if (old_hide) {
|
||||
return new HideAppInfo(info, Sets.of(info.packageName));
|
||||
} else {
|
||||
Set<String> set = new ArraySet<>();
|
||||
PackageInfo pkg = getPackageInfo(info.packageName);
|
||||
if (pkg != null) {
|
||||
addProcesses(set, pkg.activities);
|
||||
addProcesses(set, pkg.services);
|
||||
addProcesses(set, pkg.receivers);
|
||||
addProcesses(set, pkg.providers);
|
||||
}
|
||||
if (set.isEmpty())
|
||||
return null;
|
||||
return new HideAppInfo(info, set);
|
||||
}
|
||||
}).filter(Objects::nonNull).sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Event.trigger(false, Event.MAGISK_HIDE_DONE);
|
||||
}
|
||||
|
||||
// True if not system app or user already hidden it
|
||||
private boolean systemFilter(HideAppInfo target) {
|
||||
return showSystem || target.haveHidden() ||
|
||||
(target.info.flags & ApplicationInfo.FLAG_SYSTEM) == 0;
|
||||
}
|
||||
|
||||
private boolean contains(String s, String filter) {
|
||||
return s.toLowerCase().contains(filter);
|
||||
}
|
||||
|
||||
private boolean nameFilter(HideAppInfo target, String filter) {
|
||||
if (filter == null || filter.isEmpty())
|
||||
return true;
|
||||
filter = filter.toLowerCase();
|
||||
if (contains(target.name, filter))
|
||||
return true;
|
||||
for (HideProcessInfo p : target.processList) {
|
||||
if (contains(p.name, filter))
|
||||
return true;
|
||||
}
|
||||
return contains(target.info.packageName, filter);
|
||||
}
|
||||
|
||||
class HideAppInfo implements Comparable<HideAppInfo> {
|
||||
String name;
|
||||
ApplicationInfo info;
|
||||
List<HideProcessInfo> processList;
|
||||
boolean expanded;
|
||||
IndeterminateCheckBox.OnStateChangedListener listener;
|
||||
|
||||
HideAppInfo(ApplicationInfo appInfo, Set<String> set) {
|
||||
info = appInfo;
|
||||
name = Utils.getAppLabel(info, pm);
|
||||
expanded = false;
|
||||
processList = StreamSupport.stream(set)
|
||||
.map(process -> new HideProcessInfo(info.packageName, process))
|
||||
.sorted().collect(Collectors.toList());
|
||||
listener = (IndeterminateCheckBox box, @Nullable Boolean status) -> {
|
||||
if (status != null) {
|
||||
for (HideProcessInfo p : processList) {
|
||||
String cmd = Utils.fmt("magiskhide --%s %s %s",
|
||||
status ? "add" : "rm", info.packageName, p.name);
|
||||
Shell.su(cmd).submit();
|
||||
p.hidden = status;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HideAppInfo o) {
|
||||
Comparator<HideAppInfo> c;
|
||||
c = Comparators.comparing(HideAppInfo::haveHidden);
|
||||
c = Comparators.reversed(c);
|
||||
c = Comparators.thenComparing(c, t -> t.name, String::compareToIgnoreCase);
|
||||
c = Comparators.thenComparing(c, t -> t.info.packageName);
|
||||
return c.compare(this, o);
|
||||
}
|
||||
|
||||
Boolean getState() {
|
||||
boolean all = true;
|
||||
boolean hidden = false;
|
||||
for (HideProcessInfo p : processList) {
|
||||
if (!p.hidden)
|
||||
all = false;
|
||||
else
|
||||
hidden = true;
|
||||
}
|
||||
if (all)
|
||||
return true;
|
||||
return hidden ? null : false;
|
||||
}
|
||||
|
||||
boolean haveHidden() {
|
||||
Boolean c = getState();
|
||||
return c == null ? true : c;
|
||||
}
|
||||
}
|
||||
|
||||
class HideProcessInfo implements Comparable<HideProcessInfo> {
|
||||
String name;
|
||||
boolean hidden;
|
||||
|
||||
HideProcessInfo(String pkg, String process) {
|
||||
this.name = process;
|
||||
for (HideTarget t : hideList) {
|
||||
if (t.pkg.equals(pkg) && t.process.equals(process)) {
|
||||
hidden = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(HideProcessInfo o) {
|
||||
Comparator<HideProcessInfo> c;
|
||||
c = Comparators.comparing((HideProcessInfo t) -> t.hidden);
|
||||
c = Comparators.reversed(c);
|
||||
c = Comparators.thenComparing(c, t -> t.name);
|
||||
return c.compare(this, o);
|
||||
}
|
||||
}
|
||||
|
||||
class HideTarget {
|
||||
String pkg;
|
||||
String process;
|
||||
|
||||
HideTarget(String line) {
|
||||
String[] split = line.split("\\|", 2);
|
||||
pkg = split[0];
|
||||
if (split.length == 2) {
|
||||
process = split[1];
|
||||
} else {
|
||||
// Backwards compatibility
|
||||
old_hide = true;
|
||||
process = pkg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.app_icon) ImageView app_icon;
|
||||
@BindView(R.id.app_name) TextView app_name;
|
||||
@BindView(R.id.package_name) TextView package_name;
|
||||
@BindView(R.id.checkbox) IndeterminateCheckBox checkBox;
|
||||
@BindView(R.id.trigger) View trigger;
|
||||
@BindView(R.id.arrow) ImageView arrow;
|
||||
|
||||
Expandable ex;
|
||||
|
||||
AppViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
new ApplicationAdapter$AppViewHolder_ViewBinding(this, itemView);
|
||||
ex = new ArrowExpandable(new Expandable() {
|
||||
@Override
|
||||
protected void onExpand() {
|
||||
getMargins(AppViewHolder.this).bottomMargin = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCollapse() {
|
||||
getMargins(AppViewHolder.this).bottomMargin = BOTTOM_MARGIN;
|
||||
}
|
||||
}, arrow);
|
||||
}
|
||||
}
|
||||
|
||||
class ProcessViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.process) TextView process;
|
||||
@BindView(R.id.checkbox) CheckBox checkbox;
|
||||
|
||||
ProcessViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
new ApplicationAdapter$ProcessViewHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,127 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.model.entity.Module;
|
||||
import com.topjohnwu.magisk.view.SnackbarMaker;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
|
||||
|
||||
private final List<Module> mList;
|
||||
|
||||
public ModulesAdapter(List<Module> list) {
|
||||
mList = list;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
||||
Context context = holder.itemView.getContext();
|
||||
final Module module = mList.get(position);
|
||||
|
||||
String version = module.getVersion();
|
||||
String author = module.getAuthor();
|
||||
String description = module.getDescription();
|
||||
String noInfo = context.getString(R.string.no_info_provided);
|
||||
|
||||
holder.title.setText(module.getName());
|
||||
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
|
||||
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
||||
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
|
||||
|
||||
holder.checkBox.setOnCheckedChangeListener(null);
|
||||
holder.checkBox.setChecked(module.isEnabled());
|
||||
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
|
||||
int snack;
|
||||
if (isChecked) {
|
||||
module.removeDisableFile();
|
||||
snack = R.string.disable_file_removed;
|
||||
} else {
|
||||
module.createDisableFile();
|
||||
snack = R.string.disable_file_created;
|
||||
}
|
||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
holder.delete.setOnClickListener(v -> {
|
||||
boolean removed = module.willBeRemoved();
|
||||
int snack;
|
||||
if (removed) {
|
||||
module.deleteRemoveFile();
|
||||
snack = R.string.remove_file_deleted;
|
||||
} else {
|
||||
module.createRemoveFile();
|
||||
snack = R.string.remove_file_created;
|
||||
}
|
||||
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
||||
updateDeleteButton(holder, module);
|
||||
});
|
||||
|
||||
if (module.isUpdated()) {
|
||||
holder.notice.setVisibility(View.VISIBLE);
|
||||
holder.notice.setText(R.string.update_file_created);
|
||||
holder.delete.setEnabled(false);
|
||||
} else {
|
||||
updateDeleteButton(holder, module);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDeleteButton(ViewHolder holder, Module module) {
|
||||
holder.notice.setVisibility(module.willBeRemoved() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (module.willBeRemoved()) {
|
||||
holder.delete.setImageResource(R.drawable.ic_undelete);
|
||||
} else {
|
||||
holder.delete.setImageResource(R.drawable.ic_delete);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mList.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.title) TextView title;
|
||||
@BindView(R.id.version_name) TextView versionName;
|
||||
@BindView(R.id.description) TextView description;
|
||||
@BindView(R.id.notice) TextView notice;
|
||||
@BindView(R.id.checkbox) CheckBox checkBox;
|
||||
@BindView(R.id.author) TextView author;
|
||||
@BindView(R.id.delete) ImageView delete;
|
||||
|
||||
ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
new ModulesAdapter$ViewHolder_ViewBinding(this, itemView);
|
||||
|
||||
if (!Shell.rootAccess()) {
|
||||
checkBox.setEnabled(false);
|
||||
delete.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,172 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB;
|
||||
import com.topjohnwu.magisk.model.entity.Policy;
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
||||
import com.topjohnwu.magisk.view.ArrowExpandable;
|
||||
import com.topjohnwu.magisk.view.Expandable;
|
||||
import com.topjohnwu.magisk.view.ExpandableViewHolder;
|
||||
import com.topjohnwu.magisk.view.SnackbarMaker;
|
||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
|
||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
|
||||
|
||||
private List<Policy> policyList;
|
||||
private MagiskDB dbHelper;
|
||||
private PackageManager pm;
|
||||
private boolean[] expandList;
|
||||
|
||||
public PolicyAdapter(List<Policy> list, MagiskDB db, PackageManager pm) {
|
||||
policyList = list;
|
||||
expandList = new boolean[policyList.size()];
|
||||
dbHelper = db;
|
||||
this.pm = pm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false);
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
Policy policy = policyList.get(position);
|
||||
|
||||
holder.settings.setExpanded(expandList[position]);
|
||||
holder.trigger.setOnClickListener(view -> {
|
||||
if (holder.settings.isExpanded()) {
|
||||
holder.settings.collapse();
|
||||
expandList[position] = false;
|
||||
} else {
|
||||
holder.settings.expand();
|
||||
expandList[position] = true;
|
||||
}
|
||||
});
|
||||
|
||||
holder.appName.setText(policy.appName);
|
||||
holder.packageName.setText(policy.packageName);
|
||||
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
||||
|
||||
holder.notificationSwitch.setOnCheckedChangeListener(null);
|
||||
holder.loggingSwitch.setOnCheckedChangeListener(null);
|
||||
|
||||
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
|
||||
holder.notificationSwitch.setChecked(policy.notification);
|
||||
holder.loggingSwitch.setChecked(policy.logging);
|
||||
|
||||
holder.masterSwitch.setOnClickListener(v -> {
|
||||
boolean isChecked = holder.masterSwitch.isChecked();
|
||||
Runnable r = () -> {
|
||||
if ((isChecked && policy.policy == Policy.DENY) ||
|
||||
(!isChecked && policy.policy == Policy.ALLOW)) {
|
||||
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
|
||||
String message = v.getContext().getString(
|
||||
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
|
||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.updatePolicy(policy);
|
||||
}
|
||||
};
|
||||
if (FingerprintHelper.useFingerprint()) {
|
||||
holder.masterSwitch.setChecked(!isChecked);
|
||||
new FingerprintAuthDialog((Activity) v.getContext(), () -> {
|
||||
holder.masterSwitch.setChecked(isChecked);
|
||||
r.run();
|
||||
}).show();
|
||||
} else {
|
||||
r.run();
|
||||
}
|
||||
});
|
||||
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
||||
if ((isChecked && !policy.notification) ||
|
||||
(!isChecked && policy.notification)) {
|
||||
policy.notification = isChecked;
|
||||
String message = v.getContext().getString(
|
||||
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
|
||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.updatePolicy(policy);
|
||||
}
|
||||
});
|
||||
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
||||
if ((isChecked && !policy.logging) ||
|
||||
(!isChecked && policy.logging)) {
|
||||
policy.logging = isChecked;
|
||||
String message = v.getContext().getString(
|
||||
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
|
||||
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.updatePolicy(policy);
|
||||
}
|
||||
});
|
||||
holder.delete.setOnClickListener(v -> {
|
||||
DialogInterface.OnClickListener l = (dialog, which) -> {
|
||||
policyList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
notifyItemRangeChanged(position, policyList.size());
|
||||
SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName),
|
||||
Snackbar.LENGTH_SHORT).show();
|
||||
dbHelper.deletePolicy(policy);
|
||||
};
|
||||
if (FingerprintHelper.useFingerprint()) {
|
||||
new FingerprintAuthDialog((Activity) v.getContext(),
|
||||
() -> l.onClick(null, 0)).show();
|
||||
} else {
|
||||
new CustomAlertDialog((Activity) v.getContext())
|
||||
.setTitle(R.string.su_revoke_title)
|
||||
.setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName))
|
||||
.setPositiveButton(R.string.yes, l)
|
||||
.setNegativeButton(R.string.no_thanks, null)
|
||||
.setCancelable(true)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return policyList.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.app_name) TextView appName;
|
||||
@BindView(R.id.package_name) TextView packageName;
|
||||
@BindView(R.id.app_icon) ImageView appIcon;
|
||||
@BindView(R.id.master_switch) SwitchCompat masterSwitch;
|
||||
@BindView(R.id.notification_switch) SwitchCompat notificationSwitch;
|
||||
@BindView(R.id.logging_switch) SwitchCompat loggingSwitch;
|
||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
||||
@BindView(R.id.arrow) ImageView arrow;
|
||||
@BindView(R.id.trigger) View trigger;
|
||||
@BindView(R.id.delete) ImageView delete;
|
||||
@BindView(R.id.more_info) ImageView moreInfo;
|
||||
|
||||
Expandable settings;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
new PolicyAdapter$ViewHolder_ViewBinding(this, itemView);
|
||||
settings = new ArrowExpandable(new ExpandableViewHolder(expandLayout), arrow);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,246 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SearchView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
import com.topjohnwu.magisk.model.entity.Module;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
import com.topjohnwu.magisk.view.MarkDownWindow;
|
||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import butterknife.BindView;
|
||||
import java9.util.stream.StreamSupport;
|
||||
|
||||
public class ReposAdapter
|
||||
extends SectionedAdapter<ReposAdapter.SectionHolder, ReposAdapter.RepoHolder>
|
||||
implements Event.AutoListener, SearchView.OnQueryTextListener {
|
||||
|
||||
private static final int UPDATES = 0;
|
||||
private static final int INSTALLED = 1;
|
||||
private static final int OTHERS = 2;
|
||||
|
||||
private Map<String, Module> moduleMap;
|
||||
private RepoDatabaseHelper repoDB;
|
||||
private List<Pair<Integer, List<Repo>>> repoPairs;
|
||||
private List<Repo> fullList;
|
||||
private SearchView mSearch;
|
||||
|
||||
public ReposAdapter() {
|
||||
repoDB = App.self.repoDB;
|
||||
moduleMap = Collections.emptyMap();
|
||||
fullList = Collections.emptyList();
|
||||
repoPairs = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return repoPairs.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(int section) {
|
||||
return repoPairs.get(section).second.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.section, parent, false);
|
||||
return new SectionHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepoHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
|
||||
return new RepoHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindSectionViewHolder(SectionHolder holder, int section) {
|
||||
switch (repoPairs.get(section).first) {
|
||||
case UPDATES:
|
||||
holder.sectionText.setText(R.string.update_available);
|
||||
break;
|
||||
case INSTALLED:
|
||||
holder.sectionText.setText(R.string.installed);
|
||||
break;
|
||||
case OTHERS:
|
||||
holder.sectionText.setText(R.string.not_installed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(RepoHolder holder, int section, int position) {
|
||||
Repo repo = repoPairs.get(section).second.get(position);
|
||||
Context context = holder.itemView.getContext();
|
||||
|
||||
String name = repo.getName();
|
||||
String version = repo.getVersion();
|
||||
String author = repo.getAuthor();
|
||||
String description = repo.getDescription();
|
||||
String noInfo = context.getString(R.string.no_info_provided);
|
||||
|
||||
holder.title.setText(TextUtils.isEmpty(name) ? noInfo : name);
|
||||
holder.versionName.setText(TextUtils.isEmpty(version) ? noInfo : version);
|
||||
holder.author.setText(TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
||||
holder.description.setText(TextUtils.isEmpty(description) ? noInfo : description);
|
||||
holder.updateTime.setText(context.getString(R.string.updated_on, repo.getLastUpdateString()));
|
||||
|
||||
holder.infoLayout.setOnClickListener(v ->
|
||||
MarkDownWindow.show((BaseActivity) context, null, repo.getDetailUrl()));
|
||||
|
||||
holder.downloadImage.setOnClickListener(v -> {
|
||||
new CustomAlertDialog((BaseActivity) context)
|
||||
.setTitle(context.getString(R.string.repo_install_title, repo.getName()))
|
||||
.setMessage(context.getString(R.string.repo_install_msg, repo.getDownloadFilename()))
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(R.string.install, (d, i) ->
|
||||
startDownload((BaseActivity) context, repo, true))
|
||||
.setNeutralButton(R.string.download, (d, i) ->
|
||||
startDownload((BaseActivity) context, repo, false))
|
||||
.setNegativeButton(R.string.no_thanks, null)
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
private void startDownload(BaseActivity activity, Repo repo, Boolean install) {
|
||||
activity.runWithExternalRW(() -> {
|
||||
Intent intent = new Intent(activity, ClassMap.get(DownloadModuleService.class))
|
||||
.putExtra("repo", repo).putExtra("install", install);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity.startForegroundService(intent);
|
||||
} else {
|
||||
activity.startService(intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateLists() {
|
||||
if (mSearch != null)
|
||||
onQueryTextChange(mSearch.getQuery().toString());
|
||||
else
|
||||
onQueryTextChange("");
|
||||
}
|
||||
|
||||
private static boolean noCaseContain(String a, String b) {
|
||||
return a.toLowerCase().contains(b.toLowerCase());
|
||||
}
|
||||
|
||||
public void setSearchView(SearchView view) {
|
||||
mSearch = view;
|
||||
mSearch.setOnQueryTextListener(this);
|
||||
}
|
||||
|
||||
public void notifyDBChanged(boolean refresh) {
|
||||
try (Cursor c = repoDB.getRepoCursor()) {
|
||||
fullList = new ArrayList<>(c.getCount());
|
||||
while (c.moveToNext())
|
||||
fullList.add(new Repo(c));
|
||||
}
|
||||
if (refresh)
|
||||
updateLists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(int event) {
|
||||
moduleMap = Event.getResult(event);
|
||||
updateLists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getListeningEvents() {
|
||||
return new int[] {Event.MODULE_LOAD_DONE};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String s) {
|
||||
List<Repo> updates = new ArrayList<>();
|
||||
List<Repo> installed = new ArrayList<>();
|
||||
List<Repo> others = new ArrayList<>();
|
||||
|
||||
StreamSupport.stream(fullList)
|
||||
.filter(repo -> noCaseContain(repo.getName(), s)
|
||||
|| noCaseContain(repo.getAuthor(), s)
|
||||
|| noCaseContain(repo.getDescription(), s))
|
||||
.forEach(repo -> {
|
||||
Module module = moduleMap.get(repo.getId());
|
||||
if (module != null) {
|
||||
if (repo.getVersionCode() > module.getVersionCode()) {
|
||||
// Updates
|
||||
updates.add(repo);
|
||||
} else {
|
||||
installed.add(repo);
|
||||
}
|
||||
} else {
|
||||
others.add(repo);
|
||||
}
|
||||
});
|
||||
|
||||
repoPairs.clear();
|
||||
if (!updates.isEmpty())
|
||||
repoPairs.add(new Pair<>(UPDATES, updates));
|
||||
if (!installed.isEmpty())
|
||||
repoPairs.add(new Pair<>(INSTALLED, installed));
|
||||
if (!others.isEmpty())
|
||||
repoPairs.add(new Pair<>(OTHERS, others));
|
||||
|
||||
notifyDataSetChanged();
|
||||
return false;
|
||||
}
|
||||
|
||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.section_text) TextView sectionText;
|
||||
|
||||
SectionHolder(View itemView) {
|
||||
super(itemView);
|
||||
new ReposAdapter$SectionHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
}
|
||||
|
||||
static class RepoHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.title) TextView title;
|
||||
@BindView(R.id.version_name) TextView versionName;
|
||||
@BindView(R.id.description) TextView description;
|
||||
@BindView(R.id.author) TextView author;
|
||||
@BindView(R.id.info_layout) View infoLayout;
|
||||
@BindView(R.id.download) ImageView downloadImage;
|
||||
@BindView(R.id.update_time) TextView updateTime;
|
||||
|
||||
RepoHolder(View itemView) {
|
||||
super(itemView);
|
||||
new ReposAdapter$RepoHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,96 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public abstract class SectionedAdapter<S extends RecyclerView.ViewHolder, C extends RecyclerView.ViewHolder>
|
||||
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
private static final int SECTION_TYPE = Integer.MIN_VALUE;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
final public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
if (viewType == SECTION_TYPE)
|
||||
return onCreateSectionViewHolder(parent);
|
||||
return onCreateItemViewHolder(parent, viewType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
final public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
PositionInfo info = getPositionInfo(position);
|
||||
if (info.position == -1)
|
||||
onBindSectionViewHolder((S) holder, info.section);
|
||||
else
|
||||
onBindItemViewHolder((C) holder, info.section, info.position);
|
||||
}
|
||||
|
||||
@Override
|
||||
final public int getItemCount() {
|
||||
int size, sec;
|
||||
size = sec = getSectionCount();
|
||||
for (int i = 0; i < sec; ++i){
|
||||
size += getItemCount(i);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
final public int getItemViewType(int position) {
|
||||
PositionInfo info = getPositionInfo(position);
|
||||
if (info.position == -1)
|
||||
return SECTION_TYPE;
|
||||
else
|
||||
return getItemViewType(info.section, info.position);
|
||||
}
|
||||
|
||||
public int getItemViewType(int section, int position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected int getSectionPosition(int section) {
|
||||
return getItemPosition(section, -1);
|
||||
}
|
||||
|
||||
protected int getItemPosition(int section, int position) {
|
||||
int realPosition = 0;
|
||||
// Previous sections
|
||||
for (int i = 0; i < section; ++i) {
|
||||
realPosition += getItemCount(i) + 1;
|
||||
}
|
||||
// Current section
|
||||
realPosition += position + 1;
|
||||
return realPosition;
|
||||
}
|
||||
|
||||
private PositionInfo getPositionInfo(int position) {
|
||||
int section = 0;
|
||||
while (true) {
|
||||
if (position == 0)
|
||||
return new PositionInfo(section, -1);
|
||||
position -= 1;
|
||||
if (position < getItemCount(section))
|
||||
return new PositionInfo(section, position);
|
||||
position -= getItemCount(section++);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PositionInfo {
|
||||
int section;
|
||||
int position;
|
||||
PositionInfo(int section, int position) {
|
||||
this.section = section;
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract int getSectionCount();
|
||||
public abstract int getItemCount(int section);
|
||||
public abstract S onCreateSectionViewHolder(ViewGroup parent);
|
||||
public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType);
|
||||
public abstract void onBindSectionViewHolder(S holder, int section);
|
||||
public abstract void onBindItemViewHolder(C holder, int section, int position);
|
||||
}
|
@@ -1,103 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class StringListAdapter<VH extends StringListAdapter.ViewHolder>
|
||||
extends RecyclerView.Adapter<VH> {
|
||||
|
||||
private RecyclerView rv;
|
||||
private boolean dynamic;
|
||||
private int screenWidth;
|
||||
private int txtWidth = -1;
|
||||
private int padding;
|
||||
|
||||
protected List<String> mList;
|
||||
|
||||
public StringListAdapter(List<String> list) {
|
||||
this(list, false);
|
||||
}
|
||||
|
||||
public StringListAdapter(List<String> list, boolean isDynamic) {
|
||||
mList = list;
|
||||
dynamic = isDynamic;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public final VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(itemLayoutRes(), parent, false);
|
||||
VH vh = createViewHolder(v);
|
||||
if (txtWidth < 0)
|
||||
onUpdateTextWidth(vh);
|
||||
return vh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull VH holder, int position) {
|
||||
holder.txt.setText(mList.get(position));
|
||||
holder.txt.getLayoutParams().width = txtWidth;
|
||||
if (dynamic)
|
||||
onUpdateTextWidth(holder);
|
||||
}
|
||||
|
||||
protected void onUpdateTextWidth(VH vh) {
|
||||
if (txtWidth < 0) {
|
||||
txtWidth = screenWidth - padding;
|
||||
} else {
|
||||
vh.txt.measure(0, 0);
|
||||
int width = vh.txt.getMeasuredWidth();
|
||||
if (width > txtWidth) {
|
||||
txtWidth = width;
|
||||
vh.txt.getLayoutParams().width = txtWidth;
|
||||
}
|
||||
}
|
||||
if (rv.getWidth() != txtWidth + padding)
|
||||
rv.requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView rv) {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
((Activity) rv.getContext()).getWindowManager()
|
||||
.getDefaultDisplay().getMetrics(displayMetrics);
|
||||
screenWidth = displayMetrics.widthPixels;
|
||||
padding = rv.getPaddingStart() + rv.getPaddingEnd();
|
||||
this.rv = rv;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int getItemCount() {
|
||||
return mList.size();
|
||||
}
|
||||
|
||||
@LayoutRes
|
||||
protected abstract int itemLayoutRes();
|
||||
|
||||
@NonNull
|
||||
public abstract VH createViewHolder(@NonNull View v);
|
||||
|
||||
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public TextView txt;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
txt = itemView.findViewById(textViewResId());
|
||||
}
|
||||
|
||||
@IdRes
|
||||
protected abstract int textViewResId();
|
||||
}
|
||||
}
|
@@ -1,144 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.RotateAnimation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB;
|
||||
import com.topjohnwu.magisk.model.entity.SuLogEntry;
|
||||
import com.topjohnwu.magisk.view.ExpandableViewHolder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
|
||||
|
||||
private List<List<SuLogEntry>> logEntries;
|
||||
private Set<Integer> itemExpanded, sectionExpanded;
|
||||
private MagiskDB suDB;
|
||||
|
||||
public SuLogAdapter(MagiskDB db) {
|
||||
suDB = db;
|
||||
logEntries = Collections.emptyList();
|
||||
sectionExpanded = new HashSet<>();
|
||||
itemExpanded = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return logEntries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(int section) {
|
||||
return sectionExpanded.contains(section) ? logEntries.get(section).size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
|
||||
return new SectionHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
|
||||
return new LogViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindSectionViewHolder(SectionHolder holder, int section) {
|
||||
SuLogEntry entry = logEntries.get(section).get(0);
|
||||
holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0);
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
RotateAnimation rotate;
|
||||
if (sectionExpanded.contains(section)) {
|
||||
holder.arrow.setRotation(0);
|
||||
rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
sectionExpanded.remove(section);
|
||||
notifyItemRangeRemoved(getItemPosition(section, 0), logEntries.get(section).size());
|
||||
} else {
|
||||
rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
sectionExpanded.add(section);
|
||||
notifyItemRangeInserted(getItemPosition(section, 0), logEntries.get(section).size());
|
||||
}
|
||||
rotate.setDuration(300);
|
||||
rotate.setFillAfter(true);
|
||||
holder.arrow.setAnimation(rotate);
|
||||
});
|
||||
holder.date.setText(entry.getDateString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(LogViewHolder holder, int section, int position) {
|
||||
SuLogEntry entry = logEntries.get(section).get(position);
|
||||
int realIdx = getItemPosition(section, position);
|
||||
holder.expandable.setExpanded(itemExpanded.contains(realIdx));
|
||||
holder.itemView.setOnClickListener(view -> {
|
||||
if (holder.expandable.isExpanded()) {
|
||||
holder.expandable.collapse();
|
||||
itemExpanded.remove(realIdx);
|
||||
} else {
|
||||
holder.expandable.expand();
|
||||
itemExpanded.add(realIdx);
|
||||
}
|
||||
});
|
||||
Context context = holder.itemView.getContext();
|
||||
holder.appName.setText(entry.appName);
|
||||
holder.action.setText(entry.action ? R.string.grant : R.string.deny);
|
||||
holder.pid.setText(context.getString(R.string.pid, entry.fromPid));
|
||||
holder.uid.setText(context.getString(R.string.target_uid, entry.toUid));
|
||||
holder.command.setText(context.getString(R.string.command, entry.command));
|
||||
holder.time.setText(entry.getTimeString());
|
||||
}
|
||||
|
||||
public void notifyDBChanged() {
|
||||
logEntries = suDB.getLogs();
|
||||
itemExpanded.clear();
|
||||
sectionExpanded.clear();
|
||||
sectionExpanded.add(0);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class SectionHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.date) TextView date;
|
||||
@BindView(R.id.arrow) ImageView arrow;
|
||||
|
||||
SectionHolder(View itemView) {
|
||||
super(itemView);
|
||||
new SuLogAdapter$SectionHolder_ViewBinding(this, itemView);
|
||||
}
|
||||
}
|
||||
|
||||
static class LogViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
@BindView(R.id.app_name) TextView appName;
|
||||
@BindView(R.id.action) TextView action;
|
||||
@BindView(R.id.time) TextView time;
|
||||
@BindView(R.id.pid) TextView pid;
|
||||
@BindView(R.id.uid) TextView uid;
|
||||
@BindView(R.id.cmd) TextView command;
|
||||
@BindView(R.id.expand_layout) ViewGroup expandLayout;
|
||||
|
||||
ExpandableViewHolder expandable;
|
||||
|
||||
LogViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
new SuLogAdapter$LogViewHolder_ViewBinding(this, itemView);
|
||||
expandable = new ExpandableViewHolder(expandLayout);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.adapters;
|
||||
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TabFragmentAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private List<Fragment> fragmentList;
|
||||
private List<String> titleList;
|
||||
|
||||
public TabFragmentAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
fragmentList = new ArrayList<>();
|
||||
titleList = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return fragmentList.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return fragmentList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return titleList.get(position);
|
||||
}
|
||||
|
||||
public void addTab(Fragment fragment, String title) {
|
||||
fragmentList.add(fragment);
|
||||
titleList.add(title);
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
package com.topjohnwu.magisk.model.binding
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
|
||||
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
|
||||
class BindingAdapter : BindingRecyclerViewAdapter<ComparableRvItem<*>>() {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onBindBinding(
|
||||
binding: ViewDataBinding,
|
||||
variableId: Int,
|
||||
layoutRes: Int,
|
||||
position: Int,
|
||||
item: ComparableRvItem<*>
|
||||
) {
|
||||
super.onBindBinding(binding, variableId, layoutRes, position, item)
|
||||
|
||||
when (item) {
|
||||
is LenientRvItem -> {
|
||||
val recycler = recyclerView ?: return
|
||||
item.onBindingBound(binding)
|
||||
item.onBindingBound(binding, recycler)
|
||||
}
|
||||
else -> item.onBindingBound(binding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
this.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
}
|
@@ -36,9 +36,18 @@ public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
||||
mVersionCode = p.readInt();
|
||||
}
|
||||
|
||||
protected BaseModule(MagiskModule m) {
|
||||
mId = m.getId();
|
||||
mName = m.getName();
|
||||
mVersion = m.getVersion();
|
||||
mAuthor = m.getAuthor();
|
||||
mDescription = m.getDescription();
|
||||
mVersionCode = Integer.parseInt(m.getVersionCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull BaseModule module) {
|
||||
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
||||
return getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,7 +80,9 @@ public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
||||
return values;
|
||||
}
|
||||
|
||||
protected void parseProps(List<String> props) { parseProps(props.toArray(new String[0])); }
|
||||
protected void parseProps(List<String> props) {
|
||||
parseProps(props.toArray(new String[0]));
|
||||
}
|
||||
|
||||
protected void parseProps(String[] props) throws NumberFormatException {
|
||||
for (String line : props) {
|
||||
|
@@ -0,0 +1,16 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.utils.packageInfo
|
||||
import com.topjohnwu.magisk.utils.processes
|
||||
|
||||
class HideAppInfo(
|
||||
val info: ApplicationInfo,
|
||||
val name: String,
|
||||
val icon: Drawable
|
||||
) {
|
||||
|
||||
val processes = info.packageInfo?.processes?.distinct() ?: listOf(info.packageName)
|
||||
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
class HideTarget(line: String) {
|
||||
|
||||
private val split = line.split(Regex("\\|"), 2)
|
||||
|
||||
val packageName = split[0]
|
||||
val process = split.getOrElse(1) { packageName }
|
||||
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.utils.timeFormatTime
|
||||
import com.topjohnwu.magisk.utils.toTime
|
||||
import java.util.*
|
||||
|
||||
data class MagiskLog(
|
||||
val fromUid: Int,
|
||||
val toUid: Int,
|
||||
val fromPid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val command: String,
|
||||
val action: Boolean,
|
||||
val date: Date
|
||||
) {
|
||||
val timeString = date.time.toTime(timeFormatTime)
|
||||
}
|
||||
|
||||
data class WrappedMagiskLog(
|
||||
val time: Long,
|
||||
val items: List<MagiskLog>
|
||||
)
|
||||
|
||||
fun Map<String, String>.toLog(): MagiskLog {
|
||||
return MagiskLog(
|
||||
fromUid = get("from_uid")?.toIntOrNull() ?: -1,
|
||||
toUid = get("to_uid")?.toIntOrNull() ?: -1,
|
||||
fromPid = get("from_pid")?.toIntOrNull() ?: -1,
|
||||
packageName = get("package_name").orEmpty(),
|
||||
appName = get("app_name").orEmpty(),
|
||||
command = get("command").orEmpty(),
|
||||
action = get("action")?.toIntOrNull() != 0,
|
||||
date = get("time")?.toLongOrNull()?.toDate() ?: Date()
|
||||
)
|
||||
}
|
||||
|
||||
fun Long.toDate() = Date(this)
|
||||
|
||||
fun MagiskLog.toMap() = mapOf(
|
||||
"from_uid" to fromUid,
|
||||
"to_uid" to toUid,
|
||||
"from_pid" to fromPid,
|
||||
"package_name" to packageName,
|
||||
"app_name" to appName,
|
||||
"command" to command,
|
||||
"action" to action,
|
||||
"time" to date.time
|
||||
)
|
||||
|
||||
fun MagiskPolicy.toLog(
|
||||
toUid: Int,
|
||||
fromPid: Int,
|
||||
command: String,
|
||||
date: Date
|
||||
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, date)
|
@@ -0,0 +1,86 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import io.reactivex.Single
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.File
|
||||
|
||||
interface MagiskModule : Parcelable {
|
||||
val id: String
|
||||
val name: String
|
||||
val author: String
|
||||
val version: String
|
||||
val versionCode: String
|
||||
val description: String
|
||||
}
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repository(
|
||||
@PrimaryKey @NonNull
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
override val author: String,
|
||||
override val version: String,
|
||||
override val versionCode: String,
|
||||
override val description: String,
|
||||
val lastUpdate: Long
|
||||
) : MagiskModule
|
||||
|
||||
@Parcelize
|
||||
data class Module(
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
override val author: String,
|
||||
override val version: String,
|
||||
override val versionCode: String,
|
||||
override val description: String,
|
||||
val path: String
|
||||
) : MagiskModule
|
||||
|
||||
@AnyThread
|
||||
fun File.toModule(): Single<Module> {
|
||||
val path = "${Const.MAGISK_PATH}/$name"
|
||||
return "dos2unix < $path/module.prop".su()
|
||||
.map { it.first().toModule(path) }
|
||||
}
|
||||
|
||||
fun Map<String, String>.toModule(path: String): Module {
|
||||
return Module(
|
||||
id = get("id").orEmpty(),
|
||||
name = get("name").orEmpty(),
|
||||
author = get("author").orEmpty(),
|
||||
version = get("version").orEmpty(),
|
||||
versionCode = get("versionCode").orEmpty(),
|
||||
description = get("description").orEmpty(),
|
||||
path = path
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun ResponseBody.toRepository(lastUpdate: Long) = string()
|
||||
.split(Regex("\\n"))
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { Pair(it[0], it[1]) }
|
||||
.toMap()
|
||||
.toRepository(lastUpdate)
|
||||
|
||||
@AnyThread
|
||||
fun Map<String, String>.toRepository(lastUpdate: Long) = Repository(
|
||||
id = get("id").orEmpty(),
|
||||
name = get("name").orEmpty(),
|
||||
author = get("author").orEmpty(),
|
||||
version = get("version").orEmpty(),
|
||||
versionCode = get("versionCode").orEmpty(),
|
||||
description = get("description").orEmpty(),
|
||||
lastUpdate = lastUpdate
|
||||
)
|
@@ -0,0 +1,68 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
|
||||
|
||||
|
||||
data class MagiskPolicy(
|
||||
val uid: Int,
|
||||
val packageName: String,
|
||||
val appName: String,
|
||||
val policy: Int = INTERACTIVE,
|
||||
val until: Long = -1L,
|
||||
val logging: Boolean = true,
|
||||
val notification: Boolean = true,
|
||||
val applicationInfo: ApplicationInfo
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val INTERACTIVE = 0
|
||||
const val DENY = 1
|
||||
const val ALLOW = 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun MagiskPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val uid = get("uid")?.toIntOrNull() ?: -1
|
||||
val packageName = get("package_name").orEmpty()
|
||||
val info = pm.getApplicationInfo(packageName, 0)
|
||||
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
|
||||
return MagiskPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
|
||||
until = get("until")?.toLongOrNull() ?: -1L,
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
|
||||
?: throw PackageManager.NameNotFoundException()
|
||||
val info = pm.getApplicationInfo(pkg, 0)
|
||||
return MagiskPolicy(
|
||||
uid = this,
|
||||
packageName = pkg,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
)
|
||||
}
|
@@ -6,16 +6,33 @@ import android.os.Parcelable;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
public class Module extends BaseModule {
|
||||
public class OldModule extends BaseModule {
|
||||
|
||||
private SuFile mRemoveFile, mDisableFile, mUpdateFile;
|
||||
private boolean mEnable, mRemove, mUpdated;
|
||||
public static final Parcelable.Creator<OldModule> CREATOR = new Creator<OldModule>() {
|
||||
/* It won't be used at any place */
|
||||
@Override
|
||||
public OldModule createFromParcel(Parcel source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public Module(String path) {
|
||||
@Override
|
||||
public OldModule[] newArray(int size) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
private final SuFile mRemoveFile;
|
||||
private final SuFile mDisableFile;
|
||||
private final SuFile mUpdateFile;
|
||||
private final boolean mUpdated;
|
||||
private boolean mEnable;
|
||||
private boolean mRemove;
|
||||
|
||||
public OldModule(String path) {
|
||||
|
||||
try {
|
||||
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
mRemoveFile = new SuFile(path, "remove");
|
||||
mDisableFile = new SuFile(path, "disable");
|
||||
@@ -35,19 +52,6 @@ public class Module extends BaseModule {
|
||||
mUpdated = mUpdateFile.exists();
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Module> CREATOR = new Creator<Module>() {
|
||||
/* It won't be used at any place */
|
||||
@Override
|
||||
public Module createFromParcel(Parcel source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Module[] newArray(int size) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
public void createDisableFile() {
|
||||
mEnable = !mDisableFile.createNewFile();
|
||||
}
|
@@ -27,7 +27,7 @@ public class Policy implements Comparable<Policy>{
|
||||
this.uid = uid;
|
||||
packageName = pkgs[0];
|
||||
info = pm.getApplicationInfo(packageName, 0);
|
||||
appName = Utils.getAppLabel(info, pm);
|
||||
appName = Utils.INSTANCE.getAppLabel(info, pm);
|
||||
}
|
||||
|
||||
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||
|
@@ -29,6 +29,11 @@ public class Repo extends BaseModule {
|
||||
mLastUpdate = new Date(p.readLong());
|
||||
}
|
||||
|
||||
public Repo(Repository repo) {
|
||||
super(repo);
|
||||
mLastUpdate = new Date(repo.getLastUpdate());
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
|
||||
|
||||
@Override
|
||||
@@ -49,7 +54,7 @@ public class Repo extends BaseModule {
|
||||
}
|
||||
|
||||
public void update() throws IllegalRepoException {
|
||||
String props[] = Utils.dlString(getPropUrl()).split("\\n");
|
||||
String[] props = Utils.INSTANCE.dlString(getPropUrl()).split("\\n");
|
||||
try {
|
||||
parseProps(props);
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -98,7 +103,7 @@ public class Repo extends BaseModule {
|
||||
}
|
||||
|
||||
public String getDownloadFilename() {
|
||||
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
|
||||
return Utils.INSTANCE.getLegalFilename(getName() + "-" + getVersion() + ".zip");
|
||||
}
|
||||
|
||||
public class IllegalRepoException extends Exception {
|
||||
|
@@ -15,11 +15,11 @@ public class SuLogEntry {
|
||||
public boolean action;
|
||||
public Date date;
|
||||
|
||||
public SuLogEntry(Policy policy) {
|
||||
fromUid = policy.uid;
|
||||
packageName = policy.packageName;
|
||||
appName = policy.appName;
|
||||
action = policy.policy == Policy.ALLOW;
|
||||
public SuLogEntry(MagiskPolicy policy) {
|
||||
fromUid = policy.getUid();
|
||||
packageName = policy.getPackageName();
|
||||
appName = policy.getAppName();
|
||||
action = policy.getPolicy() == Policy.ALLOW;
|
||||
}
|
||||
|
||||
public SuLogEntry(ContentValues values) {
|
||||
@@ -47,10 +47,10 @@ public class SuLogEntry {
|
||||
}
|
||||
|
||||
public String getDateString() {
|
||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
|
||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.getLocale()).format(date);
|
||||
}
|
||||
|
||||
public String getTimeString() {
|
||||
return new SimpleDateFormat("h:mm a", LocaleManager.locale).format(date);
|
||||
return new SimpleDateFormat("h:mm a", LocaleManager.getLocale()).format(date);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,33 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class UpdateInfo(
|
||||
val app: ManagerJson = ManagerJson(),
|
||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||
val magisk: MagiskJson = MagiskJson()
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class UninstallerJson(
|
||||
val link: String = ""
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = "",
|
||||
@Json(name = "md5") val hash: String = ""
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class ManagerJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = ""
|
||||
)
|
@@ -0,0 +1,3 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
data class Version(val version: String, val versionCode: Int)
|
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_console
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
||||
val view = binding.root as TextView
|
||||
view.measure(0, 0)
|
||||
val desiredWidth = view.measuredWidth
|
||||
|
||||
view.updateLayoutParams { width = desiredWidth }
|
||||
|
||||
if (recyclerView.width < desiredWidth) {
|
||||
recyclerView.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
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.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
|
||||
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
|
||||
ComparableRvItem<HideRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_hide_app
|
||||
|
||||
val packageName = item.info.packageName.orEmpty()
|
||||
val items = DiffObservableList(callback).also {
|
||||
val items = item.processes.map {
|
||||
val isHidden = targets.any { target ->
|
||||
packageName == target.packageName && it == target.process
|
||||
}
|
||||
HideProcessRvItem(packageName, it, isHidden)
|
||||
}
|
||||
it.update(items)
|
||||
}
|
||||
val isHiddenState = KObservableField(currentState)
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
private val itemsProcess get() = items.filterIsInstance<HideProcessRvItem>()
|
||||
|
||||
private val currentState
|
||||
get() = when (itemsProcess.count { it.isHidden.value }) {
|
||||
items.size -> IndeterminateState.CHECKED
|
||||
in 1 until items.size -> IndeterminateState.INDETERMINATE
|
||||
else -> IndeterminateState.UNCHECKED
|
||||
}
|
||||
|
||||
init {
|
||||
itemsProcess.forEach {
|
||||
it.isHidden.addOnPropertyChangedCallback { isHiddenState.value = currentState }
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() {
|
||||
val desiredState = when (isHiddenState.value) {
|
||||
IndeterminateState.INDETERMINATE,
|
||||
IndeterminateState.UNCHECKED -> true
|
||||
IndeterminateState.CHECKED -> false
|
||||
}
|
||||
itemsProcess.forEach { it.isHidden.value = desiredState }
|
||||
isHiddenState.value = currentState
|
||||
}
|
||||
|
||||
fun toggleExpansion() {
|
||||
if (items.size <= 1) return
|
||||
isExpanded.toggle()
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: HideRvItem): Boolean = items.all { other.items.contains(it) }
|
||||
override fun itemSameAs(other: HideRvItem): Boolean = item.info == other.item.info
|
||||
|
||||
}
|
||||
|
||||
class HideProcessRvItem(
|
||||
val packageName: String,
|
||||
val process: String,
|
||||
isHidden: Boolean
|
||||
) : ComparableRvItem<HideProcessRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_hide_process
|
||||
|
||||
val isHidden = KObservableField(isHidden)
|
||||
|
||||
private val rxBus: RxBus by inject()
|
||||
|
||||
init {
|
||||
this.isHidden.addOnPropertyChangedCallback {
|
||||
rxBus.post(HideProcessEvent(this@HideProcessRvItem))
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() = isHidden.toggle()
|
||||
|
||||
override fun contentSameAs(other: HideProcessRvItem): Boolean = itemSameAs(other)
|
||||
override fun itemSameAs(other: HideProcessRvItem): Boolean =
|
||||
packageName == other.packageName && process == other.process
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
|
||||
/**
|
||||
* This item addresses issues where enclosing recycler has to be invalidated or generally
|
||||
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
|
||||
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
|
||||
* */
|
||||
abstract class LenientRvItem<in T> : ComparableRvItem<T>() {
|
||||
|
||||
open fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {}
|
||||
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
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.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.timeFormatMedium
|
||||
import com.topjohnwu.magisk.utils.toTime
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
|
||||
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_log
|
||||
|
||||
val items = DiffObservableList(callback)
|
||||
|
||||
fun update(list: List<LogItemRvItem>) {
|
||||
list.firstOrNull()?.isExpanded?.value = true
|
||||
items.update(list)
|
||||
}
|
||||
|
||||
//two of these will never be present, safe to assume it's unique
|
||||
override fun contentSameAs(other: LogRvItem): Boolean = false
|
||||
|
||||
override fun itemSameAs(other: LogRvItem): Boolean = false
|
||||
}
|
||||
|
||||
class LogItemRvItem(
|
||||
item: WrappedMagiskLog
|
||||
) : ComparableRvItem<LogItemRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log
|
||||
|
||||
val date = item.time.toTime(timeFormatMedium)
|
||||
val items: List<ComparableRvItem<*>> = item.items.map { LogItemEntryRvItem(it) }
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemRvItem): Boolean = items
|
||||
.any { !other.items.contains(it) }
|
||||
|
||||
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
|
||||
}
|
||||
|
||||
class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log_entry
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
override fun contentSameAs(other: LogItemEntryRvItem) = item.fromUid == other.item.fromUid &&
|
||||
item.toUid == other.item.toUid &&
|
||||
item.fromPid == other.item.fromPid &&
|
||||
item.packageName == other.item.packageName &&
|
||||
item.command == other.item.command &&
|
||||
item.action == other.item.action &&
|
||||
item.date == other.item.date
|
||||
|
||||
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
|
||||
}
|
||||
|
||||
class MagiskLogRvItem : ComparableRvItem<MagiskLogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_magisk_log
|
||||
|
||||
val items = DiffObservableList(callback)
|
||||
|
||||
fun update(list: List<ConsoleRvItem>) {
|
||||
items.update(list)
|
||||
}
|
||||
|
||||
//two of these will never be present, safe to assume it's unique
|
||||
override fun contentSameAs(other: MagiskLogRvItem): Boolean = false
|
||||
|
||||
override fun itemSameAs(other: MagiskLogRvItem): Boolean = false
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.model.entity.OldModule
|
||||
import com.topjohnwu.magisk.model.entity.Repo
|
||||
import com.topjohnwu.magisk.model.entity.Repository
|
||||
import com.topjohnwu.magisk.utils.get
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
|
||||
class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_module
|
||||
|
||||
val lastActionNotice = KObservableField("")
|
||||
val isChecked = KObservableField(item.isEnabled)
|
||||
val isDeletable = KObservableField(item.willBeRemoved())
|
||||
|
||||
init {
|
||||
isChecked.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> item.removeDisableFile().notice(R.string.disable_file_removed)
|
||||
false -> item.createDisableFile().notice(R.string.disable_file_created)
|
||||
}
|
||||
}
|
||||
isDeletable.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> item.createRemoveFile().notice(R.string.remove_file_created)
|
||||
false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted)
|
||||
}
|
||||
}
|
||||
when {
|
||||
item.isUpdated -> notice(R.string.update_file_created)
|
||||
item.willBeRemoved() -> notice(R.string.remove_file_created)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() = isChecked.toggle()
|
||||
fun toggleDelete() = isDeletable.toggle()
|
||||
|
||||
@Suppress("unused")
|
||||
private fun Any.notice(@StringRes info: Int) {
|
||||
lastActionNotice.value = get<Resources>().getString(info)
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
|
||||
&& item.versionCode == other.item.versionCode
|
||||
&& item.description == other.item.description
|
||||
&& item.name == other.item.name
|
||||
|
||||
override fun itemSameAs(other: ModuleRvItem): Boolean = item.id == other.item.id
|
||||
}
|
||||
|
||||
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
||||
|
||||
constructor(repo: Repository) : this(Repo(repo))
|
||||
|
||||
override val layoutRes: Int = R.layout.item_repo
|
||||
|
||||
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version
|
||||
&& item.lastUpdate == other.item.lastUpdate
|
||||
&& item.versionCode == other.item.versionCode
|
||||
&& item.description == other.item.description
|
||||
&& item.detailUrl == other.item.detailUrl
|
||||
|
||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
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.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.Policy
|
||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
|
||||
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_policy
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
val isEnabled = KObservableField(item.policy == Policy.ALLOW)
|
||||
val shouldNotify = KObservableField(item.notification)
|
||||
val shouldLog = KObservableField(item.logging)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
|
||||
private val rxBus: RxBus by inject()
|
||||
|
||||
private val currentStateItem
|
||||
get() = item.copy(
|
||||
policy = if (isEnabled.value) Policy.ALLOW else Policy.DENY,
|
||||
notification = shouldNotify.value,
|
||||
logging = shouldLog.value
|
||||
)
|
||||
|
||||
init {
|
||||
isEnabled.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyEnableEvent(this@PolicyRvItem, it))
|
||||
}
|
||||
shouldNotify.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem))
|
||||
}
|
||||
shouldLog.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
rxBus.post(PolicyUpdateEvent.Log(currentStateItem))
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: PolicyRvItem): Boolean = itemSameAs(other)
|
||||
override fun itemSameAs(other: PolicyRvItem): Boolean = item.uid == other.item.uid
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_section
|
||||
|
||||
override fun contentSameAs(other: SectionRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: SectionRvItem) = text == other.text
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_spinner
|
||||
|
||||
override fun contentSameAs(other: SpinnerRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: SpinnerRvItem) = item == other.item
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package com.topjohnwu.magisk.model.entity.state
|
||||
|
||||
enum class IndeterminateState {
|
||||
CHECKED, INDETERMINATE, UNCHECKED
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
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.recycler.HideProcessRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||
|
||||
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
|
||||
|
||||
class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event
|
||||
sealed class PolicyUpdateEvent(val item: MagiskPolicy) : RxBus.Event {
|
||||
class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item)
|
||||
class Log(item: MagiskPolicy) : PolicyUpdateEvent(item)
|
||||
}
|
||||
|
||||
class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event
|
@@ -0,0 +1,40 @@
|
||||
package com.topjohnwu.magisk.model.events
|
||||
|
||||
import android.app.Activity
|
||||
import com.skoumal.teanity.viewevents.ViewEvent
|
||||
import com.topjohnwu.magisk.model.entity.Policy
|
||||
import com.topjohnwu.magisk.model.entity.Repo
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
|
||||
|
||||
data class OpenLinkEvent(val url: String) : ViewEvent()
|
||||
|
||||
class ManagerInstallEvent : ViewEvent()
|
||||
class MagiskInstallEvent : ViewEvent()
|
||||
|
||||
class ManagerChangelogEvent : ViewEvent()
|
||||
class MagiskChangelogEvent : ViewEvent()
|
||||
|
||||
class UninstallEvent : ViewEvent()
|
||||
class EnvFixEvent : ViewEvent()
|
||||
|
||||
class UpdateSafetyNetEvent : ViewEvent()
|
||||
|
||||
class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent()
|
||||
|
||||
class OpenFilePickerEvent : ViewEvent()
|
||||
|
||||
class OpenChangelogEvent(val item: Repo) : ViewEvent()
|
||||
class InstallModuleEvent(val item: Repo) : ViewEvent()
|
||||
|
||||
class PageChangedEvent : ViewEvent()
|
||||
|
||||
class PermissionEvent(
|
||||
val permissions: List<String>,
|
||||
val callback: PublishSubject<Boolean>
|
||||
) : ViewEvent()
|
||||
|
||||
class BackPressEvent : ViewEvent()
|
||||
|
||||
class SuDialogEvent(val policy: Policy) : ViewEvent()
|
||||
class DieEvent : ViewEvent()
|
@@ -0,0 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.flash
|
||||
|
||||
interface FlashResultListener {
|
||||
|
||||
fun onResult(isSuccess: Boolean)
|
||||
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
package com.topjohnwu.magisk.model.flash
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.tasks.FlashZip
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
sealed class Flashing(
|
||||
uri: Uri,
|
||||
private val console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
private val resultListener: FlashResultListener
|
||||
) : FlashZip(uri, console, log) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (!success) {
|
||||
console.add("! Installation failed")
|
||||
}
|
||||
|
||||
resultListener.onResult(success)
|
||||
}
|
||||
|
||||
class Install(
|
||||
uri: Uri,
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Flashing(uri, console, log, resultListener) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
//Utils.loadModules()
|
||||
}
|
||||
super.onResult(success)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Uninstall(
|
||||
uri: Uri,
|
||||
console: MutableList<String>,
|
||||
log: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Flashing(uri, console, log, resultListener) {
|
||||
|
||||
private val context: Context by inject()
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
UiThreadHandler.handler.postDelayed(3000) {
|
||||
Shell.su("pm uninstall " + context.packageName).exec()
|
||||
}
|
||||
}
|
||||
super.onResult(success)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
package com.topjohnwu.magisk.model.flash
|
||||
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.tasks.MagiskInstaller
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
sealed class Patching(
|
||||
private val console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
private val resultListener: FlashResultListener
|
||||
) : MagiskInstaller(console, logs) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
console.add("- All done!")
|
||||
} else {
|
||||
Shell.sh("rm -rf $installDir").submit()
|
||||
console.add("! Installation failed")
|
||||
}
|
||||
resultListener.onResult(success)
|
||||
}
|
||||
|
||||
class File(
|
||||
private val uri: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Patching(console, logs, resultListener) {
|
||||
override fun operations() =
|
||||
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Patching(console, logs, resultListener) {
|
||||
override fun operations() =
|
||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||
}
|
||||
|
||||
class Direct(
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Patching(console, logs, resultListener) {
|
||||
override fun operations() =
|
||||
findImage() && extractZip() && patchBoot() && flashBoot()
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
package com.topjohnwu.magisk.model.navigation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.AnimRes
|
||||
import androidx.annotation.AnimatorRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.skoumal.teanity.viewevents.NavigationDslMarker
|
||||
import com.skoumal.teanity.viewevents.ViewEvent
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class MagiskNavigationEvent(
|
||||
val navDirections: MagiskNavDirectionsBuilder,
|
||||
val navOptions: MagiskNavOptions,
|
||||
val animOptions: MagiskAnimBuilder
|
||||
) : ViewEvent() {
|
||||
|
||||
companion object {
|
||||
operator fun invoke(builder: Builder.() -> Unit) = Builder().apply(builder).build()
|
||||
}
|
||||
|
||||
@NavigationDslMarker
|
||||
class Builder {
|
||||
|
||||
private var animOptions: MagiskAnimBuilder = MagiskAnimBuilder()
|
||||
private var navOptions: MagiskNavOptions = MagiskNavOptions()
|
||||
private val directionsBuilder = MagiskNavDirectionsBuilder()
|
||||
|
||||
fun args(builder: Bundle.() -> Unit) = directionsBuilder.args(builder)
|
||||
|
||||
fun navAnim(builder: MagiskAnimBuilder.() -> Unit) {
|
||||
animOptions = MagiskAnimBuilder().apply(builder)
|
||||
}
|
||||
|
||||
fun navOptions(builder: MagiskNavOptions.() -> Unit) {
|
||||
navOptions = MagiskNavOptions().apply(builder)
|
||||
}
|
||||
|
||||
fun navDirections(builder: MagiskNavDirectionsBuilder.() -> Unit) {
|
||||
directionsBuilder.apply(builder)
|
||||
}
|
||||
|
||||
internal fun build() = MagiskNavigationEvent(directionsBuilder, navOptions, animOptions)
|
||||
}
|
||||
}
|
||||
|
||||
@NavigationDslMarker
|
||||
class MagiskNavDirectionsBuilder {
|
||||
|
||||
var destination: KClass<out Fragment>? = null
|
||||
var isActivity: Boolean = false
|
||||
val args: Bundle = Bundle()
|
||||
|
||||
fun args(builder: Bundle.() -> Unit) = args.apply(builder)
|
||||
|
||||
}
|
||||
|
||||
@NavigationDslMarker
|
||||
class MagiskNavOptions {
|
||||
var popUpTo: KClass<*>? = null
|
||||
var inclusive: Boolean = false
|
||||
var clearTask: Boolean = false
|
||||
var singleTop: Boolean = false
|
||||
}
|
||||
|
||||
@NavigationDslMarker
|
||||
class MagiskAnimBuilder {
|
||||
@AnimRes
|
||||
@AnimatorRes
|
||||
var enter = 0
|
||||
|
||||
@AnimRes
|
||||
@AnimatorRes
|
||||
var exit = 0
|
||||
|
||||
@AnimRes
|
||||
@AnimatorRes
|
||||
var popEnter = 0
|
||||
|
||||
@AnimRes
|
||||
@AnimatorRes
|
||||
var popExit = 0
|
||||
|
||||
val anySet: Boolean get() = enter != 0 || exit != 0 || popEnter != 0 || popExit != 0
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
package com.topjohnwu.magisk.model.navigation
|
||||
|
||||
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
|
||||
import com.topjohnwu.magisk.ui.home.HomeFragment
|
||||
import com.topjohnwu.magisk.ui.log.LogFragment
|
||||
import com.topjohnwu.magisk.ui.module.ModulesFragment
|
||||
import com.topjohnwu.magisk.ui.module.ReposFragment
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsFragment
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
|
||||
|
||||
|
||||
object Navigation {
|
||||
|
||||
fun home() = MagiskNavigationEvent {
|
||||
navDirections { destination = HomeFragment::class }
|
||||
navOptions { popUpTo = HomeFragment::class }
|
||||
}
|
||||
|
||||
fun superuser() = MagiskNavigationEvent {
|
||||
navDirections { destination = SuperuserFragment::class }
|
||||
}
|
||||
|
||||
fun modules() = MagiskNavigationEvent {
|
||||
navDirections { destination = ModulesFragment::class }
|
||||
}
|
||||
|
||||
fun repos() = MagiskNavigationEvent {
|
||||
navDirections { destination = ReposFragment::class }
|
||||
}
|
||||
|
||||
fun hide() = MagiskNavigationEvent {
|
||||
navDirections { destination = MagiskHideFragment::class }
|
||||
}
|
||||
|
||||
fun log() = MagiskNavigationEvent {
|
||||
navDirections { destination = LogFragment::class }
|
||||
}
|
||||
|
||||
fun settings() = MagiskNavigationEvent {
|
||||
navDirections { destination = SettingsFragment::class }
|
||||
}
|
||||
|
||||
fun fromSection(section: String) = when (section) {
|
||||
"superuser" -> superuser()
|
||||
"modules" -> modules()
|
||||
"downloads" -> repos()
|
||||
"magiskhide" -> hide()
|
||||
"log" -> log()
|
||||
"settings" -> settings()
|
||||
else -> home()
|
||||
}
|
||||
|
||||
|
||||
object Main {
|
||||
const val OPEN_NAV = 1
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.model.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
interface Navigator {
|
||||
|
||||
//TODO Elevate Fragment to MagiskFragment<*,*> once everything is on board with it
|
||||
val baseFragments: List<KClass<out Fragment>>
|
||||
|
||||
fun navigateTo(event: MagiskNavigationEvent)
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
package com.topjohnwu.magisk.model.observer
|
||||
|
||||
import androidx.databinding.Observable
|
||||
import androidx.databinding.ObservableField
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
class Observer<T>(vararg dependencies: Observable, private val observer: () -> T) :
|
||||
ObservableField<T>(*dependencies), Serializable {
|
||||
|
||||
val value: T get() = observer()
|
||||
|
||||
@Deprecated(
|
||||
message = "Use KObservableField.value syntax from code",
|
||||
replaceWith = ReplaceWith("value")
|
||||
)
|
||||
override fun get(): T {
|
||||
return value
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "Observer cannot be set",
|
||||
level = DeprecationLevel.HIDDEN
|
||||
)
|
||||
override fun set(newValue: T) {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Observer(value=$value)"
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
package com.topjohnwu.magisk.model.permissions
|
||||
|
||||
typealias SimpleCallback = () -> Unit
|
||||
typealias PermissionRationaleCallback = (List<String>) -> Unit
|
||||
|
||||
class PermissionRequestBuilder {
|
||||
|
||||
private var onSuccessCallback: SimpleCallback = {}
|
||||
private var onFailureCallback: SimpleCallback = {}
|
||||
private var onShowRationaleCallback: PermissionRationaleCallback = {}
|
||||
|
||||
fun onSuccess(callback: SimpleCallback) {
|
||||
onSuccessCallback = callback
|
||||
}
|
||||
|
||||
fun onFailure(callback: SimpleCallback) {
|
||||
onFailureCallback = callback
|
||||
}
|
||||
|
||||
fun onShowRationale(callback: PermissionRationaleCallback) {
|
||||
onShowRationaleCallback = callback
|
||||
}
|
||||
|
||||
fun build(): PermissionRequest {
|
||||
return PermissionRequest(onSuccessCallback, onFailureCallback, onShowRationaleCallback)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PermissionRequest(
|
||||
private val onSuccessCallback: SimpleCallback,
|
||||
private val onFailureCallback: SimpleCallback,
|
||||
private val onShowRationaleCallback: PermissionRationaleCallback
|
||||
) {
|
||||
|
||||
fun onSuccess() = onSuccessCallback()
|
||||
fun onFailure() = onFailureCallback()
|
||||
fun onShowRationale(permissions: List<String>) = onShowRationaleCallback(permissions)
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class BooleanProperty(
|
||||
private val name: String,
|
||||
private val default: Boolean,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>
|
||||
): Boolean {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>,
|
||||
value: Boolean
|
||||
) {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class FloatProperty(
|
||||
private val name: String,
|
||||
private val default: Float,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>
|
||||
): Float {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>,
|
||||
value: Float
|
||||
) {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class IntProperty(
|
||||
private val name: String,
|
||||
private val default: Int,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>
|
||||
): Int {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>,
|
||||
value: Int
|
||||
) {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class LongProperty(
|
||||
private val name: String,
|
||||
private val default: Long,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>
|
||||
): Long {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>,
|
||||
value: Long
|
||||
) {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
interface PreferenceModel {
|
||||
|
||||
val context: Context
|
||||
|
||||
val fileName: String
|
||||
get() = "${context.packageName}_preferences"
|
||||
val commitPrefs: Boolean
|
||||
get() = false
|
||||
val prefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
fun preferenceStrInt(
|
||||
name: String,
|
||||
default: Int,
|
||||
writeDefault: Boolean = false,
|
||||
commit: Boolean = commitPrefs
|
||||
) = object: ReadWriteProperty<PreferenceModel, Int> {
|
||||
val base = StringProperty(name, default.toString(), commit)
|
||||
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
|
||||
base.getValue(thisRef, property).toInt()
|
||||
|
||||
override fun setValue(thisRef: PreferenceModel, property: KProperty<*>, value: Int) =
|
||||
base.setValue(thisRef, property, value.toString())
|
||||
}
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Boolean,
|
||||
commit: Boolean = commitPrefs
|
||||
) = BooleanProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = commitPrefs
|
||||
) = FloatProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = commitPrefs
|
||||
) = IntProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = commitPrefs
|
||||
) = LongProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = commitPrefs
|
||||
) = StringProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = commitPrefs
|
||||
) = StringSetProperty(name, default, commit)
|
||||
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
abstract class Property {
|
||||
|
||||
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
|
||||
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
|
||||
fun SharedPreferences.Editor.put(name: String, value: Int) = putInt(name, value)
|
||||
fun SharedPreferences.Editor.put(name: String, value: Long) = putLong(name, value)
|
||||
fun SharedPreferences.Editor.put(name: String, value: String) = putString(name, value)
|
||||
fun SharedPreferences.Editor.put(name: String, value: Set<String>) = putStringSet(name, value)
|
||||
|
||||
fun SharedPreferences.get(name: String, value: Boolean) = getBoolean(name, value)
|
||||
fun SharedPreferences.get(name: String, value: Float) = getFloat(name, value)
|
||||
fun SharedPreferences.get(name: String, value: Int) = getInt(name, value)
|
||||
fun SharedPreferences.get(name: String, value: Long) = getLong(name, value)
|
||||
fun SharedPreferences.get(name: String, value: String) = getString(name, value) ?: value
|
||||
fun SharedPreferences.get(name: String, value: Set<String>) = getStringSet(name, value) ?: value
|
||||
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class StringProperty(
|
||||
private val name: String,
|
||||
private val default: String,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, String> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>
|
||||
): String {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>,
|
||||
value: String
|
||||
) {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
class StringSetProperty(
|
||||
private val name: String,
|
||||
private val default: Set<String>,
|
||||
private val commit: Boolean
|
||||
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
|
||||
|
||||
override operator fun getValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>
|
||||
): Set<String> {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
thisRef: PreferenceModel,
|
||||
property: KProperty<*>,
|
||||
value: Set<String>
|
||||
) {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
thisRef.prefs.edit(commit) { put(prefName, value) }
|
||||
}
|
||||
}
|
@@ -1,84 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
|
||||
import com.topjohnwu.magisk.utils.DownloadApp;
|
||||
import com.topjohnwu.magisk.utils.SuLogger;
|
||||
import com.topjohnwu.magisk.view.Notifications;
|
||||
import com.topjohnwu.magisk.view.Shortcuts;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
public class GeneralReceiver extends BroadcastReceiver {
|
||||
|
||||
private String getPkg(Intent i) {
|
||||
return i.getData() == null ? "" : i.getData().getEncodedSchemeSpecificPart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
App app = App.self;
|
||||
if (intent == null)
|
||||
return;
|
||||
String action = intent.getAction();
|
||||
if (action == null)
|
||||
return;
|
||||
switch (action) {
|
||||
case Intent.ACTION_REBOOT:
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
action = intent.getStringExtra("action");
|
||||
if (action == null) {
|
||||
// Actual boot completed event
|
||||
Shell.su("mm_patch_dtbo").submit(result -> {
|
||||
if (result.isSuccess())
|
||||
Notifications.dtboPatched();
|
||||
});
|
||||
break;
|
||||
}
|
||||
switch (action) {
|
||||
case SuRequestActivity.REQUEST:
|
||||
Intent i = new Intent(app, ClassMap.get(SuRequestActivity.class))
|
||||
.setAction(action)
|
||||
.putExtra("socket", intent.getStringExtra("socket"))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
|
||||
app.startActivity(i);
|
||||
break;
|
||||
case SuRequestActivity.LOG:
|
||||
SuLogger.handleLogs(intent);
|
||||
break;
|
||||
case SuRequestActivity.NOTIFY:
|
||||
SuLogger.handleNotify(intent);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_REPLACED:
|
||||
// This will only work pre-O
|
||||
if (Config.get(Config.Key.SU_REAUTH)) {
|
||||
app.mDB.deletePolicy(getPkg(intent));
|
||||
}
|
||||
break;
|
||||
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
|
||||
String pkg = getPkg(intent);
|
||||
app.mDB.deletePolicy(pkg);
|
||||
Shell.su("magiskhide --rm " + pkg).submit();
|
||||
break;
|
||||
case Intent.ACTION_LOCALE_CHANGED:
|
||||
Shortcuts.setup(context);
|
||||
break;
|
||||
case Const.Key.BROADCAST_MANAGER_UPDATE:
|
||||
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK);
|
||||
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME));
|
||||
break;
|
||||
case Const.Key.BROADCAST_REBOOT:
|
||||
Shell.su("/system/bin/reboot").submit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.magisk.model.receiver
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.repository.AppRepository
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.DownloadApp
|
||||
import com.topjohnwu.magisk.utils.SuLogger
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.reboot
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
open class GeneralReceiver : BroadcastReceiver() {
|
||||
|
||||
private val appRepo: AppRepository by inject()
|
||||
|
||||
companion object {
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
}
|
||||
|
||||
private fun getPkg(intent: Intent): String {
|
||||
return intent.data?.encodedSchemeSpecificPart ?: ""
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null)
|
||||
return
|
||||
var action: String? = intent.action ?: return
|
||||
when (action) {
|
||||
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
|
||||
action = intent.getStringExtra("action")
|
||||
if (action == null) {
|
||||
// Actual boot completed event
|
||||
Shell.su("mm_patch_dtbo").submit { result ->
|
||||
if (result.isSuccess)
|
||||
Notifications.dtboPatched()
|
||||
}
|
||||
return
|
||||
}
|
||||
when (action) {
|
||||
REQUEST -> {
|
||||
val i = Intent(context, ClassMap[SuRequestActivity::class.java])
|
||||
.setAction(action)
|
||||
.putExtra("socket", intent.getStringExtra("socket"))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
|
||||
context.startActivity(i)
|
||||
}
|
||||
LOG -> SuLogger.handleLogs(intent)
|
||||
NOTIFY -> SuLogger.handleNotify(intent)
|
||||
TEST -> Shell.su("magisk --use-broadcast").submit()
|
||||
}
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED ->
|
||||
// This will only work pre-O
|
||||
if (Config.suReAuth)
|
||||
appRepo.delete(getPkg(intent)).blockingGet()
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
val pkg = getPkg(intent)
|
||||
appRepo.delete(pkg).blockingGet()
|
||||
"magiskhide --rm $pkg".su().blockingGet()
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
|
||||
Const.Key.BROADCAST_MANAGER_UPDATE -> {
|
||||
Info.remote = Info.remote.copy(app = Info.remote.app.copy(
|
||||
link = intent.getStringExtra(Const.Key.INTENT_SET_LINK) ?: ""))
|
||||
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME))
|
||||
}
|
||||
Const.Key.BROADCAST_REBOOT -> reboot()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.update;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.ListenableWorker;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker;
|
||||
import com.topjohnwu.magisk.tasks.CheckUpdates;
|
||||
import com.topjohnwu.magisk.view.Notifications;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
public class UpdateCheckService extends DelegateWorker {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListenableWorker.Result doWork() {
|
||||
if (App.foreground() == null) {
|
||||
Shell.getShell();
|
||||
CheckUpdates.check(this::onCheckDone);
|
||||
}
|
||||
return ListenableWorker.Result.success();
|
||||
}
|
||||
|
||||
private void onCheckDone() {
|
||||
if (BuildConfig.VERSION_CODE < Config.remoteManagerVersionCode) {
|
||||
Notifications.managerUpdate();
|
||||
} else if (Config.magiskVersionCode < Config.remoteMagiskVersionCode) {
|
||||
Notifications.magiskUpdate();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.model.update
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
class UpdateCheckService : DelegateWorker() {
|
||||
|
||||
private val magiskRepo: MagiskRepository by inject()
|
||||
|
||||
override fun doWork(): ListenableWorker.Result {
|
||||
// Make sure shell initializer was ran
|
||||
Shell.getShell()
|
||||
return runCatching {
|
||||
magiskRepo.fetchUpdate().blockingGet()
|
||||
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
|
||||
Notifications.managerUpdate()
|
||||
else if (Info.magiskVersionCode < Info.remote.magisk.versionCode)
|
||||
Notifications.magiskUpdate()
|
||||
ListenableWorker.Result.success()
|
||||
}.getOrElse {
|
||||
ListenableWorker.Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
75
app/src/main/java/com/topjohnwu/magisk/model/zip/Zip.kt
Normal file
75
app/src/main/java/com/topjohnwu/magisk/model/zip/Zip.kt
Normal file
@@ -0,0 +1,75 @@
|
||||
package com.topjohnwu.magisk.model.zip
|
||||
|
||||
import com.topjohnwu.magisk.utils.forEach
|
||||
import com.topjohnwu.magisk.utils.withStreams
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import java.io.File
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
|
||||
class Zip private constructor(private val values: Builder) {
|
||||
|
||||
companion object {
|
||||
operator fun invoke(builder: Builder.() -> Unit): Zip {
|
||||
return Zip(Builder().apply(builder))
|
||||
}
|
||||
}
|
||||
|
||||
class Builder {
|
||||
lateinit var zip: File
|
||||
lateinit var destination: File
|
||||
var excludeDirs = true
|
||||
}
|
||||
|
||||
data class Path(val path: String, val pullFromDir: Boolean = true)
|
||||
|
||||
fun unzip(vararg paths: Pair<String, Boolean>) =
|
||||
unzip(*paths.map { Path(it.first, it.second) }.toTypedArray())
|
||||
|
||||
@Suppress("RedundantLambdaArrow")
|
||||
fun unzip(vararg paths: Path) {
|
||||
ensureRequiredParams()
|
||||
|
||||
values.zip.zipStream().use {
|
||||
it.forEach { e ->
|
||||
val currentPath = paths.firstOrNull { e.name.startsWith(it.path) }
|
||||
val isDirectory = values.excludeDirs && e.isDirectory
|
||||
if (currentPath == null || isDirectory) {
|
||||
// Ignore directories, only create files
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val name = if (currentPath.pullFromDir) {
|
||||
e.name.substring(e.name.lastIndexOf('/') + 1)
|
||||
} else {
|
||||
e.name
|
||||
}
|
||||
|
||||
val out = File(values.destination, name)
|
||||
.ensureExists()
|
||||
.outputStream()
|
||||
//.suOutputStream()
|
||||
|
||||
withStreams(it, out) { reader, writer ->
|
||||
reader.copyTo(writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureRequiredParams() {
|
||||
if (!values.zip.exists()) {
|
||||
throw RuntimeException("Zip file does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.ensureExists() =
|
||||
if ((!parentFile.exists() && !parentFile.mkdirs()) || parentFile is SuFile) {
|
||||
SuFile(parentFile, name).apply { parentFile.mkdirs() }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
private fun File.zipStream() = ZipInputStream(inputStream())
|
||||
|
||||
}
|
@@ -1,132 +0,0 @@
|
||||
package com.topjohnwu.magisk.tasks;
|
||||
|
||||
import android.os.SystemClock;
|
||||
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.net.Request;
|
||||
import com.topjohnwu.net.ResponseListener;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CheckUpdates {
|
||||
|
||||
private static Request getRequest() {
|
||||
String url;
|
||||
switch ((int) Config.get(Config.Key.UPDATE_CHANNEL)) {
|
||||
case Config.Value.BETA_CHANNEL:
|
||||
url = Const.Url.BETA_URL;
|
||||
break;
|
||||
case Config.Value.CUSTOM_CHANNEL:
|
||||
url = Config.get(Config.Key.CUSTOM_CHANNEL);
|
||||
break;
|
||||
case Config.Value.CANARY_CHANNEL:
|
||||
url = Const.Url.CANARY_URL;
|
||||
break;
|
||||
case Config.Value.CANARY_DEBUG_CHANNEL:
|
||||
url = Const.Url.CANARY_DEBUG_URL;
|
||||
break;
|
||||
default:
|
||||
url = Const.Url.STABLE_URL;
|
||||
break;
|
||||
}
|
||||
return Networking.get(url);
|
||||
}
|
||||
|
||||
public static void check() {
|
||||
check(null);
|
||||
}
|
||||
|
||||
public static void check(Runnable cb) {
|
||||
Request request = getRequest();
|
||||
UpdateListener listener = new UpdateListener(cb);
|
||||
if (ShellUtils.onMainThread()) {
|
||||
request.getAsJSONObject(listener);
|
||||
} else {
|
||||
JSONObject json = request.execForJSONObject().getResult();
|
||||
if (json != null)
|
||||
listener.onResponse(json);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UpdateListener implements ResponseListener<JSONObject> {
|
||||
|
||||
private Runnable cb;
|
||||
private long start;
|
||||
|
||||
UpdateListener(Runnable callback) {
|
||||
cb = callback;
|
||||
start = SystemClock.uptimeMillis();
|
||||
}
|
||||
|
||||
private int getInt(JSONObject json, String name, int defValue) {
|
||||
if (json == null)
|
||||
return defValue;
|
||||
try {
|
||||
return json.getInt(name);
|
||||
} catch (JSONException e) {
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
private String getString(JSONObject json, String name, String defValue) {
|
||||
if (json == null)
|
||||
return defValue;
|
||||
try {
|
||||
return json.getString(name);
|
||||
} catch (JSONException e) {
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getJson(JSONObject json, String name) {
|
||||
try {
|
||||
return json.getJSONObject(name);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(JSONObject json) {
|
||||
JSONObject magisk = getJson(json, "magisk");
|
||||
Config.remoteMagiskVersionCode = getInt(magisk, "versionCode", -1);
|
||||
|
||||
if ((int) Config.get(Config.Key.UPDATE_CHANNEL) == Config.Value.DEFAULT_CHANNEL) {
|
||||
if (Config.magiskVersionCode > Config.remoteMagiskVersionCode) {
|
||||
// If we are newer than current stable channel, switch to beta
|
||||
Config.set(Config.Key.UPDATE_CHANNEL, Config.Value.BETA_CHANNEL);
|
||||
check(cb);
|
||||
return;
|
||||
} else {
|
||||
Config.set(Config.Key.UPDATE_CHANNEL, Config.Value.STABLE_CHANNEL);
|
||||
}
|
||||
}
|
||||
|
||||
Config.remoteMagiskVersionString = getString(magisk, "version", null);
|
||||
Config.magiskLink = getString(magisk, "link", null);
|
||||
Config.magiskNoteLink = getString(magisk, "note", null);
|
||||
Config.magiskMD5 = getString(magisk, "md5", null);
|
||||
|
||||
JSONObject manager = getJson(json, "app");
|
||||
Config.remoteManagerVersionString = getString(manager, "version", null);
|
||||
Config.remoteManagerVersionCode = getInt(manager, "versionCode", -1);
|
||||
Config.managerLink = getString(manager, "link", null);
|
||||
Config.managerNoteLink = getString(manager, "note", null);
|
||||
|
||||
JSONObject uninstaller = getJson(json, "uninstaller");
|
||||
Config.uninstallerLink = getString(uninstaller, "link", null);
|
||||
|
||||
UiThreadHandler.handler.postAtTime(() -> Event.trigger(Event.UPDATE_CHECK_DONE),
|
||||
start + 1000 /* Add artificial delay to let UI behave correctly */);
|
||||
|
||||
if (cb != null)
|
||||
cb.run();
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
package com.topjohnwu.magisk.tasks;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.magisk.utils.ZipUtils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class FlashZip {
|
||||
|
||||
private Uri mUri;
|
||||
private File tmpFile;
|
||||
private List<String> console, logs;
|
||||
|
||||
public FlashZip(Uri uri, List<String> out, List<String> err) {
|
||||
mUri = uri;
|
||||
console = out;
|
||||
logs = err;
|
||||
tmpFile = new File(App.self.getCacheDir(), "install.zip");
|
||||
}
|
||||
|
||||
private boolean unzipAndCheck() throws IOException {
|
||||
ZipUtils.unzip(tmpFile, tmpFile.getParentFile(), "META-INF/com/google/android", true);
|
||||
return Shell.su("grep -q '#MAGISK' " + new File(tmpFile.getParentFile(), "updater-script"))
|
||||
.exec().isSuccess();
|
||||
}
|
||||
|
||||
private boolean flash() throws IOException {
|
||||
console.add("- Copying zip to temp directory");
|
||||
try (InputStream in = App.self.getContentResolver().openInputStream(mUri);
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(tmpFile))) {
|
||||
if (in == null) throw new FileNotFoundException();
|
||||
InputStream buf= new BufferedInputStream(in);
|
||||
ShellUtils.pump(buf, out);
|
||||
} catch (FileNotFoundException e) {
|
||||
console.add("! Invalid Uri");
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
console.add("! Cannot copy to cache");
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
if (!unzipAndCheck()) {
|
||||
console.add("! This zip is not a Magisk Module!");
|
||||
return false;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
console.add("! Unzip error");
|
||||
throw e;
|
||||
}
|
||||
console.add("- Installing " + Utils.getNameFromUri(App.self, mUri));
|
||||
return Shell.su("cd " + tmpFile.getParent(),
|
||||
"BOOTMODE=true sh update-binary dummy 1 " + tmpFile)
|
||||
.to(console, logs)
|
||||
.exec().isSuccess();
|
||||
}
|
||||
|
||||
public void exec() {
|
||||
App.THREAD_POOL.execute(() -> {
|
||||
boolean success = false;
|
||||
try {
|
||||
success = flash();
|
||||
} catch (IOException ignored) {}
|
||||
Shell.su("cd /", "rm -rf " + tmpFile.getParent() + " " + Const.TMP_FOLDER_PATH).submit();
|
||||
boolean finalSuccess = success;
|
||||
UiThreadHandler.run(() -> onResult(finalSuccess));
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void onResult(boolean success);
|
||||
}
|
96
app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt
Normal file
96
app/src/main/java/com/topjohnwu/magisk/tasks/FlashZip.kt
Normal file
@@ -0,0 +1,96 @@
|
||||
package com.topjohnwu.magisk.tasks
|
||||
|
||||
import android.net.Uri
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.utils.fileName
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.readUri
|
||||
import com.topjohnwu.magisk.utils.unzip
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
||||
abstract class FlashZip(
|
||||
private val mUri: Uri,
|
||||
private val console: MutableList<String>,
|
||||
private val logs: MutableList<String>
|
||||
) {
|
||||
|
||||
private val app: App by inject()
|
||||
private val tmpFile: File = File(app.cacheDir, "install.zip")
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun unzipAndCheck(): Boolean {
|
||||
val parentFile = tmpFile.parentFile ?: return false
|
||||
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
|
||||
|
||||
val updaterScript = File(parentFile, "updater-script")
|
||||
return Shell
|
||||
.su("grep -q '#MAGISK' $updaterScript")
|
||||
.exec()
|
||||
.isSuccess
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun flash(): Boolean {
|
||||
console.add("- Copying zip to temp directory")
|
||||
|
||||
runCatching {
|
||||
app.readUri(mUri).use { input ->
|
||||
tmpFile.outputStream().use { out -> input.copyTo(out) }
|
||||
}
|
||||
}.getOrElse {
|
||||
when (it) {
|
||||
is FileNotFoundException -> console.add("! Invalid Uri")
|
||||
is IOException -> console.add("! Cannot copy to cache")
|
||||
}
|
||||
throw it
|
||||
}
|
||||
|
||||
val isMagiskModule = runCatching {
|
||||
unzipAndCheck()
|
||||
}.getOrElse {
|
||||
console.add("! Unzip error")
|
||||
throw it
|
||||
}
|
||||
|
||||
if (!isMagiskModule) {
|
||||
console.add("! This zip is not a Magisk Module!")
|
||||
return false
|
||||
}
|
||||
|
||||
console.add("- Installing ${mUri.fileName}")
|
||||
|
||||
val parentFile = tmpFile.parent ?: return false
|
||||
|
||||
return Shell
|
||||
.su(
|
||||
"cd $parentFile",
|
||||
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
|
||||
)
|
||||
.to(console, logs)
|
||||
.exec().isSuccess
|
||||
}
|
||||
|
||||
fun exec() = Single
|
||||
.fromCallable {
|
||||
runCatching {
|
||||
flash()
|
||||
}.getOrElse {
|
||||
it.printStackTrace()
|
||||
false
|
||||
}.apply {
|
||||
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}")
|
||||
.submit()
|
||||
}
|
||||
}
|
||||
.subscribeK(onError = { onResult(false) }) { onResult(it) }
|
||||
.let { Unit } // ignores result disposable
|
||||
|
||||
|
||||
protected abstract fun onResult(success: Boolean)
|
||||
}
|
@@ -8,8 +8,8 @@ import androidx.annotation.MainThread;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.Info;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.DownloadProgressListener;
|
||||
import com.topjohnwu.net.Networking;
|
||||
@@ -123,9 +123,9 @@ public abstract class MagiskInstaller {
|
||||
|
||||
File zip = new File(App.self.getCacheDir(), "magisk.zip");
|
||||
|
||||
if (!ShellUtils.checkSum("MD5", zip, Config.magiskMD5)) {
|
||||
if (!ShellUtils.checkSum("MD5", zip, Info.remote.getMagisk().getHash())) {
|
||||
console.add("- Downloading zip");
|
||||
Networking.get(Config.magiskLink)
|
||||
Networking.get(Info.remote.getMagisk().getLink())
|
||||
.setDownloadProgressListener(new ProgressLog())
|
||||
.execForFile(zip);
|
||||
} else {
|
||||
@@ -282,10 +282,10 @@ public abstract class MagiskInstaller {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Shell.sh(Utils.fmt(
|
||||
if (!Shell.sh(Utils.INSTANCE.fmt(
|
||||
"KEEPFORCEENCRYPT=%b KEEPVERITY=%b RECOVERYMODE=%b " +
|
||||
"sh update-binary sh boot_patch.sh %s",
|
||||
Config.keepEnc, Config.keepVerity, Config.recovery, srcBoot))
|
||||
Info.keepEnc, Info.keepVerity, Info.recovery, srcBoot))
|
||||
.to(console, logs).exec().isSuccess())
|
||||
return false;
|
||||
|
||||
@@ -311,10 +311,10 @@ public abstract class MagiskInstaller {
|
||||
}
|
||||
|
||||
protected boolean flashBoot() {
|
||||
if (!Shell.su(Utils.fmt("direct_install %s %s", installDir, srcBoot))
|
||||
if (!Shell.su(Utils.INSTANCE.fmt("direct_install %s %s", installDir, srcBoot))
|
||||
.to(console, logs).exec().isSuccess())
|
||||
return false;
|
||||
if (!Config.keepVerity)
|
||||
if (!Info.keepVerity)
|
||||
Shell.su("patch_dtbo_image").to(console, logs).exec();
|
||||
return true;
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ import android.util.Pair;
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
import com.topjohnwu.magisk.utils.Logger;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
@@ -18,7 +18,6 @@ import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.net.HttpURLConnection;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
@@ -31,16 +30,19 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
public class UpdateRepos {
|
||||
private static final DateFormat DATE_FORMAT;
|
||||
import androidx.annotation.NonNull;
|
||||
import io.reactivex.Single;
|
||||
|
||||
private App app = App.self;
|
||||
@Deprecated
|
||||
public class UpdateRepos {
|
||||
|
||||
@NonNull
|
||||
private final RepoDatabaseHelper repoDB;
|
||||
private Set<String> cached;
|
||||
private Queue<Pair<String, Date>> moduleQueue;
|
||||
|
||||
static {
|
||||
DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
public UpdateRepos(@NonNull RepoDatabaseHelper repoDatabase) {
|
||||
repoDB = repoDatabase;
|
||||
}
|
||||
|
||||
private void runTasks(Runnable task) {
|
||||
@@ -54,20 +56,32 @@ public class UpdateRepos {
|
||||
f.get();
|
||||
} catch (InterruptedException e) {
|
||||
continue;
|
||||
} catch (ExecutionException ignored) {}
|
||||
} catch (ExecutionException ignored) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Static instance of (Simple)DateFormat is not threadsafe so in order to make it safe it needs
|
||||
* to be created beforehand on the same thread where it'll be used.
|
||||
* See https://stackoverflow.com/a/18383395
|
||||
*/
|
||||
private static SimpleDateFormat getDateFormat() {
|
||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
format.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return format;
|
||||
}
|
||||
|
||||
/* We sort repos by last push, which means that we only need to check whether the
|
||||
* first page is updated to determine whether the online repo database is changed
|
||||
*/
|
||||
private boolean parsePage(int page) {
|
||||
Request req = Networking.get(Utils.fmt(Const.Url.REPO_URL, page + 1));
|
||||
Request req = Networking.get(Utils.INSTANCE.fmt(Const.Url.REPO_URL, page + 1));
|
||||
if (page == 0) {
|
||||
String etag = Config.get(Config.Key.ETAG_KEY);
|
||||
if (etag != null)
|
||||
String etag = Config.getEtagKey();
|
||||
if (!etag.isEmpty())
|
||||
req.addHeaders(Const.Key.IF_NONE_MATCH, etag);
|
||||
}
|
||||
Request.Result<JSONArray> res = req.execForJSONArray();
|
||||
@@ -84,10 +98,12 @@ public class UpdateRepos {
|
||||
return true;
|
||||
|
||||
try {
|
||||
SimpleDateFormat dateFormat = getDateFormat();
|
||||
|
||||
for (int i = 0; i < res.getResult().length(); i++) {
|
||||
JSONObject rawRepo = res.getResult().getJSONObject(i);
|
||||
String id = rawRepo.getString("name");
|
||||
Date date = DATE_FORMAT.parse(rawRepo.getString("pushed_at"));
|
||||
Date date = dateFormat.parse(rawRepo.getString("pushed_at"));
|
||||
moduleQueue.offer(new Pair<>(id, date));
|
||||
}
|
||||
} catch (JSONException | ParseException e) {
|
||||
@@ -100,7 +116,7 @@ public class UpdateRepos {
|
||||
String etag = res.getConnection().getHeaderField(Config.Key.ETAG_KEY);
|
||||
if (etag != null) {
|
||||
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
|
||||
Config.set(Config.Key.ETAG_KEY, etag);
|
||||
Config.setEtagKey(etag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,17 +132,17 @@ public class UpdateRepos {
|
||||
Pair<String, Date> pair = moduleQueue.poll();
|
||||
if (pair == null)
|
||||
return;
|
||||
Repo repo = app.repoDB.getRepo(pair.first);
|
||||
Repo repo = repoDB.getRepo(pair.first);
|
||||
try {
|
||||
if (repo == null)
|
||||
repo = new Repo(pair.first);
|
||||
else
|
||||
cached.remove(pair.first);
|
||||
repo.update(pair.second);
|
||||
app.repoDB.addRepo(repo);
|
||||
repoDB.addRepo(repo);
|
||||
} catch (Repo.IllegalRepoException e) {
|
||||
Logger.debug(e.getMessage());
|
||||
app.repoDB.removeRepo(pair.first);
|
||||
repoDB.removeRepo(pair.first);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -134,7 +150,7 @@ public class UpdateRepos {
|
||||
}
|
||||
|
||||
private void fullReload() {
|
||||
Cursor c = app.repoDB.getRawCursor();
|
||||
Cursor c = repoDB.getRawCursor();
|
||||
runTasks(() -> {
|
||||
while (true) {
|
||||
Repo repo;
|
||||
@@ -145,32 +161,31 @@ public class UpdateRepos {
|
||||
}
|
||||
try {
|
||||
repo.update();
|
||||
app.repoDB.addRepo(repo);
|
||||
repoDB.addRepo(repo);
|
||||
} catch (Repo.IllegalRepoException e) {
|
||||
Logger.debug(e.getMessage());
|
||||
app.repoDB.removeRepo(repo);
|
||||
repoDB.removeRepo(repo);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void exec(boolean force) {
|
||||
Event.reset(Event.REPO_LOAD_DONE);
|
||||
App.THREAD_POOL.execute(() -> {
|
||||
cached = Collections.synchronizedSet(app.repoDB.getRepoIDSet());
|
||||
public Single<Boolean> exec(boolean force) {
|
||||
return Single.fromCallable(() -> {
|
||||
cached = Collections.synchronizedSet(repoDB.getRepoIDSet());
|
||||
moduleQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
if (loadPages()) {
|
||||
// The leftover cached means they are removed from online repo
|
||||
app.repoDB.removeRepo(cached);
|
||||
repoDB.removeRepo(cached);
|
||||
} else if (force) {
|
||||
fullReload();
|
||||
}
|
||||
Event.trigger(Event.REPO_LOAD_DONE);
|
||||
return force; // not important
|
||||
});
|
||||
}
|
||||
|
||||
public void exec() {
|
||||
exec(false);
|
||||
public Single<Boolean> exec() {
|
||||
return exec(false);
|
||||
}
|
||||
}
|
||||
|
@@ -1,194 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
||||
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment;
|
||||
import com.topjohnwu.magisk.ui.home.MagiskFragment;
|
||||
import com.topjohnwu.magisk.ui.log.LogFragment;
|
||||
import com.topjohnwu.magisk.ui.module.ModulesFragment;
|
||||
import com.topjohnwu.magisk.ui.module.ReposFragment;
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsFragment;
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
||||
public class MainActivity extends BaseActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener {
|
||||
|
||||
private final Handler mDrawerHandler = new Handler();
|
||||
private int mDrawerItem;
|
||||
private static boolean fromShortcut = false;
|
||||
|
||||
@BindView(R.id.toolbar) public Toolbar toolbar;
|
||||
@BindView(R.id.drawer_layout) DrawerLayout drawer;
|
||||
@BindView(R.id.nav_view) NavigationView navigationView;
|
||||
|
||||
private float toolbarElevation;
|
||||
|
||||
@Override
|
||||
public int getDarkTheme() {
|
||||
return R.style.AppTheme_Dark;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
if (!SplashActivity.DONE) {
|
||||
startActivity(new Intent(this, ClassMap.get(SplashActivity.class)));
|
||||
finish();
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
new MainActivity_ViewBinding(this);
|
||||
checkHideSection();
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.magisk, R.string.magisk) {
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
super.onDrawerOpened(drawerView);
|
||||
super.onDrawerSlide(drawerView, 0); // this disables the arrow @ completed tate
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerSlide(View drawerView, float slideOffset) {
|
||||
super.onDrawerSlide(drawerView, 0); // this disables the animation
|
||||
}
|
||||
};
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
toolbarElevation = toolbar.getElevation();
|
||||
}
|
||||
|
||||
drawer.addDrawerListener(toggle);
|
||||
toggle.syncState();
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
String section = getIntent().getStringExtra(Const.Key.OPEN_SECTION);
|
||||
fromShortcut = section != null;
|
||||
navigate(section);
|
||||
}
|
||||
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (drawer.isDrawerOpen(navigationView)) {
|
||||
drawer.closeDrawer(navigationView);
|
||||
} else if (mDrawerItem != R.id.magisk && !fromShortcut) {
|
||||
navigate(R.id.magisk);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
|
||||
mDrawerHandler.removeCallbacksAndMessages(null);
|
||||
mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250);
|
||||
drawer.closeDrawer(navigationView);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void checkHideSection() {
|
||||
Menu menu = navigationView.getMenu();
|
||||
menu.findItem(R.id.magiskhide).setVisible(Shell.rootAccess() &&
|
||||
(boolean) Config.get(Config.Key.MAGISKHIDE));
|
||||
menu.findItem(R.id.modules).setVisible(Shell.rootAccess() && Config.magiskVersionCode >= 0);
|
||||
menu.findItem(R.id.downloads).setVisible(Networking.checkNetworkStatus(this)
|
||||
&& Shell.rootAccess() && Config.magiskVersionCode >= 0);
|
||||
menu.findItem(R.id.log).setVisible(Shell.rootAccess());
|
||||
menu.findItem(R.id.superuser).setVisible(Utils.showSuperUser());
|
||||
}
|
||||
|
||||
public void navigate(String item) {
|
||||
int itemId = R.id.magisk;
|
||||
if (item != null) {
|
||||
switch (item) {
|
||||
case "superuser":
|
||||
itemId = R.id.superuser;
|
||||
break;
|
||||
case "modules":
|
||||
itemId = R.id.modules;
|
||||
break;
|
||||
case "downloads":
|
||||
itemId = R.id.downloads;
|
||||
break;
|
||||
case "magiskhide":
|
||||
itemId = R.id.magiskhide;
|
||||
break;
|
||||
case "log":
|
||||
itemId = R.id.log;
|
||||
break;
|
||||
case "settings":
|
||||
itemId = R.id.settings;
|
||||
break;
|
||||
}
|
||||
}
|
||||
navigate(itemId);
|
||||
}
|
||||
|
||||
public void navigate(int itemId) {
|
||||
mDrawerItem = itemId;
|
||||
navigationView.setCheckedItem(itemId);
|
||||
switch (itemId) {
|
||||
case R.id.magisk:
|
||||
fromShortcut = false;
|
||||
displayFragment(new MagiskFragment(), true);
|
||||
break;
|
||||
case R.id.superuser:
|
||||
displayFragment(new SuperuserFragment(), true);
|
||||
break;
|
||||
case R.id.modules:
|
||||
displayFragment(new ModulesFragment(), true);
|
||||
break;
|
||||
case R.id.downloads:
|
||||
displayFragment(new ReposFragment(), true);
|
||||
break;
|
||||
case R.id.magiskhide:
|
||||
displayFragment(new MagiskHideFragment(), true);
|
||||
break;
|
||||
case R.id.log:
|
||||
displayFragment(new LogFragment(), false);
|
||||
break;
|
||||
case R.id.settings:
|
||||
displayFragment(new SettingsFragment(), true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void displayFragment(@NonNull Fragment navFragment, boolean setElevation) {
|
||||
supportInvalidateOptionsMenu();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
|
||||
.replace(R.id.content_frame, navFragment)
|
||||
.commitNow();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
toolbar.setElevation(setElevation ? toolbarElevation : 0);
|
||||
}
|
||||
}
|
||||
}
|
119
app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
119
app/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
@@ -0,0 +1,119 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.databinding.ActivityMainBinding
|
||||
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
||||
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
|
||||
import com.topjohnwu.magisk.ui.home.HomeFragment
|
||||
import com.topjohnwu.magisk.ui.log.LogFragment
|
||||
import com.topjohnwu.magisk.ui.module.ModulesFragment
|
||||
import com.topjohnwu.magisk.ui.module.ReposFragment
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsFragment
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.activity_main
|
||||
override val viewModel: MainViewModel by viewModel()
|
||||
override val navHostId: Int = R.id.main_nav_host
|
||||
override val defaultPosition: Int = 0
|
||||
|
||||
override val baseFragments: List<KClass<out Fragment>> = listOf(
|
||||
HomeFragment::class,
|
||||
SuperuserFragment::class,
|
||||
MagiskHideFragment::class,
|
||||
ModulesFragment::class,
|
||||
ReposFragment::class,
|
||||
LogFragment::class,
|
||||
SettingsFragment::class
|
||||
)
|
||||
|
||||
/*override fun getDarkTheme(): Int {
|
||||
return R.style.AppTheme_Dark
|
||||
}*/
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (!SplashActivity.DONE) {
|
||||
startActivity(Intent(this, ClassMap[SplashActivity::class.java]))
|
||||
finish()
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
checkHideSection()
|
||||
setSupportActionBar(binding.mainInclude.mainToolbar)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
intent.getStringExtra(OPEN_SECTION)?.let {
|
||||
onEventDispatched(Navigation.fromSection(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTitle(title: CharSequence?) {
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
|
||||
override fun setTitle(titleId: Int) {
|
||||
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() {
|
||||
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
|
||||
binding.drawerLayout.closeDrawer(binding.navView)
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSimpleEventDispatched(event: Int) {
|
||||
super.onSimpleEventDispatched(event)
|
||||
when (event) {
|
||||
Navigation.Main.OPEN_NAV -> openNav()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START)
|
||||
|
||||
private fun checkHideSection() {
|
||||
val menu = binding.navView.menu
|
||||
menu.findItem(R.id.magiskHideFragment).isVisible =
|
||||
Shell.rootAccess() && Config.magiskHide
|
||||
menu.findItem(R.id.modulesFragment).isVisible =
|
||||
Shell.rootAccess() && Info.magiskVersionCode >= 0
|
||||
menu.findItem(R.id.reposFragment).isVisible =
|
||||
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Info.magiskVersionCode >= 0)
|
||||
menu.findItem(R.id.logFragment).isVisible =
|
||||
Shell.rootAccess()
|
||||
menu.findItem(R.id.superuserFragment).isVisible =
|
||||
Utils.showSuperUser()
|
||||
}
|
||||
}
|
27
app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt
Normal file
27
app/src/main/java/com/topjohnwu/magisk/ui/MainViewModel.kt
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.view.MenuItem
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.model.navigation.Navigation
|
||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||
|
||||
|
||||
class MainViewModel : MagiskViewModel() {
|
||||
|
||||
fun navPressed() = Navigation.Main.OPEN_NAV.publish()
|
||||
|
||||
fun navigationItemPressed(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.magiskFragment -> Navigation.home()
|
||||
R.id.superuserFragment -> Navigation.superuser()
|
||||
R.id.magiskHideFragment -> Navigation.hide()
|
||||
R.id.modulesFragment -> Navigation.modules()
|
||||
R.id.reposFragment -> Navigation.repos()
|
||||
R.id.logFragment -> Navigation.log()
|
||||
R.id.settings -> Navigation.settings()
|
||||
else -> null
|
||||
}?.publish()?.let { return@navigationItemPressed true }
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
@@ -1,97 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.magisk.tasks.CheckUpdates;
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos;
|
||||
import com.topjohnwu.magisk.ui.base.BaseActivity;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.magisk.view.Notifications;
|
||||
import com.topjohnwu.magisk.view.Shortcuts;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
public class SplashActivity extends BaseActivity {
|
||||
|
||||
public static boolean DONE = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Shell.getShell(shell -> {
|
||||
if (Config.magiskVersionCode > 0 &&
|
||||
Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.unsupport_magisk_title)
|
||||
.setMessage(R.string.unsupport_magisk_message)
|
||||
.setNegativeButton(R.string.ok, null)
|
||||
.setOnDismissListener(dialog -> finish())
|
||||
.show();
|
||||
} else {
|
||||
initAndStart();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initAndStart() {
|
||||
String pkg = Config.get(Config.Key.SU_MANAGER);
|
||||
if (pkg != null && getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
||||
Config.remove(Config.Key.SU_MANAGER);
|
||||
Shell.su("pm uninstall " + pkg).submit();
|
||||
}
|
||||
if (TextUtils.equals(pkg, getPackageName())) {
|
||||
try {
|
||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
||||
getPackageManager().getApplicationInfo(BuildConfig.APPLICATION_ID, 0);
|
||||
Shell.su("pm uninstall " + BuildConfig.APPLICATION_ID).submit();
|
||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
||||
}
|
||||
|
||||
// Dynamic detect all locales
|
||||
LocaleManager.loadAvailableLocales(R.string.app_changelog);
|
||||
|
||||
// Set default configs
|
||||
Config.initialize();
|
||||
|
||||
// Create notification channel on Android O
|
||||
Notifications.setup(this);
|
||||
|
||||
// Schedule periodic update checks
|
||||
Utils.scheduleUpdateCheck();
|
||||
CheckUpdates.check();
|
||||
|
||||
// Setup shortcuts
|
||||
Shortcuts.setup(this);
|
||||
|
||||
// Create repo database
|
||||
app.repoDB = new RepoDatabaseHelper(this);
|
||||
|
||||
// Magisk working as expected
|
||||
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
|
||||
// Load modules
|
||||
Utils.loadModules(false);
|
||||
// Load repos
|
||||
if (Networking.checkNetworkStatus(this))
|
||||
new UpdateRepos().exec();
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, ClassMap.get(MainActivity.class));
|
||||
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION));
|
||||
DONE = true;
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
72
app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt
Normal file
72
app/src/main/java/com/topjohnwu/magisk/ui/SplashActivity.kt
Normal file
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
open class SplashActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Shell.getShell {
|
||||
if (Info.magiskVersionCode > 0 && Info.magiskVersionCode < Const.MagiskVersion.MIN_SUPPORT) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.unsupport_magisk_title)
|
||||
.setMessage(R.string.unsupport_magisk_message)
|
||||
.setNegativeButton(R.string.ok, null)
|
||||
.setOnDismissListener { finish() }
|
||||
.show()
|
||||
} else {
|
||||
initAndStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAndStart() {
|
||||
val pkg = Config.suManager
|
||||
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
|
||||
get<SettingsDao>().delete(Config.Key.SU_MANAGER)
|
||||
Shell.su("pm uninstall $pkg").submit()
|
||||
}
|
||||
if (TextUtils.equals(pkg, packageName)) {
|
||||
runCatching {
|
||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
||||
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
|
||||
Shell.su("pm uninstall " + BuildConfig.APPLICATION_ID).submit()
|
||||
}
|
||||
}
|
||||
|
||||
// Set default configs
|
||||
Config.initialize()
|
||||
|
||||
// Create notification channel on Android O
|
||||
Notifications.setup(this)
|
||||
|
||||
// Schedule periodic update checks
|
||||
Utils.scheduleUpdateCheck()
|
||||
|
||||
// Setup shortcuts
|
||||
Shortcuts.setup(this)
|
||||
|
||||
val intent = Intent(this, ClassMap[MainActivity::class.java])
|
||||
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
|
||||
DONE = true
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
var DONE = false
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user